import { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { AlertCircle } from "lucide-react"; import type { Patient } from "fhir/r4"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../components/ui/dialog.tsx"; import { Alert, AlertDescription } from "../../../components/ui/alert.tsx"; import { Button } from "../../../components/ui/button.tsx"; import PatientSearch from "../../../components/PatientSearch/PatientSearch"; import { formatFhirHumanName } from "../../../utils/fhirHumanName"; // `formatDate` parses a date-only value as LOCAL midnight, which matters here more than // anywhere: a birth date rendered a day west of UTC is the field somebody is comparing. import { formatDate } from "../../../utils/dateUtils"; import { relayServerFailure } from "../../../utils/fetchFailureMessage"; import { useAuthServiceContext } from "../../../services"; import type { InboundRequisition, RequisitionCandidate } from "../../../hooks/useInboundRequisitions.ts"; import { patientSearchHandle } from "../../../utils/patientSearchTerm"; import { getPractitionerRoleWorkspaces, STAFF_WORKSPACES } from "../../../utils/staffRoleCatalog"; import ReceptionistRegisterPatientDialog, { type RegisterPatientPrefill, } from "../ReceptionistHomePage/ReceptionistRegisterPatientDialog.tsx"; import "./MatchRequisitionDialog.css"; // SAYING WHO THE PAPER IS ABOUT, which is the one act this whole slice exists to make deliberate. // // THE TWO ARE SHOWN SIDE BY SIDE AND THE SCREEN COMPARES NOTHING. What the sender wrote sits next to // what the record says, and a person reads both. Two people share a name and a town; the one who does // not share a date of birth is somebody else's blood test, so the comparison is a human's to make and // a highlighted "match" would be a machine making it in everything but name. // // A MISSING DATE OF BIRTH IS SAID OUT LOUD rather than left blank. A blank reads as agreement, and // this is the field that separates two people with one name -- somebody matching on a name alone // should know that is what they are doing. // // A new patient is registered through the shared dialog and still has to be matched here. interface Props { requisition?: InboundRequisition; onClose: () => void; onMatched: () => void; match: (requisitionId: string, patientId: string) => Promise; findCandidates: (requisitionId: string, handle: { name?: string; phone?: string }) => Promise; } const FULL_PHONE_DIGITS = 10; /** The name as written, split at its last space, as a starting point the registrar corrects. */ const prefillFrom = (requisition: InboundRequisition): RegisterPatientPrefill => { const written = (requisition.patientName ?? "").trim(); const at = written.lastIndexOf(" "); return at > 0 ? { firstName: written.slice(0, at), lastName: written.slice(at + 1) } : { lastName: written }; }; const MatchRequisitionDialog = ({ requisition, onClose, onMatched, match, findCandidates }: Props) => { // `PatientSearch` is a form control and wants a form around it. Nothing here is submitted through // react-hook-form; the provider exists so the shared control can be reused rather than rebuilt. const form = useForm(); // Registration admits the Practitioner and Receptionist roles the token carries for this practice, // whichever workspace is being worn, so the door follows the roles and not the hat. const { practitionerRoles, getCognitoGroup, currentOrganization } = useAuthServiceContext(); const mayRegister = practitionerRoles.some((role) => role.active !== false && role.organization?.reference?.split("/").pop() === currentOrganization?.id && getPractitionerRoleWorkspaces(role, getCognitoGroup(role)) .some((workspace) => workspace === STAFF_WORKSPACES.PRACTITIONER || workspace === STAFF_WORKSPACES.RECEPTIONIST)); const [patient, setPatient] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(); const [registering, setRegistering] = useState(false); const [registered, setRegistered] = useState(false); if (!requisition?.requisitionId) return null; const searchCandidates = async (term: string): Promise => { const handle = patientSearchHandle(term); if (handle.kind === "birthdate" || (handle.kind === "phone" && handle.value.length < FULL_PHONE_DIGITS)) return []; const found = await findCandidates(requisition.requisitionId as string, { [handle.kind]: handle.value }); return found.map(({ id, name, birthDate }) => ({ resourceType: "Patient" as const, id, ...(name ? { name: [{ text: name }] } : {}), ...(birthDate ? { birthDate } : {}), })); }; const submit = async () => { if (!patient?.id) return; setBusy(true); setError(undefined); try { await match(requisition.requisitionId as string, patient.id); onMatched(); onClose(); } catch (failure) { console.error("Error matching an inbound requisition:", failure); setError(relayServerFailure(failure, "That could not be matched. Nothing was changed.")); } finally { setBusy(false); } }; return ( <> { if (!open && !busy) onClose(); }}> Match patient Match this requisition to a patient record, then confirm. {error ? ( {error} ) : null}

On the requisition

{requisition.patientName}

{requisition.patientBirthDate ? `Born ${formatDate(requisition.patientBirthDate)}` : "Date of birth not provided"}

{requisition.patientBirthDate ? null : (

Confirm missing identity details with the sender.

)}

{`From ${requisition.from}`}

Choose the patient record

Compare identity details before confirming. No record is selected automatically.

{patient ? ( <>

{formatFhirHumanName(patient.name)}

{patient.birthDate ? `Born ${formatDate(patient.birthDate)}` : "No date of birth recorded"}

) : (

Nobody chosen yet

)}
{registered ? (

The new patient is registered but not matched. Compare the details, then confirm the match.

) : null} {!patient && !mayRegister ? (

No match? Leave it pending and ask staff who can register patients to add this person, then return here to match them.

) : null} {!patient && mayRegister ? (

No match? Leave it pending, or register the patient and return here to match them.

) : null}

Confirmation matches identity only; review and accept the order separately.

{ setRegistering(false); setRegistered(true); setPatient(created); }} /> ); }; export default MatchRequisitionDialog;