import type { LabTestMapping, LabTestMappingChange } from "../../../../hooks/useLabConnections.ts"; import type { OrderableRow } from "../../../../hooks/useDiagnosticCatalogue.ts"; // Pairing our catalogue tests with a laboratory's own order codes. A line from an import is matched // only by its standard code, never by name, and nothing is reviewed until a person ticks it. export const LOINC = "http://loinc.org"; const LOINC_SHAPE = /^\d{1,7}-\d$/; export const MAX_IMPORT_BYTES = 1_000_000; /** The server refuses more in one save. */ export const MAX_PAIRING_CHANGES = 500; export interface PairingRow { key: string; name: string; system: string; code: string; theirCode: string; confirm: boolean; stored?: LabTestMapping; } export interface UnusedLine { line: number; theirCode: string; theirName: string; reason: string; } export interface ImportOutcome { rows: PairingRow[]; matched: number; unused: UnusedLine[]; } const keyOf = (system: string, code: string) => system + "|" + code; export const isReviewed = (mapping?: LabTestMapping) => Boolean(mapping?.reviewedAt); /** One row per laboratory test that carries a standard code; a test without one cannot be paired. */ export const pairingRows = (orderables: OrderableRow[], stored: LabTestMapping[] = []): PairingRow[] => { const byKey = new Map(stored.map((each) => [keyOf(each.system, each.code), each])); return orderables .filter((row) => (row.kind ?? "laboratory") === "laboratory" && row.testCode) .map((row) => { const system = row.codeSystem || LOINC; const code = row.testCode as string; const mapping = byKey.get(keyOf(system, code)); return { key: keyOf(system, code), name: row.name ?? code, system, code, theirCode: mapping?.theirCode ?? "", confirm: isReviewed(mapping), stored: mapping, }; }) .sort((a, b) => a.name.localeCompare(b.name)); }; export const parseCsv = (text: string): string[][] => { const rows: string[][] = []; let row: string[] = []; let cell = ""; let quoted = false; for (let at = 0; at < text.length; at += 1) { const ch = text[at]; if (quoted) { if (ch !== '"') cell += ch; else if (text[at + 1] === '"') { cell += ch; at += 1; } else quoted = false; } else if (ch === '"') { quoted = true; } else if (ch === ",") { row.push(cell); cell = ""; } else if (ch === "\n" || ch === "\r") { if (ch === "\r" && text[at + 1] === "\n") at += 1; row.push(cell); rows.push(row); row = []; cell = ""; } else { cell += ch; } } if (cell || row.length) { row.push(cell); rows.push(row); } return rows.map((each) => each.map((value) => value.trim())).filter((each) => each.some(Boolean)); }; /** * Fills their order code on the rows a line names by LOINC, and says why every other line was not * used. A filled row is left unticked: the file is a proposal, not a review. */ export const applyImport = (rows: PairingRow[], lines: string[][]): ImportOutcome => { const heading = /code|name|loinc/i.test((lines[0] ?? []).join(" ")); const body = heading ? lines.slice(1) : lines; const first = heading ? 2 : 1; const byLoinc = new Map(rows.filter((row) => row.system === LOINC).map((row) => [row.code, row])); const filled = new Map(); const unused: UnusedLine[] = []; body.forEach(([theirCode = "", theirName = "", standard = ""], index) => { const skip = (reason: string) => unused.push({ line: index + first, theirCode, theirName, reason }); if (!theirCode) return skip("No order code"); if (theirCode.length > 64) return skip("Order code longer than 64 characters"); if (!standard) return skip("No LOINC code, and a name alone is not a match"); if (!LOINC_SHAPE.test(standard)) return skip("Not a LOINC code"); const row = byLoinc.get(standard); if (!row) return skip("No test in your catalogue has this LOINC code"); if (filled.has(row.key) && filled.get(row.key) !== theirCode) { return skip("An earlier line already gave " + row.name + " a code"); } filled.set(row.key, theirCode); }); return { rows: rows.map((row) => { const theirCode = filled.get(row.key); return theirCode === undefined || theirCode === row.theirCode ? row : { ...row, theirCode, confirm: false }; }), matched: filled.size, unused, }; }; /** What the save sends: a cleared code removes the pairing, and an untouched row sends nothing. */ export interface PairingChange { key: string; change: LabTestMappingChange; } export const pairingChanges = (rows: PairingRow[]): PairingChange[] => rows.flatMap((row): PairingChange[] => { const theirCode = row.theirCode.trim(); const { stored } = row; if (!theirCode) { return stored ? [{ key: row.key, change: { system: row.system, code: row.code, remove: true } }] : []; } if (stored?.theirCode === theirCode && (isReviewed(stored) || !row.confirm)) return []; return [{ key: row.key, change: { system: row.system, code: row.code, theirCode, confirm: row.confirm } }]; });