import { HeartPulse } from "lucide-react"; import { Patient, PatientContact } from "fhir/r4"; import { ProfileField, ProfileInput, ProfileRow, ProfileRows } from "./ProfileFields"; import { ProfileSection } from "./ProfileSection"; import { type EmergencyContactDraft } from "../../utils/patientFieldEditors"; import { formatFhirHumanName } from "../../utils/fhirHumanName"; import { getFirstContactPointBySystem } from "../../utils/fhirContactPoint"; import { formatPhoneNumber } from "../../utils/phoneNumber"; import { Input } from "../ui/input"; import { findContainedPhysician, formatPhysician, physicianToDraft, setPrimaryPhysician, type PhysicianDraft, } from "./primaryPhysician"; import { useSectionEditor, type SectionUpdatePatient } from "./useSectionEditor"; /** What Who to contact edits, compared at save time against the latest read. */ const whoToContactOf = (patient: Patient) => ({ contact: patient.contact, physician: findContainedPhysician(patient), localReferences: (patient.generalPractitioner ?? []).filter((ref) => ref.reference?.startsWith("#")), }); interface EmergencyContactsProps { /** The signed-in patient is reading their own record, so the notice says "You". */ ownRecord?: boolean; patient: Patient; updatePatient: SectionUpdatePatient; onSeen?: (patient: Patient) => void; submitting: boolean; /** Shows the section without its Edit control. */ readOnly?: boolean; setSubmitting: (submitting: boolean) => void; } // FHIR patient-contact relationship: C = Emergency Contact (v2-0131). The pre-migration // multi-contact CRUD dialog always wrote this coding; new contacts created here default to // it too so a text-only relationship edit never leaves a contact with no coding at all. const EMERGENCY_CONTACT_CODING = { system: "http://terminology.hl7.org/CodeSystem/v2-0131", code: "C", display: "Emergency Contact", }; // Map a single `Patient.contact[]` entry to the flat editor draft. const emergencyContactToDraft = ( contact: PatientContact | undefined, ): EmergencyContactDraft => { const relationship = contact?.relationship?.[0]?.text ?? contact?.relationship?.[0]?.coding?.[0]?.display ?? ""; return { given: contact?.name?.given?.[0] ?? "", family: contact?.name?.family ?? "", relationship, phone: getFirstContactPointBySystem(contact?.telecom, "phone")?.value ?? "", email: getFirstContactPointBySystem(contact?.telecom, "email")?.value ?? "", }; }; // Human-readable read-state row for a single `Patient.contact[]` entry. const formatEmergencyContact = (contact: PatientContact | undefined): string => { const name = formatFhirHumanName(contact?.name, { emptyText: "" }); const relationship = contact?.relationship?.[0]?.text ?? contact?.relationship?.[0]?.coding?.[0]?.display ?? ""; const phone = getFirstContactPointBySystem(contact?.telecom, "phone")?.value; const email = getFirstContactPointBySystem(contact?.telecom, "email")?.value; return [name, relationship, phone ? formatPhoneNumber(phone) : "", email ?? ""] .filter(Boolean) .join(" ยท "); }; // Label per contact row; a contact with no name yet reads by its position. const contactLabel = (contact: PatientContact | undefined, index: number): string => { const name = formatFhirHumanName(contact?.name, { emptyText: "" }); return name || `Emergency contact ${index + 1}`; }; // Replace contact[index] on a full Patient draft, preserving that entry's non-edited props // (address/gender/organization/period and any non-phone/non-email telecom) plus every OTHER // contact entry untouched. const setEmergencyContactAt = ( draft: Patient, index: number, ec: EmergencyContactDraft, ): void => { const list = [...(draft.contact ?? [])]; const existing = list[index] ?? {}; const given = ec.given.trim(); const family = ec.family.trim(); const relationship = ec.relationship.trim(); const phone = ec.phone.trim(); const email = ec.email.trim(); const contact: PatientContact = { ...existing }; contact.name = given || family ? { ...(contact.name ?? {}), given: given ? [given] : undefined, family: family || undefined, text: [given, family].filter(Boolean).join(" ") || undefined, } : undefined; const telecom = [...(contact.telecom ?? [])]; const phoneIndex = telecom.findIndex((point) => point.system === "phone"); if (phone) { if (phoneIndex >= 0) telecom[phoneIndex] = { ...telecom[phoneIndex], value: phone }; else telecom.push({ system: "phone", value: phone }); } else if (phoneIndex >= 0) { telecom.splice(phoneIndex, 1); } const emailIndex = telecom.findIndex((point) => point.system === "email"); if (email) { if (emailIndex >= 0) telecom[emailIndex] = { ...telecom[emailIndex], value: email }; else telecom.push({ system: "email", value: email }); } else if (emailIndex >= 0) { telecom.splice(emailIndex, 1); } contact.telecom = telecom.length ? telecom : undefined; // Never drop an existing v2-0131 coding on a text-only edit; new contacts that don't have // one yet default to the "C"/Emergency Contact coding, matching the old CRUD dialog's behavior. const existingCoding = existing.relationship?.[0]?.coding; contact.relationship = relationship ? [{ coding: existingCoding ?? [EMERGENCY_CONTACT_CODING], text: relationship }] : existingCoding ? [{ coding: existingCoding }] : undefined; list[index] = contact; draft.contact = list; }; type ContactRow = { source: number | null; value: EmergencyContactDraft }; const EMPTY_CONTACT: EmergencyContactDraft = { given: "", family: "", relationship: "", phone: "", email: "" }; const isBlank = (value: EmergencyContactDraft) => !value.given.trim() && !value.family.trim() && !value.phone.trim() && !value.email.trim(); const CONTACT_FIELDS: { key: keyof EmergencyContactDraft; label: string; type?: string }[] = [ { key: "given", label: "First name" }, { key: "family", label: "Last name" }, { key: "relationship", label: "Relationship" }, { key: "phone", label: "Phone", type: "tel" }, { key: "email", label: "Email", type: "email" }, ]; const PHYSICIAN_FIELDS:{ key: keyof PhysicianDraft; label: string; type?: string }[] = [ { key: "name", label: "Primary physician" }, { key: "specialty", label: "Specialty" }, { key: "phone", label: "Physician phone", type: "tel" }, { key: "city", label: "Physician city" }, ]; type WhoToContactDraft = { rows: ContactRow[]; physician: PhysicianDraft }; /** The form, filled from a record: one row per contact (or one empty row), and the physician. */ const whoToContactDraftOf = (patient: Patient): WhoToContactDraft => { const contacts = patient.contact ?? []; return { rows: contacts.length ? contacts.map((contact, index) => ({ source: index, value: emergencyContactToDraft(contact) })) : [{ source: null, value: EMPTY_CONTACT }], physician: physicianToDraft(findContainedPhysician(patient)), }; }; /** Emergency contacts and the primary physician, edited as one section and saved together. */ const EmergencyContacts = ({ patient, updatePatient, onSeen, submitting, readOnly = false, ownRecord = false }: EmergencyContactsProps) => { const contacts = patient.contact ?? []; const savedPhysician = physicianToDraft(findContainedPhysician(patient)); // Saved over the record as it is now, only while this section is as it was when editing opened; a write that // loses a race is read and checked again, never written over another change here. See useSectionEditor. const { editing, draft, setDraft, opening, saving, error, stale, reloading, open, cancel, save, reload } = useSectionEditor({ patient, pick: whoToContactOf, draftOf: whoToContactDraftOf, apply: (next, { rows, physician }, opening) => { // A row's source is its place in the record the form was filled from, whichever record is saved over now. const base = opening.record.contact ?? []; const kept = rows.filter((row) => !isBlank(row.value)); next.contact = kept.map((row) => row.source !== null && base[row.source] ? JSON.parse(JSON.stringify(base[row.source])) as PatientContact : { relationship: [{ coding: [EMERGENCY_CONTACT_CODING] }] }); kept.forEach((row, index) => setEmergencyContactAt(next, index, row.value)); if (!next.contact.length) next.contact = undefined; setPrimaryPhysician(next, physician); return next; }, updatePatient, onSeen, failure: "Who to contact was not saved", }); const rows = draft?.rows ?? []; const physician = draft?.physician ?? savedPhysician; const setRows = (next: ContactRow[]) => setDraft({ ...draft, rows: next }); const setPhysician = (next: PhysicianDraft) => setDraft({ ...draft, physician: next }); return (