feat: chatbot rendering provider override and NPI provider ordering

- AI chat extracts 'with provider <name>' and routes claim to that provider
- Claim form reads provider from sessionStorage before any async effects run,
  preventing saved claim/procedure data from overriding the chatbot selection
- NPI provider settings table shows Provider #1 / #2 labels with up/down
  reorder buttons; Provider #1 is always the default for claims
- Default provider now uses sortOrder instead of hardcoded 'Mary Scannell'
- Added sortOrder column to NpiProvider schema with migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-06-11 13:17:05 -04:00
parent d4b9c1b889
commit 75c49ab1df
77 changed files with 385 additions and 105 deletions

View File

@@ -22,6 +22,8 @@ export interface ChatClassification {
dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY)
// --- insurance hint (only if explicitly stated in the message) ---
insuranceHint?: string; // raw text, e.g. "masshealth", "BCBS", "CCA"
// --- rendering/treating provider (only if explicitly stated, e.g. "with provider Kai Gao") ---
renderingProvider?: string; // raw name, e.g. "Kai Gao", "Dr. Smith"
// --- procedures (raw text, NOT CDT codes — CDT lookup is done in workflow) ---
procedureNames?: string[]; // for check_and_claim, e.g. ["perio exam", "adult cleaning"]
// --- scheduling ---
@@ -46,6 +48,7 @@ Respond ONLY with valid JSON (no markdown fences):
"memberId": "<member/insurance ID if given explicitly or found in history>",
"dob": "<date of birth in MM/DD/YYYY if given explicitly or found in history>",
"insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>",
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
"procedureNames": ["<raw procedure name>", ...],
"appointmentDate": "<YYYY-MM-DD; use today's date (${today}) if user says 'today'; omit only if no date is mentioned at all>",
"appointmentTime": "<HH:MM 24h if a specific time is mentioned, omit if not stated>",
@@ -104,6 +107,9 @@ Rules:
e.g. "3 PA (#3, 14, 30)" → ["1 pa, #3", "2nd pa, #14", "2nd pa, #30"]
e.g. "2 pa #3 #14" → ["1 pa, #3", "2nd pa, #14"]
- insuranceHint is only set when the user explicitly names an insurance in the message
- renderingProvider is only set when the user explicitly names a treating/rendering provider or doctor
e.g. "with provider Kai Gao", "provider Dr. Smith", "rendered by Kai Gao", "doctor Kai Gao"
Extract just the name (without "Dr." prefix unless it's part of the name), omit if not mentioned
- Keep fallbackReply to 1-2 sentences
- For navigate intents, fallbackReply = "Opening the [page] page..." (e.g. "Opening the eligibility page...")
- appointmentDate applies to BOTH schedule_appointment AND claim_only/check_and_claim:

View File

@@ -594,6 +594,7 @@ async function handleClaimOnly(
serviceDate,
appointmentId,
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })),
renderingProvider: c.renderingProvider ?? null,
},
};
}

View File

@@ -73,6 +73,22 @@ router.put("/:id", async (req: Request, res: Response) => {
}
});
router.post("/reorder", async (req: Request, res: Response) => {
try {
if (!req.user?.id) {
return res.status(401).json({ message: "Unauthorized" });
}
const { orderedIds } = req.body;
if (!Array.isArray(orderedIds)) {
return res.status(400).json({ message: "orderedIds must be an array" });
}
await storage.reorderNpiProviders(req.user.id, orderedIds.map(Number));
res.status(200).json({ ok: true });
} catch (err) {
res.status(500).json({ error: "Failed to reorder NPI providers", details: String(err) });
}
});
router.delete("/:id", async (req: Request, res: Response) => {
try {
if (!req.user?.id) {

View File

@@ -10,6 +10,7 @@ export interface INpiProviderStorage {
updates: Partial<NpiProvider>,
): Promise<NpiProvider | null>;
deleteNpiProvider(userId: number, id: number): Promise<boolean>;
reorderNpiProviders(userId: number, orderedIds: number[]): Promise<void>;
}
export const npiProviderStorage: INpiProviderStorage = {
@@ -20,7 +21,7 @@ export const npiProviderStorage: INpiProviderStorage = {
async getNpiProvidersByUser(userId: number) {
return db.npiProvider.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
});
},
@@ -47,4 +48,15 @@ export const npiProviderStorage: INpiProviderStorage = {
return false;
}
},
async reorderNpiProviders(userId: number, orderedIds: number[]) {
await Promise.all(
orderedIds.map((id, index) =>
db.npiProvider.update({
where: { id, userId },
data: { sortOrder: index + 1 },
})
)
);
},
};