import { Check, X } from "lucide-react"; import type { MatchCandidate } from "../../../services/PatientService"; // - CONFIRM, DON'T REVEAL. // // Every value rendered on this card is a value the RECEPTIONIST typed into the register-patient // form. Nothing here is read off the matched patient's own record. That is not a rendering // choice - it is the whole reason this feature is allowed to exist. // // The server's matcher can hit on phone alone, and the product's own patient-facing copy explains // why: "someone in your household used your phone number." A candidate returned on a phone-only // match is frequently a DIFFERENT human - a family member who is not in the room and has not // consented to anything. Rendering their name or date of birth here would be a disclosure with no // patient anywhere in the loop. // // The server already enforces this: it sends back only the caller's own submitted values plus a // matched/differs verdict, and a `hidden` marker for any field the receptionist did not submit. // This component must not invent a way around that - skip `hidden` fields entirely, never render a // placeholder for them, and never surface `candidateToken` (it is opaque, pass-through only). // // If a future change needs the candidate's own data, that needs a server contract change and a new // product decision - not a new field added here. const LABELS: Record = { firstname: "First name", lastname: "Last name", phone: "Phone", birthDate: "Date of birth", email: "Email", postalCode: "ZIP", }; const ORDER = ["firstname", "lastname", "phone", "birthDate", "email", "postalCode"] as const; interface Props { candidate: MatchCandidate; onLink: () => void; onDismiss: () => void; linking: boolean; allowCreate?: boolean; } export default function DuplicateCandidateCard({ candidate, onLink, onDismiss, linking, allowCreate = true }: Props) { // A phone-only hit is the household case the matcher is known to produce. Steer AWAY from linking: // the cost of a wrong link is a stranger's record adopted into this practice. // // This must be derived from the FIELD VERDICTS rendered above, not from `candidate.ruleHits`. // `ruleHits` records which matching RULE fired server-side, and the name rule requires name + DOB // together - so a submitted record can visibly match on both first AND last name (each rendered // with a "matches" checkmark two lines up) while `ruleHits` is still `["phone"]`, because the DOB // differed and the combined name+DOB rule never fired. Reading `ruleHits` here made the card tell // the receptionist "only the phone number matched" in the SAME breath it showed both names matching // - the wrong direction to be wrong, since it nudges toward "Different person - create new", i.e. // creating the very duplicate this card exists to prevent. The literal claim is only true when // phone is the ONLY field the card itself displays a match for. const matchedFieldNames = ORDER.filter((name) => { const field = candidate.fields[name]; return field.submitted && field.matches; }); const phoneOnly = matchedFieldNames.length === 1 && matchedFieldNames[0] === "phone"; // The one case where the card is not guessing. Everything else here is a possible match the // receptionist has to judge; this is the address the person signs in with, and the server will // refuse to create a second record for it either way. const sameAccount = candidate.sameAccount === true; return (

{sameAccount ? "This person already has an Akashic account" : "Possible existing record"}

{sameAccount ? "That email address is how they sign in, so this is them. Add them to this practice rather than creating a second record." : "Someone is already registered with Akashic whose details overlap with what you entered."}

{ORDER.map((name) => { const field = candidate.fields[name]; if (!field?.submitted) return null; return (
{LABELS[name]}
{field.value}
{field.matches ? ( ) : ( )}
); })}
{/* NOT ASKED WHEN THE ANSWER IS ALREADY KNOWN. "Have you used Akashic at another practice before?" is the right question for a possible match and a strange one to put to somebody whose own email address is on the screen matching. */} {phoneOnly && !sameAccount && (

Only the phone number matched. This is often another member of the same household rather than the same person — check before you add them.

)} {!sameAccount && (

Ask the patient whether they have used Akashic at another practice before.

)} {/* SAYS WHAT THE PRACTICE GETS, because the button used to imply far more than it grants. Adding somebody here is an org-scoped adoption: the affiliation tag makes them findable and bookable, and `clinicalAuthorized` stays false - so nobody here can open the chart until the patient authorizes it, and it will say "Not authorized" straight afterwards. A sentence that let a receptionist believe otherwise would be a worse defect than the wording it replaced. */}

They join this practice's patient list. You will still need their authorization before anyone here can open their record.

{/* "LINK" IS THE WORD FOR SOMETHING ELSE AND MUST NOT APPEAR HERE. Merging two ACCOUNTS - picking a survivor and disabling the other login - is patient-only, behind a one-time code, and no staff role can reach it. This button does not do that and never could, but it said "link", and two readers independently concluded on the same day that staff were merging people's records across organizations. One raised it as a spine violation. The behaviour was always right; the label named the forbidden neighbour. */}
{/* BOTH HALVES THE SAME VARIANT, and the sameness is the point rather than the colour. One creates a second patient record and the other joins an existing person; neither is safe and neither is ours to recommend. Painting the join blue and the split grey was the product taking a clinical position in paint. */} {!sameAccount && allowCreate && ( )}

Adding them is recorded against your name.

); }