feat(products-ui): import run detail (§5.5, PUC-4/5/8)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,89 @@
|
||||
// Run detail (SD-0002 §5.5) — stub; Task 14 replaces this with the run report.
|
||||
// Run detail (SD-0002 §5.5) — report card for a completed or in-progress import run.
|
||||
// NO images section this slice (SLICE-7). PUC-4/5/8.
|
||||
import { useEffect, useState } from "react";
|
||||
import { getRun, type RunDetail as RunDetailType } from "../../productsApi";
|
||||
import { Banner } from "../../ui/kit";
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
applying: "Importing…",
|
||||
fetching_images: "Fetching images…",
|
||||
complete: "Complete",
|
||||
complete_with_problems: "Complete with problems",
|
||||
};
|
||||
|
||||
export default function RunDetail({ runId }: { runId: number }) {
|
||||
void runId;
|
||||
return null;
|
||||
const [run, setRun] = useState<RunDetailType | null>(null);
|
||||
const [loadFail, setLoadFail] = useState<"gone" | "failed" | null>(null);
|
||||
|
||||
async function load() {
|
||||
setLoadFail(null);
|
||||
const resp = await getRun(runId);
|
||||
if (!resp.ok) {
|
||||
setLoadFail(resp.status === 404 ? "gone" : "failed");
|
||||
return;
|
||||
}
|
||||
setRun(resp.value);
|
||||
}
|
||||
useEffect(() => {
|
||||
void load();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [runId]);
|
||||
|
||||
if (loadFail === "gone") {
|
||||
return (
|
||||
<Banner tone="attn" title="No such import run">
|
||||
<a href="#/products">← Products</a>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (loadFail === "failed") {
|
||||
return (
|
||||
<Banner tone="attn" title="Couldn't load this import run">
|
||||
Something went wrong on our side.{" "}
|
||||
<button type="button" className="linklike" onClick={() => void load()}>
|
||||
Retry
|
||||
</button>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (!run) return <p className="note">Loading…</p>;
|
||||
|
||||
const dialectLabel = run.dialect === "canonical" ? "Canonical format" : run.dialect;
|
||||
|
||||
return (
|
||||
<div className="products">
|
||||
<p className="note">
|
||||
<a href="#/products">← Products</a>
|
||||
</p>
|
||||
<h1>{run.file_name}</h1>
|
||||
<p className="note">
|
||||
Imported {new Date(run.created_at).toLocaleString()} by {run.by} · {dialectLabel}
|
||||
</p>
|
||||
<p className="note">
|
||||
{run.products_added} added · {run.products_updated} updated · {run.rows_errored} rows in
|
||||
error
|
||||
</p>
|
||||
<p className="note">{STATUS_LABELS[run.status] ?? run.status}</p>
|
||||
{run.errors.length > 0 && (
|
||||
<table className="errortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Line</th>
|
||||
<th>Column</th>
|
||||
<th>Problem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{run.errors.map((e, i) => (
|
||||
<tr key={i}>
|
||||
<td>{e.line}</td>
|
||||
<td>{e.column ?? ""}</td>
|
||||
<td>{e.message}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user