From fa6c55421c9d5d848c5953a3ed0f05b453af7b34 Mon Sep 17 00:00:00 2001 From: Gitead Date: Wed, 14 Oct 2026 23:13:27 -0400 Subject: [PATCH] feat: picking a policy holder copies their insurance, address and status Selecting a policy holder from the patient search fills the dependent's insurance type, member ID, group number, status, address, city, state and zip from the holder's record. Dependents may now share the holder's insurance ID without the duplicate-ID check blocking the save. Also teach the AI CDT lookup that "PNAO" is a typo of "pano" (D0330). Co-Authored-By: Claude Opus 5.5 --- apps/Backend/src/ai/cdt-lookup.ts | 1 + apps/Backend/src/routes/patients.ts | 10 ++++++++-- apps/Backend/src/storage/patients-storage.ts | 10 ++++++++++ .../src/components/patients/patient-form.tsx | 20 ++++++++++++++++++- .../patients/policy-holder-search.tsx | 5 +++-- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/apps/Backend/src/ai/cdt-lookup.ts b/apps/Backend/src/ai/cdt-lookup.ts index 85cc8ace..bd66d3fc 100644 --- a/apps/Backend/src/ai/cdt-lookup.ts +++ b/apps/Backend/src/ai/cdt-lookup.ts @@ -72,6 +72,7 @@ const ALIAS_MAP: Record = { "fmx": "intraoral complete series", "pano": "panoramic", "panorex": "panoramic", + "pnao": "panoramic", // common typo of "pano" "bitewing": "bitewing", "bw": "bitewing two", "2bw": "bitewing two", diff --git a/apps/Backend/src/routes/patients.ts b/apps/Backend/src/routes/patients.ts index 09a2555d..81728a46 100755 --- a/apps/Backend/src/routes/patients.ts +++ b/apps/Backend/src/routes/patients.ts @@ -223,6 +223,11 @@ router.post("/create-temp", async (req: Request, res: Response): Promise => } }); +// Dependents (spouse, children) are covered under the subscriber's member ID, so a +// shared insurance ID is expected when the patient has a policy holder. +const hasPolicyHolder = (p: { policyHolder?: string | null; policyHolderPatientId?: number | null }) => + p.policyHolderPatientId != null || !!(p.policyHolder ?? "").trim(); + // Create a new patient router.post("/", async (req: Request, res: Response): Promise => { try { @@ -242,7 +247,7 @@ router.post("/", async (req: Request, res: Response): Promise => { const patientData = insertPatientSchema.parse(body); // Check for duplicate insuranceId if it's provided - if (patientData.insuranceId) { + if (patientData.insuranceId && !hasPolicyHolder(patientData as any)) { const existingPatient = await storage.getPatientByInsuranceId( patientData.insuranceId as string ); @@ -307,7 +312,8 @@ router.put( // If updating insuranceId, check for uniqueness (excluding self) if ( patientData.insuranceId && - patientData.insuranceId !== existingPatient.insuranceId + patientData.insuranceId !== existingPatient.insuranceId && + !hasPolicyHolder({ ...existingPatient, ...(patientData as any) }) ) { const duplicatePatient = await storage.getPatientByInsuranceId( patientData.insuranceId as string diff --git a/apps/Backend/src/storage/patients-storage.ts b/apps/Backend/src/storage/patients-storage.ts index a3a05572..9289cae3 100755 --- a/apps/Backend/src/storage/patients-storage.ts +++ b/apps/Backend/src/storage/patients-storage.ts @@ -34,6 +34,11 @@ export interface IStorage { dateOfBirth: Date; insuranceId: string | null; insuranceProvider: string | null; + groupNumber: string | null; + address: string | null; + city: string | null; + state: string | null; + zipCode: string | null; status: string; }[] >; @@ -184,6 +189,11 @@ export const patientsStorage: IStorage = { dateOfBirth: true, insuranceId: true, insuranceProvider: true, + groupNumber: true, + address: true, + city: true, + state: true, + zipCode: true, status: true, }, }); diff --git a/apps/Frontend/src/components/patients/patient-form.tsx b/apps/Frontend/src/components/patients/patient-form.tsx index c36cd707..465633b3 100755 --- a/apps/Frontend/src/components/patients/patient-form.tsx +++ b/apps/Frontend/src/components/patients/patient-form.tsx @@ -596,9 +596,27 @@ export const PatientForm = forwardRef( value={field.value || ""} holderPatientId={(form.watch("policyHolderPatientId" as any) as number | null) ?? null} excludePatientId={patient?.id} - onChange={(name, holderPatientId) => { + onChange={(name, holderPatientId, holder) => { field.onChange(name); form.setValue("policyHolderPatientId" as any, holderPatientId, { shouldDirty: true }); + // Dependents share the holder's policy, so copy their insurance, member ID and group number + if (holder?.insuranceProvider) { + form.setValue("insuranceProvider", holder.insuranceProvider, { shouldDirty: true }); + } + if (holder?.insuranceId) { + form.setValue("insuranceId", String(holder.insuranceId), { shouldDirty: true }); + } + if (holder?.groupNumber) { + form.setValue("groupNumber", holder.groupNumber, { shouldDirty: true }); + } + // Same policy, so the holder's eligibility status applies to the dependent + if (holder?.status) { + form.setValue("status" as any, holder.status, { shouldDirty: true }); + } + // Dependents usually live with the holder, so copy the address too + for (const key of ["address", "city", "state", "zipCode"] as const) { + if (holder?.[key]) form.setValue(key, holder[key], { shouldDirty: true }); + } // Picking someone else means the patient isn't the holder if (name.trim() && form.getValues("policyHolderRelationship" as any) === "SELF") { form.setValue("policyHolderRelationship" as any, "", { shouldDirty: true }); diff --git a/apps/Frontend/src/components/patients/policy-holder-search.tsx b/apps/Frontend/src/components/patients/policy-holder-search.tsx index 55819587..e941eabb 100644 --- a/apps/Frontend/src/components/patients/policy-holder-search.tsx +++ b/apps/Frontend/src/components/patients/policy-holder-search.tsx @@ -14,7 +14,8 @@ interface PolicyHolderSearchProps { holderPatientId: number | null; /** Patient being edited — hidden from the results */ excludePatientId?: number; - onChange: (name: string, holderPatientId: number | null) => void; + /** `holder` is the picked patient record, only passed when one is chosen from the results */ + onChange: (name: string, holderPatientId: number | null, holder?: Patient) => void; } // Type to search patients in the DB and pick the policy holder; free text is still allowed @@ -97,7 +98,7 @@ export function PolicyHolderSearch({ value, holderPatientId, excludePatientId, o type="button" className="block w-full px-3 py-2 text-left text-sm hover:bg-gray-100" onClick={() => { - onChange(`${p.firstName} ${p.lastName}`.trim(), Number(p.id)); + onChange(`${p.firstName} ${p.lastName}`.trim(), Number(p.id), p); setOpen(false); }} >