- LabDraft.tsx discard button: chain .catch to surface a failed discard in the existing error banner instead of an unhandled rejection. - LabDraw.tsx toggleHistory: wrap the marker-history fetch in try/catch so a failed request silently no-ops (the row just doesn't expand) rather than throwing unhandled from the click handler.
100 lines
4.2 KiB
TypeScript
100 lines
4.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router";
|
|
import type { ExtractedMarker } from "@helios/shared";
|
|
import { api } from "../api";
|
|
|
|
export function LabDraft() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const nav = useNavigate();
|
|
const [meta, setMeta] = useState<{ filename: string; status: string; error: string | null } | null>(null);
|
|
const [collectedDate, setCollectedDate] = useState("");
|
|
const [labName, setLabName] = useState("");
|
|
const [markers, setMarkers] = useState<ExtractedMarker[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
api.labDraft(id).then((r) => {
|
|
setMeta({ filename: r.filename, status: r.status, error: r.error });
|
|
if (r.draft) {
|
|
setCollectedDate(r.draft.collectedDate ?? "");
|
|
setLabName(r.draft.labName ?? "");
|
|
setMarkers(r.draft.markers);
|
|
}
|
|
}).catch((e) => setError(e.message));
|
|
}, [id]);
|
|
|
|
const edit = (i: number, patch: Partial<ExtractedMarker>) =>
|
|
setMarkers((m) => m.map((row, j) => (j === i ? { ...row, ...patch } : row)));
|
|
const remove = (i: number) => setMarkers((m) => m.filter((_, j) => j !== i));
|
|
const addRow = () => setMarkers((m) => [...m, { panel: null, name: "", value: "", unit: null, referenceRange: null, flagged: false }]);
|
|
|
|
const confirm = async () => {
|
|
if (!id) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const { drawId } = await api.confirmDraft(id, { collectedDate, labName: labName || null, markers });
|
|
nav(`/labs/draw/${drawId}`);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "confirm failed");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
if (!meta) return <p>Loading…</p>;
|
|
return (
|
|
<section>
|
|
<h2>Review: {meta.filename}</h2>
|
|
{meta.error && <p className="error">Extraction failed: {meta.error}. You can still enter values manually below.</p>}
|
|
{meta.status !== "pending" && <p className="muted">This draft is already {meta.status}.</p>}
|
|
<p className="hint">Check every value against the PDF (<a href={`/api/labs/drafts/${id}/file`} target="_blank" rel="noreferrer">open original</a>). Nothing is saved until you confirm.</p>
|
|
|
|
<div className="row">
|
|
<label>Collected <input type="date" value={collectedDate} onChange={(e) => setCollectedDate(e.target.value)} /></label>
|
|
<label>Lab <input value={labName} onChange={(e) => setLabName(e.target.value)} placeholder="lab name" /></label>
|
|
</div>
|
|
|
|
<table className="review-table">
|
|
<thead><tr><th>Name</th><th>Value</th><th>Unit</th><th>Range</th><th>Flag</th><th /></tr></thead>
|
|
<tbody>
|
|
{markers.map((m, i) => (
|
|
<tr key={i}>
|
|
<td><input value={m.name} onChange={(e) => edit(i, { name: e.target.value })} /></td>
|
|
<td><input value={m.value} onChange={(e) => edit(i, { value: e.target.value })} /></td>
|
|
<td><input value={m.unit ?? ""} onChange={(e) => edit(i, { unit: e.target.value || null })} /></td>
|
|
<td><input value={m.referenceRange ?? ""} onChange={(e) => edit(i, { referenceRange: e.target.value || null })} /></td>
|
|
<td><input type="checkbox" checked={m.flagged} onChange={(e) => edit(i, { flagged: e.target.checked })} /></td>
|
|
<td><button className="link" onClick={() => remove(i)}>✕</button></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
<button className="link" onClick={addRow}>+ add marker</button>
|
|
|
|
{error && <p className="error">{error}</p>}
|
|
{meta.status === "pending" && (
|
|
<div className="row">
|
|
<button disabled={busy || !collectedDate || markers.length === 0} onClick={confirm}>
|
|
{busy ? "Saving…" : `Confirm ${markers.length} markers`}
|
|
</button>
|
|
<button
|
|
className="danger"
|
|
onClick={() =>
|
|
id &&
|
|
api
|
|
.discardDraft(id)
|
|
.then(() => nav("/labs"))
|
|
.catch((e) => setError(e instanceof Error ? e.message : "discard failed"))
|
|
}
|
|
>
|
|
Discard
|
|
</button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|