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:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user