import { useEffect, useMemo, useState, type ChangeEvent } from "react"; import { AlertCircle, Loader2 } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../../components/ui/dialog.tsx"; import { Button } from "../../../../components/ui/button.tsx"; import { Input } from "../../../../components/ui/input.tsx"; import { Label } from "../../../../components/ui/label.tsx"; import { Checkbox } from "../../../../components/ui/checkbox.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../../components/ui/tabs.tsx"; import { Alert, AlertDescription } from "../../../../components/ui/alert.tsx"; import { relayServerFailure } from "../../../../utils/fetchFailureMessage"; import { useDiagnosticCatalogue } from "../../../../hooks/useDiagnosticCatalogue.ts"; import type { LabConnection, LabTestMappingChange } from "../../../../hooks/useLabConnections.ts"; import { applyImport, LOINC, MAX_IMPORT_BYTES, MAX_PAIRING_CHANGES, pairingChanges, pairingRows, parseCsv, type ImportOutcome, type PairingRow, } from "./labTestCodes.ts"; import "./DiagnosticsPage.css"; import { EmptyState } from "../../../../components/ui/empty-state"; export type TestCodesTarget = { connection: LabConnection } | undefined; interface Props { target: TestCodesTarget; onClose: () => void; saveTestMappings: (id: string, changes: LabTestMappingChange[]) => Promise; } const readText = (file: File) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result ?? "")); reader.onerror = () => reject(reader.error); reader.readAsText(file); }); const LabTestCodesDialog = ({ target, onClose, saveTestMappings }: Props) => target ? : null; const Pairing = ({ connection, onClose, saveTestMappings }: { connection: LabConnection; onClose: () => void; saveTestMappings: Props["saveTestMappings"]; }) => { const { orderables, loading, error: catalogueError } = useDiagnosticCatalogue(); const [rows, setRows] = useState([]); const [imported, setImported] = useState(); const [fileError, setFileError] = useState(); const [saving, setSaving] = useState(false); const [error, setError] = useState(); const [problems, setProblems] = useState>({}); const [saved, setSaved] = useState(false); useEffect(() => { if (!loading) setRows(pairingRows(orderables, connection.testMappings)); }, [loading, orderables, connection.testMappings]); const changes = useMemo(() => pairingChanges(rows), [rows]); const edit = (key: string, patch: Partial) => { setSaved(false); setRows((current) => current.map((row) => { if (row.key !== key) return row; const next = { ...row, ...patch }; // A changed code is a new pairing, so its review does not carry over. return patch.theirCode !== undefined && patch.theirCode.trim() !== row.theirCode.trim() ? { ...next, confirm: false } : next; })); }; const upload = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; setFileError(undefined); setImported(undefined); if (!file) return; if (file.size > MAX_IMPORT_BYTES) { setFileError("That file is larger than 1 MB. Split it and import each part."); return; } try { const outcome = applyImport(rows, parseCsv(await readText(file))); setRows(outcome.rows); setImported(outcome); setSaved(false); } catch (failure) { console.error("Error reading a laboratory catalogue file:", failure); setFileError("That file could not be read. Save it as CSV and try again."); } }; const submit = async () => { setSaving(true); setError(undefined); setProblems({}); try { const next = await saveTestMappings(connection.id as string, changes.map((each) => each.change)); setRows(pairingRows(orderables, next?.testMappings)); setImported(undefined); setSaved(true); } catch (failure) { const listed = (failure as { body?: { problems?: { row: number; message: string }[] } })?.body?.problems; if (Array.isArray(listed) && listed.length > 0) { setProblems(Object.fromEntries(listed.map((each) => [changes[each.row]?.key ?? "", each.message]))); setError("Some test codes could not be saved, so none were. Each problem is shown on its row."); } else { setError(relayServerFailure(failure, "The test codes could not be saved. Please try again.")); } } finally { setSaving(false); } }; const tooMany = changes.length > MAX_PAIRING_CHANGES; return ( { if (!open && !saving) onClose(); }}> {(connection.name ?? "Laboratory") + " \u00b7 catalogue & test codes"} Match each test to this laboratory's order code. Electronic ordering is available only after someone reviews the match. {error ? ( {error} ) : null} {saved ?

Test codes saved.

: null} Upload spreadsheet Enter manually void upload(e)} />

Three columns: their order code, their test name, LOINC code. A line is matched by its LOINC code only. Nothing is marked reviewed until you tick it.

{fileError ?

{fileError}

: null} {imported ? : null}

Type their order code beside each test, then tick the ones you have checked.

{loading ? (

{"Loading your test catalogue\u2026"}

) : catalogueError ? (

{catalogueError}

) : rows.length === 0 ? ( ) : (
{rows.map((row) => ( ))}
Reviewed Test in Akashic Standard code Their order code
)}

Check specimen requirements, units and order questions before approving a match. A similar name alone is not a match. Clearing a code removes that pairing.

{tooMany ? (

{"Save at most " + MAX_PAIRING_CHANGES + " changes at a time."}

) : null}
); }; const standardCode = (row: PairingRow) => (row.system === LOINC ? "LOINC " : "") + row.code; const PairingLine = ({ row, problem, edit }: { row: PairingRow; problem?: string; edit: (key: string, patch: Partial) => void; }) => { // A saved review stays until the code changes; to withdraw it, change or clear the code. const keepsReview = Boolean(row.stored?.reviewedAt) && row.stored?.theirCode === row.theirCode.trim(); return ( edit(row.key, { confirm: checked === true })} /> {row.name} {standardCode(row)} {standardCode(row)} edit(row.key, { theirCode: e.target.value })} /> {problem ?

{problem}

: null} ); }; const ImportSummary = ({ outcome }: { outcome: ImportOutcome }) => (

{(outcome.matched === 1 ? "1 test was" : outcome.matched + " tests were") + " filled from the file. Review each one and tick it before saving."}

{outcome.unused.length > 0 ? ( <>

{outcome.unused.length === 1 ? "1 line was not used:" : outcome.unused.length + " lines were not used:"}

    {outcome.unused.slice(0, 50).map((each) => (
  • {"Line " + each.line + ": " + [each.theirCode, each.theirName].filter(Boolean).join(" ") + " (" + each.reason + ")"}
  • ))}
{outcome.unused.length > 50 ? (

{"And " + (outcome.unused.length - 50) + " more."}

) : null} ) : null}
); export default LabTestCodesDialog;