import { useCallback, useEffect, useState } from "react"; import { useHttpService } from "../services/HttpService.tsx"; import { environment } from "../environments/environment.ts"; import { relayServerFailure } from "../utils/fetchFailureMessage"; /** * Where the practice controls stand, and the two acts that move them. * * SEPARATE FROM THE BENCH RATHER THAN A WIDER CALL TO IT. The worklist is what somebody is standing * at, so it has to render when this cannot -- one request answering both would take the bench down * with the quality record. * * THE SERVER DECIDES WHAT IS OVERDUE. It knows when the practice day starts and which tests have work * in front of them; a browser asking either question would get a different answer in a different * timezone, and a control board that disagreed with the record would be worse than none. */ export interface ControlRun { runId?: string; orderableId?: string; test?: string; lot?: string; level?: string; outcome?: "pass" | "fail"; ranAt?: string; ranBy?: string; correctiveAction?: string; correctedAt?: string; correctedBy?: string; } export interface OverdueControl { orderableId?: string; test?: string; lastRunAt?: string; lastOutcome?: "pass" | "fail"; } /** Work on the bench today that no catalogue test matches, so no control can be matched to it. */ export interface UncheckedControl { test?: string; } export interface QualityControl { openFailures: ControlRun[]; overdue: OverdueControl[]; today: ControlRun[]; unchecked: UncheckedControl[]; } const EMPTY: QualityControl = { openFailures: [], overdue: [], today: [], unchecked: [] }; export const QUALITY_UNREADABLE = "Unable to load quality control. We cannot confirm the controls are current."; /** * A section the server did not send is unknown, never empty: the strip says the controls are current * when every section is empty, so a missing one would read as a clean bill of health. */ const toQualityControl = (response: Partial | undefined): QualityControl | undefined => Array.isArray(response?.openFailures) && Array.isArray(response?.overdue) && Array.isArray(response?.today) && Array.isArray(response?.unchecked) ? (response as QualityControl) : undefined; const ENDPOINT = `${environment.fhirClinicalApi}/diagnostic-quality-control`; export const useQualityControl = () => { const { get, post } = useHttpService(); const [quality, setQuality] = useState(EMPTY); const [loading, setLoading] = useState(true); const [error, setError] = useState(); const load = useCallback(async () => { setLoading(true); setError(undefined); try { const response = toQualityControl(await get>(ENDPOINT)); if (!response) { setError(QUALITY_UNREADABLE); return; } setQuality(response); } catch (err) { console.error("Error loading quality control:", err); setError(relayServerFailure(err, QUALITY_UNREADABLE)); } finally { setLoading(false); } }, [get]); useEffect(() => { void load(); }, [load]); const logRun = useCallback( async (run: { orderableId: string; lot: string; level?: string; outcome: "pass" | "fail" }, admit?: () => void) => { const created = await post(ENDPOINT, { orderableId: run.orderableId, lot: run.lot.trim(), ...(run.level?.trim() ? { level: run.level.trim() } : {}), outcome: run.outcome, }, ...(admit ? [{ admit }] : [])); // An answer without the run says nothing about it, so the record is read again. if (!created) { await load(); return; } const orderableId = created.orderableId ?? run.orderableId; // Only a pass covers the day; after a failure the test is still owed, as the server will say. setQuality((prev) => ({ openFailures: created.outcome === "fail" ? [created, ...prev.openFailures] : prev.openFailures, overdue: created.outcome === "pass" ? prev.overdue.filter((row) => row.orderableId !== orderableId) : prev.overdue.map((row) => row.orderableId === orderableId ? { ...row, lastRunAt: created.ranAt ?? row.lastRunAt, lastOutcome: created.outcome } : row), today: [created, ...prev.today], unchecked: prev.unchecked, })); }, [post, load], ); /** The only thing that closes a failure. The server refuses an empty action, and so does the form. */ const recordCorrectiveAction = useCallback( async (runId: string, action: string, admit?: () => void) => { await post(`${ENDPOINT}/${runId}/correction`, { action: action.trim() }, ...(admit ? [{ admit }] : [])); await load(); }, [post, load], ); return { quality, loading, error, reload: load, logRun, recordCorrectiveAction }; };