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); }} >