import type { Patient, Practitioner, Reference } from "fhir/r4"; // The patient-reported physician is a Practitioner contained in the Patient, referenced as #pcp1 from // generalPractitioner. Real Practitioner/ references and other contained resources are left alone. const PCP_LOCAL_ID = "pcp1"; export interface PhysicianDraft { name: string; specialty: string; phone: string; city: string; } export const EMPTY_PHYSICIAN: PhysicianDraft = { name: "", specialty: "", phone: "", city: "" }; const isLocalReference = (ref: Reference): boolean => Boolean(ref.reference?.startsWith("#")); export const findContainedPhysician = (patient: Patient): Practitioner | undefined => { const localRef = (patient.generalPractitioner ?? []).find(isLocalReference); if (!localRef?.reference) return undefined; const localId = localRef.reference.substring(1); const contained = (patient.contained ?? []).find((resource) => resource.id === localId); return contained?.resourceType === "Practitioner" ? (contained as Practitioner) : undefined; }; export const physicianToDraft = (physician: Practitioner | undefined): PhysicianDraft => physician ? { name: physician.name?.[0]?.text ?? "", specialty: physician.qualification?.[0]?.code?.text ?? "", phone: physician.telecom?.find((point) => point.system === "phone")?.value ?? "", city: physician.address?.[0]?.city ?? "", } : EMPTY_PHYSICIAN; const trimmedDraft = (draft: PhysicianDraft): PhysicianDraft => ({ name: draft.name.trim(), specialty: draft.specialty.trim(), phone: draft.phone.trim(), city: draft.city.trim(), }); /** The physician as recorded, with only the fields the form changed rewritten; everything else on it is kept. */ const withChanges = (physician: Practitioner, was: PhysicianDraft, now: PhysicianDraft): Practitioner => { const next: Practitioner = JSON.parse(JSON.stringify(physician)); if (now.name !== was.name) { // The structured parts described the old name, so they go with it; the entry's use and period stay. const entry = { ...next.name?.[0], text: now.name }; delete entry.given; delete entry.family; delete entry.prefix; delete entry.suffix; next.name = [entry, ...(next.name ?? []).slice(1)]; } if (now.specialty !== was.specialty) { const [first, ...others] = next.qualification ?? []; next.qualification = now.specialty ? [{ ...first, code: { text: now.specialty } }, ...others] : others; if (!next.qualification.length) delete next.qualification; } if (now.phone !== was.phone) { const telecom = [...(next.telecom ?? [])]; const at = telecom.findIndex((point) => point.system === "phone"); if (now.phone && at >= 0) telecom[at] = { ...telecom[at], value: now.phone }; else if (now.phone) telecom.push({ system: "phone", value: now.phone }); else if (at >= 0) telecom.splice(at, 1); next.telecom = telecom.length ? telecom : undefined; if (!next.telecom) delete next.telecom; } if (now.city !== was.city) { const [first, ...others] = next.address ?? []; const address = { ...first, city: now.city || undefined }; if (!address.city) delete address.city; next.address = Object.keys(address).length ? [address, ...others] : others; if (!next.address.length) delete next.address; } return next; }; /** * Writes the physician onto a Patient draft; a blank name removes the patient-reported physician. Only what the form * changed is written: the recorded physician's other details (a street address, an identifier, another number) are * kept, and an unchanged physician is left exactly as recorded. */ export const setPrimaryPhysician = (next: Patient, draft: PhysicianDraft): void => { const current = findContainedPhysician(next); const was = trimmedDraft(physicianToDraft(current)); const now = trimmedDraft(draft); if (JSON.stringify(was) === JSON.stringify(now)) return; const localId = current?.id ?? PCP_LOCAL_ID; if (!now.name) { const contained = (next.contained ?? []).filter((resource) => resource.id !== localId); const references = (next.generalPractitioner ?? []).filter((ref) => !isLocalReference(ref)); next.contained = contained.length ? contained : undefined; next.generalPractitioner = references.length ? references : undefined; return; } // Changed in place, where it is recorded, so nothing else in either list moves. const physician: Practitioner = current ? withChanges(current, was, now) : { resourceType: "Practitioner", id: localId, name: [{ text: now.name }], ...(now.specialty && { qualification: [{ code: { text: now.specialty } }] }), ...(now.phone && { telecom: [{ system: "phone" as const, value: now.phone }] }), ...(now.city && { address: [{ city: now.city }] }), }; next.contained = current ? (next.contained ?? []).map((resource) => (resource.id === localId ? physician : resource)) : [...(next.contained ?? []), physician]; const references = [...(next.generalPractitioner ?? [])]; const at = references.findIndex(isLocalReference); const reference: Reference = { ...(at >= 0 ? references[at] : {}), reference: `#${localId}`, type: "Practitioner", display: now.name }; if (at >= 0) references[at] = reference; else references.push(reference); next.generalPractitioner = references; }; export const formatPhysician = (draft: PhysicianDraft, format: (phone: string) => string): string => [draft.name, draft.specialty, draft.phone ? format(draft.phone) : "", draft.city].filter(Boolean).join(" ยท ");