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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user