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 }) {
|
export default function RunDetail({ runId }: { runId: number }) {
|
||||||
void runId;
|
const [run, setRun] = useState<RunDetailType | null>(null);
|
||||||
return 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