feat: add Provider field to appointment form, drive AI claim automation

Adds an optional Provider (NPI) select to the Edit Appointment form,
sourced from Settings > NPI Providers. The AI "Claim for Column" batch
flow now prefers the appointment's own provider when resolving which
provider Selenium should select on the insurance site (e.g. MassHealth
rendering provider dropdown), falling back to the existing per-procedure
selection, active claim, or first configured provider.

Also resyncs packages/db's committed compiled schema/client artifacts
with their .ts sources — they had drifted out of sync (some untouched
since July), which was silently stripping the new npiProviderId field
via stale .strict() zod schemas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ff
2026-07-20 23:07:10 -04:00
parent ca568de817
commit b1932fa3d6
1285 changed files with 21237 additions and 4432 deletions

View File

@@ -428,6 +428,7 @@ router.put(
if (rawTypeLocked !== undefined) updatePayload.typeLocked = Boolean(rawTypeLocked);
if (appointmentData.status !== undefined) updatePayload.status = appointmentData.status;
if (appointmentData.notes !== undefined) updatePayload.notes = appointmentData.notes;
if ("npiProviderId" in appointmentData) updatePayload.npiProviderId = appointmentData.npiProviderId ?? null;
if (isDateChanged) updatePayload.eligibilityStatus = "UNKNOWN";
// Update appointment

View File

@@ -758,14 +758,13 @@ router.post(
const npiProviders = await storage.getNpiProvidersByUser(req.user.id);
const npiProvider = npiProviders[0] ?? null;
// procNpiProviderId = user's explicit choice saved via "Select Procedures" form
// This ALWAYS wins over any previously stored claim npiProviderId
// procNpiProviderId = user's explicit choice saved via "Select Procedures" form (legacy manual flow)
const procNpiProviderId = (apptProcedures as any[]).find((p) => p.npiProviderId)?.npiProviderId ?? null;
// Priority: Select Procedures choice > existing claim > first provider
const claimNpiProviderId = procNpiProviderId ?? activeClaim?.npiProviderId ?? npiProvider?.id ?? null;
// Priority: appointment's Provider field (set in Edit Appointment) > Select Procedures choice > existing claim > first provider
const claimNpiProviderId = (apt as any).npiProviderId ?? procNpiProviderId ?? activeClaim?.npiProviderId ?? npiProvider?.id ?? null;
console.log(`[batch-column] apt=${apt.id} siteKey=${siteKey} procNpiId=${procNpiProviderId} claimNpiId=${activeClaim?.npiProviderId} resolved=${claimNpiProviderId}`);
console.log(`[batch-column] apt=${apt.id} siteKey=${siteKey} aptNpiId=${(apt as any).npiProviderId} procNpiId=${procNpiProviderId} claimNpiId=${activeClaim?.npiProviderId} resolved=${claimNpiProviderId}`);
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
@@ -773,8 +772,8 @@ router.post(
let claimId: number;
if (activeClaim?.id) {
claimId = activeClaim.id;
if (procNpiProviderId && activeClaim.npiProviderId !== procNpiProviderId) {
await storage.updateClaim(claimId, { npiProviderId: procNpiProviderId });
if (claimNpiProviderId && activeClaim.npiProviderId !== claimNpiProviderId) {
await storage.updateClaim(claimId, { npiProviderId: claimNpiProviderId });
}
} else {
// Validate required integer fields before sending to Prisma

View File

@@ -30,6 +30,7 @@ import {
Appointment,
InsertAppointment,
insertAppointmentSchema,
NpiProvider,
Patient,
Staff,
UpdateAppointment,
@@ -94,6 +95,15 @@ export function AppointmentForm({
enabled: !!user,
});
const { data: npiProviders = [] as NpiProvider[] } = useQuery<NpiProvider[]>({
queryKey: ["/api/npiProviders/"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/npiProviders/");
return res.json();
},
enabled: !!user,
});
const colorMap: Record<string, string> = {
"Dr. Kai Gao": "bg-blue-600",
"Dr. Jane Smith": "bg-emerald-600",
@@ -120,6 +130,10 @@ export function AppointmentForm({
typeof appointment.staffId === "number"
? appointment.staffId
: undefined,
npiProviderId:
typeof appointment.npiProviderId === "number"
? appointment.npiProviderId
: undefined,
}
: prefillData
? {
@@ -614,6 +628,47 @@ export function AppointmentForm({
)}
/>
<FormField
control={form.control}
name="npiProviderId"
render={({ field }) => (
<FormItem>
<FormLabel>
Provider{" "}
<span className="text-muted-foreground text-xs">
(used by AI to select the rendering provider on insurance claims)
</span>
</FormLabel>
<Select
disabled={isLoading}
onValueChange={(val) =>
field.onChange(val === "unassigned" ? undefined : Number(val))
}
value={field.value ? String(field.value) : "unassigned"}
defaultValue={field.value ? String(field.value) : "unassigned"}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="unassigned">Unassigned</SelectItem>
{npiProviders.map((provider) => (
<SelectItem
key={provider.id}
value={provider.id?.toString() || ""}
>
{provider.providerName}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"