feat: batch eligibility, batch claim, and batch check+claim from AI chat
- Add batch_eligibility, batch_claim, and batch_check_and_claim intents to AI classifier so multiple patients can be processed one by one - Add queue processing on insurance-status and claims pages to auto-start the next patient after each check/claim completes - Make patient schema firstName, lastName, phone optional so patients can be created with just member ID + DOB from eligibility checks - Cancel buttons now preserve chat history instead of clearing it - Patient-found card shows Check Eligibility, Eligibility & Appointment Today, and Cancel buttons - Claim service date asks user to pick between latest appointment and today when they differ - Login page subtitle styled with animated gradient Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ import { getLlm, type AiProvider } from "./llm-factory";
|
|||||||
export type InternalChatIntent =
|
export type InternalChatIntent =
|
||||||
| "check_eligibility" // by patient name → look up in DB
|
| "check_eligibility" // by patient name → look up in DB
|
||||||
| "eligibility_by_id" // by explicit memberId + dob (no name)
|
| "eligibility_by_id" // by explicit memberId + dob (no name)
|
||||||
|
| "batch_eligibility" // multiple patients by memberId + dob
|
||||||
|
| "batch_claim" // claim same procedures for multiple patients by name
|
||||||
|
| "batch_check_and_claim" // eligibility + claim for multiple patients by memberId+dob
|
||||||
| "check_and_claim" // eligibility + claim procedures
|
| "check_and_claim" // eligibility + claim procedures
|
||||||
| "find_patient" // look up patient record only
|
| "find_patient" // look up patient record only
|
||||||
| "schedule_appointment" // add patient to today's (or specified) schedule
|
| "schedule_appointment" // add patient to today's (or specified) schedule
|
||||||
@@ -21,6 +24,10 @@ export interface ChatClassification {
|
|||||||
patientName?: string; // for check_eligibility / find_patient / schedule_appointment
|
patientName?: string; // for check_eligibility / find_patient / schedule_appointment
|
||||||
memberId?: string; // for eligibility_by_id / check_and_claim
|
memberId?: string; // for eligibility_by_id / check_and_claim
|
||||||
dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY)
|
dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY)
|
||||||
|
// --- batch eligibility (multiple patients) ---
|
||||||
|
patients?: { memberId: string; dob: string }[]; // for batch_eligibility
|
||||||
|
// --- batch claim (same procedures for multiple patients by name) ---
|
||||||
|
patientNames?: string[]; // for batch_claim
|
||||||
// --- insurance hint (only if explicitly stated in the message) ---
|
// --- insurance hint (only if explicitly stated in the message) ---
|
||||||
insuranceHint?: string; // raw text, e.g. "masshealth", "BCBS", "CCA"
|
insuranceHint?: string; // raw text, e.g. "masshealth", "BCBS", "CCA"
|
||||||
// --- rendering/treating provider (only if explicitly stated, e.g. "with provider Kai Gao") ---
|
// --- rendering/treating provider (only if explicitly stated, e.g. "with provider Kai Gao") ---
|
||||||
@@ -48,6 +55,8 @@ Respond ONLY with valid JSON (no markdown fences):
|
|||||||
"patientName": "<full name if mentioned by name>",
|
"patientName": "<full name if mentioned by name>",
|
||||||
"memberId": "<member/insurance ID if given explicitly or found in history>",
|
"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>",
|
"dob": "<date of birth in MM/DD/YYYY if given explicitly or found in history>",
|
||||||
|
"patients": [{"memberId": "<id>", "dob": "<MM/DD/YYYY>"}, ...],
|
||||||
|
"patientNames": ["<name1>", "<name2>", ...],
|
||||||
"insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>",
|
"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>",
|
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
|
||||||
"procedureNames": ["<raw procedure name>", ...],
|
"procedureNames": ["<raw procedure name>", ...],
|
||||||
@@ -61,12 +70,27 @@ Omit any field that is not present in the message or history.
|
|||||||
Intents:
|
Intents:
|
||||||
- check_eligibility : user wants to check insurance for a patient identified by NAME only
|
- check_eligibility : user wants to check insurance for a patient identified by NAME only
|
||||||
e.g. "check Maria Jesus", "verify insurance for John Smith"
|
e.g. "check Maria Jesus", "verify insurance for John Smith"
|
||||||
- eligibility_by_id : user provides a member ID and date of birth (no patient name)
|
- eligibility_by_id : user provides a SINGLE member ID and date of birth (no patient name)
|
||||||
e.g. "check masshealth for 100xxxx, 10/10/1988"
|
e.g. "check masshealth for 100xxxx, 10/10/1988"
|
||||||
ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date
|
ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date
|
||||||
e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026"
|
e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026"
|
||||||
e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026"
|
e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026"
|
||||||
In these cases set appointmentDate to the mentioned date (YYYY-MM-DD)
|
In these cases set appointmentDate to the mentioned date (YYYY-MM-DD)
|
||||||
|
- batch_eligibility : user provides MULTIPLE member IDs with dates of birth in one message
|
||||||
|
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000"
|
||||||
|
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000"
|
||||||
|
Use this ONLY when TWO OR MORE distinct memberId+dob pairs are given.
|
||||||
|
Put each pair into the "patients" array. Also set insuranceHint if stated.
|
||||||
|
- batch_claim : user wants to claim the SAME procedures for MULTIPLE patients identified by NAME
|
||||||
|
e.g. "claim perio exam and adult prophy for Jackaline and Keioson"
|
||||||
|
e.g. "perio exam, adult cleaning for Maria and John"
|
||||||
|
Use this ONLY when procedures AND two or more patient names are given.
|
||||||
|
Put each patient name into the "patientNames" array. Put procedure names in "procedureNames".
|
||||||
|
- batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them
|
||||||
|
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy"
|
||||||
|
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000 and claim D0120 D1110 for them"
|
||||||
|
Use this when TWO OR MORE memberId+dob pairs are given WITH procedures.
|
||||||
|
Put each pair into "patients" array. Put procedure names in "procedureNames".
|
||||||
- check_and_claim : user wants to check eligibility AND submit PROCEDURES/BILLING as claims
|
- check_and_claim : user wants to check eligibility AND submit PROCEDURES/BILLING as claims
|
||||||
e.g. "check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning"
|
e.g. "check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning"
|
||||||
e.g. "check Maria Jesus and claim D0120 D1110"
|
e.g. "check Maria Jesus and claim D0120 D1110"
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export interface ChatResponse {
|
|||||||
| "show_patient"
|
| "show_patient"
|
||||||
| "check_eligibility_prefill"
|
| "check_eligibility_prefill"
|
||||||
| "eligibility_id_ready"
|
| "eligibility_id_ready"
|
||||||
|
| "batch_eligibility_ready"
|
||||||
|
| "batch_claim_ready"
|
||||||
|
| "batch_check_and_claim_ready"
|
||||||
| "check_and_claim_ready"
|
| "check_and_claim_ready"
|
||||||
| "need_insurance_clarification"
|
| "need_insurance_clarification"
|
||||||
| "appointment_created"
|
| "appointment_created"
|
||||||
@@ -314,12 +317,30 @@ export async function runInternalChatWorkflow(
|
|||||||
return await handleEligibilityById(classification, storage);
|
return await handleEligibilityById(classification, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Batch eligibility (multiple patients) ─────────────────────────────────
|
||||||
|
|
||||||
|
if (intent === "batch_eligibility") {
|
||||||
|
return await handleBatchEligibility(classification, storage);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Check eligibility + claim procedures ──────────────────────────────────
|
// ── Check eligibility + claim procedures ──────────────────────────────────
|
||||||
|
|
||||||
if (intent === "check_and_claim") {
|
if (intent === "check_and_claim") {
|
||||||
return await handleCheckAndClaim(classification, storage, customAliases);
|
return await handleCheckAndClaim(classification, storage, customAliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Batch check & claim (eligibility + claim for multiple patients) ────────
|
||||||
|
|
||||||
|
if (intent === "batch_check_and_claim") {
|
||||||
|
return await handleBatchCheckAndClaim(classification, storage, customAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Batch claim (same procedures for multiple patients) ───────────────────
|
||||||
|
|
||||||
|
if (intent === "batch_claim") {
|
||||||
|
return await handleBatchClaim(classification, storage, customAliases);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Claim only (no eligibility check) ─────────────────────────────────────
|
// ── Claim only (no eligibility check) ─────────────────────────────────────
|
||||||
|
|
||||||
if (intent === "claim_only") {
|
if (intent === "claim_only") {
|
||||||
@@ -436,6 +457,266 @@ async function handleEligibilityById(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── batch_eligibility ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function handleBatchEligibility(
|
||||||
|
c: ChatClassification,
|
||||||
|
storage: StorageLike
|
||||||
|
): Promise<ChatResponse> {
|
||||||
|
const pairs = c.patients ?? [];
|
||||||
|
if (pairs.length < 2) {
|
||||||
|
// Fallback to single if somehow only 1 pair
|
||||||
|
if (pairs.length === 1) {
|
||||||
|
return await handleEligibilityById(
|
||||||
|
{ ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "eligibility_by_id" },
|
||||||
|
storage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { reply: "Please provide at least two member ID + DOB pairs to batch-check." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved: {
|
||||||
|
memberId: string;
|
||||||
|
dob: string;
|
||||||
|
siteKey: string;
|
||||||
|
autoCheck: string;
|
||||||
|
patient: ResolvedPatient | null;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const { memberId, dob } of pairs) {
|
||||||
|
const id = memberId?.trim();
|
||||||
|
const d = dob?.trim();
|
||||||
|
if (!id || !d) continue;
|
||||||
|
|
||||||
|
const existing = await findPatientByMemberId(id, d, storage);
|
||||||
|
const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
|
||||||
|
const resolvedDob = d ?? patient?.dateOfBirth ?? null;
|
||||||
|
if (!resolvedDob) continue;
|
||||||
|
|
||||||
|
const siteKey = resolveSiteKey(
|
||||||
|
patient?.insuranceProvider ?? null,
|
||||||
|
c.insuranceHint ?? null
|
||||||
|
) ?? "MH";
|
||||||
|
|
||||||
|
resolved.push({
|
||||||
|
memberId: id,
|
||||||
|
dob: resolvedDob,
|
||||||
|
siteKey,
|
||||||
|
autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
|
||||||
|
patient,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.length === 0) {
|
||||||
|
return { reply: "Could not parse any valid member ID + DOB pairs." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = resolved.map((r) => {
|
||||||
|
const name = r.patient
|
||||||
|
? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
|
||||||
|
: `ID ${r.memberId}`;
|
||||||
|
return name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
reply: `Ready to check eligibility for ${resolved.length} patients: ${labels.join(", ")}.`,
|
||||||
|
action: "batch_eligibility_ready",
|
||||||
|
actionData: { queue: resolved },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── batch_check_and_claim ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function handleBatchCheckAndClaim(
|
||||||
|
c: ChatClassification,
|
||||||
|
storage: StorageLike,
|
||||||
|
customAliases: { phrase: string; cdtCode: string }[]
|
||||||
|
): Promise<ChatResponse> {
|
||||||
|
const pairs = c.patients ?? [];
|
||||||
|
if (pairs.length < 2) {
|
||||||
|
if (pairs.length === 1) {
|
||||||
|
return await handleCheckAndClaim(
|
||||||
|
{ ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "check_and_claim" },
|
||||||
|
storage,
|
||||||
|
customAliases
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { reply: "Please provide at least two member ID + DOB pairs." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
|
||||||
|
if (procedureNames.length === 0) {
|
||||||
|
return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
|
||||||
|
const matched = cdtResults.filter((r) => r.code !== null);
|
||||||
|
const unmatched = cdtResults.filter((r) => r.code === null);
|
||||||
|
|
||||||
|
if (unmatched.length > 0) {
|
||||||
|
const phrases = unmatched.map((r) => r.input);
|
||||||
|
return {
|
||||||
|
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
|
||||||
|
action: "need_cdt_clarification",
|
||||||
|
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved: {
|
||||||
|
memberId: string;
|
||||||
|
dob: string;
|
||||||
|
siteKey: string;
|
||||||
|
autoCheck: string;
|
||||||
|
patient: ResolvedPatient | null;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const { memberId, dob } of pairs) {
|
||||||
|
const id = memberId?.trim();
|
||||||
|
const d = dob?.trim();
|
||||||
|
if (!id || !d) continue;
|
||||||
|
|
||||||
|
const existing = await findPatientByMemberId(id, d, storage);
|
||||||
|
const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
|
||||||
|
const resolvedDob = d ?? patient?.dateOfBirth ?? null;
|
||||||
|
if (!resolvedDob) continue;
|
||||||
|
|
||||||
|
const siteKey = resolveSiteKey(
|
||||||
|
patient?.insuranceProvider ?? null,
|
||||||
|
c.insuranceHint ?? null
|
||||||
|
) ?? "MH";
|
||||||
|
|
||||||
|
resolved.push({
|
||||||
|
memberId: id,
|
||||||
|
dob: resolvedDob,
|
||||||
|
siteKey,
|
||||||
|
autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
|
||||||
|
patient,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.length === 0) {
|
||||||
|
return { reply: "Could not parse any valid member ID + DOB pairs." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = resolved.map((r) => {
|
||||||
|
const name = r.patient
|
||||||
|
? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
|
||||||
|
: `ID ${r.memberId}`;
|
||||||
|
return name;
|
||||||
|
});
|
||||||
|
const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
reply: `Ready to check eligibility and claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`,
|
||||||
|
action: "batch_check_and_claim_ready",
|
||||||
|
actionData: {
|
||||||
|
queue: resolved,
|
||||||
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||||
|
renderingProvider: c.renderingProvider ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── batch_claim ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function handleBatchClaim(
|
||||||
|
c: ChatClassification,
|
||||||
|
storage: StorageLike,
|
||||||
|
customAliases: { phrase: string; cdtCode: string }[]
|
||||||
|
): Promise<ChatResponse> {
|
||||||
|
const names = c.patientNames ?? [];
|
||||||
|
if (names.length < 2) {
|
||||||
|
if (names.length === 1) {
|
||||||
|
return await handleClaimOnly(
|
||||||
|
{ ...c, patientName: names[0], intent: "claim_only" },
|
||||||
|
storage,
|
||||||
|
customAliases
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { reply: "Please include at least two patient names to batch-claim." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
|
||||||
|
if (procedureNames.length === 0) {
|
||||||
|
return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
|
||||||
|
const matched = cdtResults.filter((r) => r.code !== null);
|
||||||
|
const unmatched = cdtResults.filter((r) => r.code === null);
|
||||||
|
|
||||||
|
if (unmatched.length > 0) {
|
||||||
|
const phrases = unmatched.map((r) => r.input);
|
||||||
|
return {
|
||||||
|
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
|
||||||
|
action: "need_cdt_clarification",
|
||||||
|
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved: {
|
||||||
|
patient: ResolvedPatient;
|
||||||
|
siteKey: string;
|
||||||
|
serviceDate: string | null;
|
||||||
|
appointmentId: number | null;
|
||||||
|
}[] = [];
|
||||||
|
const notFound: string[] = [];
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const raw = await findPatientByName(trimmed, storage);
|
||||||
|
if (!raw) {
|
||||||
|
notFound.push(trimmed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const patient = patientToResult(raw);
|
||||||
|
const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
|
||||||
|
|
||||||
|
const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate);
|
||||||
|
if (d1120Warning) return d1120Warning;
|
||||||
|
|
||||||
|
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
|
||||||
|
|
||||||
|
let serviceDate: string | null = c.appointmentDate ?? null;
|
||||||
|
let appointmentId: number | null = null;
|
||||||
|
if (!serviceDate) {
|
||||||
|
const now = new Date();
|
||||||
|
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.push({ patient, siteKey, serviceDate, appointmentId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notFound.length > 0 && resolved.length === 0) {
|
||||||
|
return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = resolved.map((r) =>
|
||||||
|
`${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
|
||||||
|
);
|
||||||
|
const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
|
||||||
|
let reply = `Ready to claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`;
|
||||||
|
if (notFound.length > 0) {
|
||||||
|
reply += ` Could not find: ${notFound.join(", ")}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reply,
|
||||||
|
action: "batch_claim_ready",
|
||||||
|
actionData: {
|
||||||
|
queue: resolved.map((r) => ({
|
||||||
|
patient: r.patient,
|
||||||
|
siteKey: r.siteKey,
|
||||||
|
serviceDate: r.serviceDate,
|
||||||
|
appointmentId: r.appointmentId,
|
||||||
|
})),
|
||||||
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||||
|
renderingProvider: c.renderingProvider ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── check_and_claim ─────────────────────────────────────────────────────────
|
// ─── check_and_claim ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function handleCheckAndClaim(
|
async function handleCheckAndClaim(
|
||||||
@@ -624,53 +905,44 @@ async function handleClaimOnly(
|
|||||||
let appointmentId: number | null = null;
|
let appointmentId: number | null = null;
|
||||||
|
|
||||||
if (!serviceDate) {
|
if (!serviceDate) {
|
||||||
|
const now = new Date();
|
||||||
|
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||||
|
|
||||||
const appts = await storage.getAppointmentsByPatientId(patient.id);
|
const appts = await storage.getAppointmentsByPatientId(patient.id);
|
||||||
const sorted = appts.sort((a: any, b: any) =>
|
const sorted = appts.sort((a: any, b: any) =>
|
||||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sorted.length >= 2) {
|
if (sorted.length > 0) {
|
||||||
const d1 = new Date(sorted[0].date).getTime();
|
const rawDate = new Date(sorted[0].date);
|
||||||
const d2 = new Date(sorted[1].date).getTime();
|
const latestStr = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;
|
||||||
const diffDays = Math.abs(d1 - d2) / (1000 * 60 * 60 * 24);
|
|
||||||
|
|
||||||
if (diffDays < 7) {
|
if (latestStr === todayStr) {
|
||||||
// Use UTC methods to avoid local-timezone day-shift on midnight UTC dates
|
serviceDate = todayStr;
|
||||||
|
appointmentId = sorted[0].id ?? null;
|
||||||
|
} else {
|
||||||
const fmtUTC = (a: any) => {
|
const fmtUTC = (a: any) => {
|
||||||
const d = new Date(a.date);
|
const d = new Date(a.date);
|
||||||
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`;
|
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`;
|
||||||
};
|
};
|
||||||
const isoUTC = (a: any) => {
|
const todayLabel = `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}/${now.getFullYear()}`;
|
||||||
const d = new Date(a.date);
|
|
||||||
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
reply: `Found two appointments close together for ${fullName}: ${fmtUTC(sorted[0])} and ${fmtUTC(sorted[1])}. Which date should I use for the claim?`,
|
reply: `Which service date for ${fullName}?`,
|
||||||
action: "need_appointment_selection",
|
action: "need_appointment_selection",
|
||||||
actionData: {
|
actionData: {
|
||||||
patient,
|
patient,
|
||||||
siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
|
siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
|
||||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||||
options: [
|
options: [
|
||||||
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: isoUTC(sorted[0]) },
|
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: latestStr },
|
||||||
{ label: fmtUTC(sorted[1]), appointmentId: sorted[1].id, serviceDate: isoUTC(sorted[1]) },
|
{ label: `${todayLabel} (Today)`, appointmentId: null, serviceDate: todayStr },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
serviceDate = todayStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sorted.length > 0) {
|
|
||||||
const rawDate = new Date(sorted[0].date);
|
|
||||||
serviceDate = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;
|
|
||||||
appointmentId = sorted[0].id ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!serviceDate) {
|
|
||||||
// No appointment on file and no date in message — default to today
|
|
||||||
const now = new Date();
|
|
||||||
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
|
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
|||||||
updates.firstName = incomingFirst;
|
updates.firstName = incomingFirst;
|
||||||
if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast)
|
if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast)
|
||||||
updates.lastName = incomingLast;
|
updates.lastName = incomingLast;
|
||||||
// Store DOB if not already set
|
|
||||||
if (dobDate && !patient.dateOfBirth) updates.dateOfBirth = dobDate;
|
if (dobDate && !patient.dateOfBirth) updates.dateOfBirth = dobDate;
|
||||||
if (Object.keys(updates).length > 0) {
|
if (Object.keys(updates).length > 0) {
|
||||||
console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates);
|
console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates);
|
||||||
@@ -163,15 +162,8 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
|||||||
try {
|
try {
|
||||||
patientData = insertPatientSchema.parse(createPayload);
|
patientData = insertPatientSchema.parse(createPayload);
|
||||||
} catch (e1) {
|
} catch (e1) {
|
||||||
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 1):`, e1);
|
console.warn(`[createOrUpdatePatient] schema parse failed:`, e1);
|
||||||
const safePayload = { ...createPayload };
|
patientData = createPayload as InsertPatient;
|
||||||
delete safePayload.dateOfBirth;
|
|
||||||
try {
|
|
||||||
patientData = insertPatientSchema.parse(safePayload);
|
|
||||||
} catch (e2) {
|
|
||||||
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 2):`, e2);
|
|
||||||
patientData = safePayload as InsertPatient;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ type Step =
|
|||||||
| "ai-loading"
|
| "ai-loading"
|
||||||
| "patient-found"
|
| "patient-found"
|
||||||
| "eligibility-id-ready"
|
| "eligibility-id-ready"
|
||||||
|
| "batch-eligibility-ready"
|
||||||
| "check-and-claim-ready"
|
| "check-and-claim-ready"
|
||||||
| "need-insurance-clarification"
|
| "need-insurance-clarification"
|
||||||
| "need-appointment-selection"
|
| "need-appointment-selection"
|
||||||
| "need-cdt-clarification"
|
| "need-cdt-clarification"
|
||||||
| "claim-ready"
|
| "claim-ready"
|
||||||
|
| "batch-claim-ready"
|
||||||
|
| "batch-check-and-claim-ready"
|
||||||
| "preauth-ready";
|
| "preauth-ready";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
@@ -160,6 +163,7 @@ export function ChatbotButton() {
|
|||||||
const [freeTextInput, setFreeTextInput] = useState("");
|
const [freeTextInput, setFreeTextInput] = useState("");
|
||||||
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
|
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
|
||||||
const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null);
|
const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null);
|
||||||
|
const [batchEligibilityData, setBatchEligibilityData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[] | null>(null);
|
||||||
const [checkAndClaimData, setCheckAndClaimData] = useState<CheckAndClaimData | null>(null);
|
const [checkAndClaimData, setCheckAndClaimData] = useState<CheckAndClaimData | null>(null);
|
||||||
const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null);
|
const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null);
|
||||||
const [apptSelectionData, setApptSelectionData] = useState<{
|
const [apptSelectionData, setApptSelectionData] = useState<{
|
||||||
@@ -181,6 +185,16 @@ export function ChatbotButton() {
|
|||||||
appointmentId: number | null;
|
appointmentId: number | null;
|
||||||
renderingProvider: string | null;
|
renderingProvider: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [batchCheckAndClaimData, setBatchCheckAndClaimData] = useState<{
|
||||||
|
queue: { memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[];
|
||||||
|
matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[];
|
||||||
|
renderingProvider: string | null;
|
||||||
|
} | null>(null);
|
||||||
|
const [batchClaimData, setBatchClaimData] = useState<{
|
||||||
|
queue: { patient: PatientResult; siteKey: string; serviceDate: string | null; appointmentId: number | null }[];
|
||||||
|
matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[];
|
||||||
|
renderingProvider: string | null;
|
||||||
|
} | null>(null);
|
||||||
const [preauthReadyData, setPreauthReadyData] = useState<{
|
const [preauthReadyData, setPreauthReadyData] = useState<{
|
||||||
patient: PatientResult | null;
|
patient: PatientResult | null;
|
||||||
matchedCodes: { code: string; description: string; toothNumber?: string }[];
|
matchedCodes: { code: string; description: string; toothNumber?: string }[];
|
||||||
@@ -257,11 +271,14 @@ export function ChatbotButton() {
|
|||||||
setFreeTextInput("");
|
setFreeTextInput("");
|
||||||
setPatientResult(null);
|
setPatientResult(null);
|
||||||
setEligibilityIdData(null);
|
setEligibilityIdData(null);
|
||||||
|
setBatchEligibilityData(null);
|
||||||
setCheckAndClaimData(null);
|
setCheckAndClaimData(null);
|
||||||
setClarificationData(null);
|
setClarificationData(null);
|
||||||
setApptSelectionData(null);
|
setApptSelectionData(null);
|
||||||
setCdtClarificationData(null);
|
setCdtClarificationData(null);
|
||||||
setClaimReadyData(null);
|
setClaimReadyData(null);
|
||||||
|
setBatchClaimData(null);
|
||||||
|
setBatchCheckAndClaimData(null);
|
||||||
setPreauthReadyData(null);
|
setPreauthReadyData(null);
|
||||||
setPendingFiles([]);
|
setPendingFiles([]);
|
||||||
};
|
};
|
||||||
@@ -326,18 +343,30 @@ export function ChatbotButton() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEligibilityFromPatient = () => {
|
const handleEligibilityFromPatient = () => {
|
||||||
if (!patientResult) return;
|
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
|
||||||
addMsg("user", "Check eligibility now");
|
addMsg("user", "Check eligibility now");
|
||||||
addMsg("bot", "Opening the eligibility check page...");
|
addMsg("bot", "Opening the eligibility check page...");
|
||||||
if (patientResult.insuranceId && patientResult.dateOfBirth) {
|
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
||||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
};
|
||||||
memberId: patientResult.insuranceId,
|
|
||||||
dob: patientResult.dateOfBirth,
|
const handleEligibilityAndAppointmentFromPatient = async () => {
|
||||||
autoCheck: getAutoCheck(patientResult.dateOfBirth),
|
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
|
||||||
}));
|
addMsg("user", "Check eligibility & add to schedule (today)");
|
||||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
addMsg("bot", "Creating appointment for today...", true);
|
||||||
|
try {
|
||||||
|
const res = await apiRequest("POST", "/api/ai/create-appointment-today", {
|
||||||
|
patientId: patientResult.id,
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
replaceLastMsg(data.message ?? "Could not create appointment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
replaceLastMsg(`Appointment added at ${data.startTime} (${data.column ?? "Column A"}) — opening eligibility check page...`);
|
||||||
|
} catch {
|
||||||
|
replaceLastMsg("Could not create appointment — opening eligibility check page...");
|
||||||
}
|
}
|
||||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
||||||
};
|
};
|
||||||
|
|
||||||
const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => {
|
const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => {
|
||||||
@@ -354,6 +383,17 @@ export function ChatbotButton() {
|
|||||||
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
|
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBatchEligibilityRun = () => {
|
||||||
|
if (!batchEligibilityData || batchEligibilityData.length === 0) return;
|
||||||
|
addMsg("user", `Check all ${batchEligibilityData.length} patients`);
|
||||||
|
addMsg("bot", `Checking ${batchEligibilityData.length} patients one by one...`);
|
||||||
|
const [first, ...rest] = batchEligibilityData;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_eligibility_queue", JSON.stringify(rest));
|
||||||
|
}
|
||||||
|
prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck);
|
||||||
|
};
|
||||||
|
|
||||||
const handleEligibilityAndAppointment = async (targetDate?: string) => {
|
const handleEligibilityAndAppointment = async (targetDate?: string) => {
|
||||||
if (!eligibilityIdData) return;
|
if (!eligibilityIdData) return;
|
||||||
const dateLabel = targetDate
|
const dateLabel = targetDate
|
||||||
@@ -445,6 +485,12 @@ export function ChatbotButton() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.action === "batch_eligibility_ready" && data.actionData?.queue) {
|
||||||
|
setBatchEligibilityData(data.actionData.queue);
|
||||||
|
setStep("batch-eligibility-ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.action === "eligibility_id_ready" && data.actionData) {
|
if (data.action === "eligibility_id_ready" && data.actionData) {
|
||||||
setEligibilityIdData({
|
setEligibilityIdData({
|
||||||
memberId: data.actionData.memberId,
|
memberId: data.actionData.memberId,
|
||||||
@@ -510,6 +556,26 @@ export function ChatbotButton() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.action === "batch_check_and_claim_ready" && data.actionData) {
|
||||||
|
setBatchCheckAndClaimData({
|
||||||
|
queue: data.actionData.queue ?? [],
|
||||||
|
matchedCodes: data.actionData.matchedCodes ?? [],
|
||||||
|
renderingProvider: data.actionData.renderingProvider ?? null,
|
||||||
|
});
|
||||||
|
setStep("batch-check-and-claim-ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.action === "batch_claim_ready" && data.actionData) {
|
||||||
|
setBatchClaimData({
|
||||||
|
queue: data.actionData.queue ?? [],
|
||||||
|
matchedCodes: data.actionData.matchedCodes ?? [],
|
||||||
|
renderingProvider: data.actionData.renderingProvider ?? null,
|
||||||
|
});
|
||||||
|
setStep("batch-claim-ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.action === "claim_only_ready" && data.actionData) {
|
if (data.action === "claim_only_ready" && data.actionData) {
|
||||||
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
|
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
|
||||||
setClaimReadyData({
|
setClaimReadyData({
|
||||||
@@ -556,7 +622,10 @@ export function ChatbotButton() {
|
|||||||
step === "ai-loading" ||
|
step === "ai-loading" ||
|
||||||
step === "patient-found" ||
|
step === "patient-found" ||
|
||||||
step === "eligibility-id-ready" ||
|
step === "eligibility-id-ready" ||
|
||||||
|
step === "batch-eligibility-ready" ||
|
||||||
step === "check-and-claim-ready" ||
|
step === "check-and-claim-ready" ||
|
||||||
|
step === "batch-claim-ready" ||
|
||||||
|
step === "batch-check-and-claim-ready" ||
|
||||||
step === "need-insurance-clarification" ||
|
step === "need-insurance-clarification" ||
|
||||||
step === "need-appointment-selection";
|
step === "need-appointment-selection";
|
||||||
|
|
||||||
@@ -684,7 +753,7 @@ export function ChatbotButton() {
|
|||||||
>
|
>
|
||||||
Continue <ChevronRight className="h-3 w-3 ml-1" />
|
Continue <ChevronRight className="h-3 w-3 ml-1" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}>
|
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -726,19 +795,29 @@ export function ChatbotButton() {
|
|||||||
{patientResult.dateOfBirth && (
|
{patientResult.dateOfBirth && (
|
||||||
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
|
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2 pt-1">
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
{patientResult.insuranceId && (
|
{patientResult.insuranceId && patientResult.dateOfBirth && (
|
||||||
<Button
|
<>
|
||||||
size="sm"
|
<Button
|
||||||
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90"
|
size="sm"
|
||||||
onClick={handleEligibilityFromPatient}
|
className="w-full h-8 text-xs bg-primary hover:bg-primary/90"
|
||||||
>
|
onClick={handleEligibilityFromPatient}
|
||||||
<Stethoscope className="h-3 w-3 mr-1" />
|
>
|
||||||
Check Eligibility
|
<Stethoscope className="h-3 w-3 mr-1" />
|
||||||
</Button>
|
Check Eligibility
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-8 text-xs bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||||
|
onClick={handleEligibilityAndAppointmentFromPatient}
|
||||||
|
>
|
||||||
|
<Calendar className="h-3 w-3 mr-1" />
|
||||||
|
Eligibility & Appointment Today
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}>
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
Done
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -785,7 +864,39 @@ export function ChatbotButton() {
|
|||||||
{new Date(eligibilityIdData.appointmentDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
{new Date(eligibilityIdData.appointmentDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={reset}>
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Batch eligibility ready */}
|
||||||
|
{step === "batch-eligibility-ready" && batchEligibilityData && (
|
||||||
|
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-3 space-y-2">
|
||||||
|
<p className="text-xs font-semibold text-indigo-800">
|
||||||
|
{batchEligibilityData.length} patients to check:
|
||||||
|
</p>
|
||||||
|
{batchEligibilityData.map((item, i) => {
|
||||||
|
const name = item.patient
|
||||||
|
? `${item.patient.firstName ?? ""} ${item.patient.lastName ?? ""}`.trim()
|
||||||
|
: `ID: ${item.memberId}`;
|
||||||
|
return (
|
||||||
|
<p key={i} className="text-xs text-indigo-600 pl-2">
|
||||||
|
{i + 1}. {name} — DOB: {item.dob}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-8 text-xs bg-indigo-600 hover:bg-indigo-700 text-white"
|
||||||
|
onClick={handleBatchEligibilityRun}
|
||||||
|
>
|
||||||
|
<Stethoscope className="h-3 w-3 mr-1" />
|
||||||
|
Check All ({batchEligibilityData.length} patients)
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -820,7 +931,7 @@ export function ChatbotButton() {
|
|||||||
<Stethoscope className="h-3 w-3 mr-1" />
|
<Stethoscope className="h-3 w-3 mr-1" />
|
||||||
Check & Claim
|
Check & Claim
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}>
|
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -884,7 +995,7 @@ export function ChatbotButton() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="ghost" className="h-7 text-xs w-full" onClick={reset}>
|
<Button size="sm" variant="ghost" className="h-7 text-xs w-full" onClick={resetStep}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -931,6 +1042,135 @@ export function ChatbotButton() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Batch check & claim ready */}
|
||||||
|
{step === "batch-check-and-claim-ready" && batchCheckAndClaimData && (
|
||||||
|
<div className="bg-purple-50 border border-purple-200 rounded-xl p-3 space-y-2">
|
||||||
|
<p className="text-xs font-semibold text-purple-800">
|
||||||
|
Check & Claim for {batchCheckAndClaimData.queue.length} patients:
|
||||||
|
</p>
|
||||||
|
{batchCheckAndClaimData.queue.map((item, i) => {
|
||||||
|
const name = item.patient
|
||||||
|
? `${item.patient.firstName ?? ""} ${item.patient.lastName ?? ""}`.trim()
|
||||||
|
: `ID: ${item.memberId}`;
|
||||||
|
return (
|
||||||
|
<p key={i} className="text-xs text-purple-600 pl-2">
|
||||||
|
{i + 1}. {name} — DOB: {item.dob}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{batchCheckAndClaimData.matchedCodes.length > 0 && (
|
||||||
|
<div className="space-y-0.5 pt-0.5">
|
||||||
|
<p className="text-xs font-medium text-purple-700">Claim after ACTIVE:</p>
|
||||||
|
{batchCheckAndClaimData.matchedCodes.map((c) => (
|
||||||
|
<p key={c.code} className="text-xs text-gray-700 pl-2">
|
||||||
|
<span className="font-medium">{c.code}</span> — {c.description}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-8 text-xs bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
const { queue, matchedCodes, renderingProvider } = batchCheckAndClaimData;
|
||||||
|
addMsg("user", `Check & claim all ${queue.length} patients`);
|
||||||
|
addMsg("bot", `Checking eligibility and claiming for ${queue.length} patients one by one...`);
|
||||||
|
const [first, ...rest] = queue;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({
|
||||||
|
remaining: rest,
|
||||||
|
matchedCodes,
|
||||||
|
renderingProvider,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
||||||
|
codes: matchedCodes,
|
||||||
|
siteKey: first!.siteKey,
|
||||||
|
patientId: first!.patient?.id ?? null,
|
||||||
|
memberId: first!.memberId,
|
||||||
|
dob: first!.dob,
|
||||||
|
serviceDate: null,
|
||||||
|
renderingProvider,
|
||||||
|
}));
|
||||||
|
prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stethoscope className="h-3 w-3 mr-1" />
|
||||||
|
Check & Claim All ({batchCheckAndClaimData.queue.length} patients)
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Batch claim ready */}
|
||||||
|
{step === "batch-claim-ready" && batchClaimData && (
|
||||||
|
<div className="bg-orange-50 border border-orange-200 rounded-xl p-3 space-y-2">
|
||||||
|
<p className="text-xs font-semibold text-orange-800">
|
||||||
|
Claim for {batchClaimData.queue.length} patients:
|
||||||
|
</p>
|
||||||
|
{batchClaimData.queue.map((item, i) => (
|
||||||
|
<p key={i} className="text-xs text-orange-600 pl-2">
|
||||||
|
{i + 1}. {item.patient.firstName} {item.patient.lastName}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
{batchClaimData.matchedCodes.length > 0 && (
|
||||||
|
<div className="space-y-0.5 pt-0.5">
|
||||||
|
<p className="text-xs font-medium text-orange-700">Procedures:</p>
|
||||||
|
{batchClaimData.matchedCodes.map((c) => (
|
||||||
|
<p key={c.code} className="text-xs text-gray-700 pl-2">
|
||||||
|
<span className="font-medium">{c.code}</span> — {c.description}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-8 text-xs bg-orange-600 hover:bg-orange-700 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
const { queue, matchedCodes, renderingProvider } = batchClaimData;
|
||||||
|
addMsg("user", `Claim all ${queue.length} patients`);
|
||||||
|
addMsg("bot", `Claiming ${queue.length} patients one by one...`);
|
||||||
|
const [first, ...rest] = queue;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({
|
||||||
|
remaining: rest,
|
||||||
|
matchedCodes,
|
||||||
|
renderingProvider,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (first!.patient.id && matchedCodes.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||||
|
codes: matchedCodes,
|
||||||
|
siteKey: first!.siteKey,
|
||||||
|
serviceDate: first!.serviceDate,
|
||||||
|
autoSubmit: true,
|
||||||
|
renderingProvider: renderingProvider ?? null,
|
||||||
|
dob: first!.patient.dateOfBirth ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
setChatbotPendingFiles(pendingFiles);
|
||||||
|
markJobStarted();
|
||||||
|
const url = first!.appointmentId
|
||||||
|
? `/claims?appointmentId=${first!.appointmentId}`
|
||||||
|
: `/claims?newPatient=${first!.patient.id}`;
|
||||||
|
setTimeout(() => { setLocation(url); setOpen(false); resetStep(); }, 600);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileText className="h-3 w-3 mr-1" />
|
||||||
|
Claim All ({batchClaimData.queue.length} patients)
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Claim ready — confirm before submitting */}
|
{/* Claim ready — confirm before submitting */}
|
||||||
{step === "claim-ready" && claimReadyData && (() => {
|
{step === "claim-ready" && claimReadyData && (() => {
|
||||||
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");
|
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");
|
||||||
|
|||||||
@@ -110,3 +110,13 @@
|
|||||||
min-width: 6rem; /* Prevent shrinking */
|
min-width: 6rem; /* Prevent shrinking */
|
||||||
appearance: none; /* Removes native styling */
|
appearance: none; /* Removes native styling */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-flash {
|
||||||
|
0% { background-position: 100% 50%; }
|
||||||
|
50% { background-position: 0% 50%; }
|
||||||
|
100% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-gradient-flash {
|
||||||
|
animation: gradient-flash 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ export default function AuthPage() {
|
|||||||
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
|
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
|
||||||
My Dental Office Management
|
My Dental Office Management
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-400">
|
<p className="text-sm font-semibold tracking-[0.15em] bg-clip-text text-transparent animate-gradient-flash"
|
||||||
Driven by multiple AI agents
|
style={{ backgroundImage: "linear-gradient(90deg, #7c3aed, #d946ef, #06b6d4, #7c3aed)", backgroundSize: "200% 100%" }}>
|
||||||
|
Driven By Multiple AI Agents
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -911,6 +911,79 @@ export default function ClaimsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// If a chatbot batch check+claim queue is pending, navigate to eligibility for the next patient
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const remaining = parsed?.remaining as any[] | undefined;
|
||||||
|
const matchedCodes = parsed?.matchedCodes ?? [];
|
||||||
|
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||||
|
if (remaining && remaining.length > 0) {
|
||||||
|
const [next, ...rest] = remaining;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||||
|
}
|
||||||
|
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
||||||
|
codes: matchedCodes,
|
||||||
|
siteKey: next.siteKey,
|
||||||
|
patientId: next.patient?.id ?? null,
|
||||||
|
memberId: next.memberId,
|
||||||
|
dob: next.dob,
|
||||||
|
serviceDate: null,
|
||||||
|
renderingProvider,
|
||||||
|
}));
|
||||||
|
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
||||||
|
memberId: next.memberId,
|
||||||
|
dob: next.dob,
|
||||||
|
autoCheck: next.autoCheck,
|
||||||
|
}));
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||||
|
setWouterLocation("/insurance-status");
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// If a chatbot batch claim queue is pending, open the next patient
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem("chatbot_claim_queue");
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const remaining = parsed?.remaining as any[] | undefined;
|
||||||
|
const matchedCodes = parsed?.matchedCodes ?? [];
|
||||||
|
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||||
|
if (remaining && remaining.length > 0) {
|
||||||
|
const [next, ...rest] = remaining;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem("chatbot_claim_queue");
|
||||||
|
}
|
||||||
|
if (next.patient?.id && matchedCodes.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||||
|
codes: matchedCodes,
|
||||||
|
siteKey: next.siteKey,
|
||||||
|
serviceDate: next.serviceDate,
|
||||||
|
autoSubmit: true,
|
||||||
|
renderingProvider,
|
||||||
|
dob: next.patient.dateOfBirth ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem("chatbot_claim_queue");
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre Auth section
|
// Pre Auth section
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
processNextInQueue();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||||
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||||
@@ -574,6 +575,7 @@ export default function InsuranceStatusPage() {
|
|||||||
if (claimed) return;
|
if (claimed) return;
|
||||||
setSelectedPatient(null);
|
setSelectedPatient(null);
|
||||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
|
processNextInQueue();
|
||||||
|
|
||||||
// Open all PDFs side by side in the modal
|
// Open all PDFs side by side in the modal
|
||||||
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
|
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
|
||||||
@@ -643,6 +645,7 @@ export default function InsuranceStatusPage() {
|
|||||||
|
|
||||||
setSelectedPatient(null);
|
setSelectedPatient(null);
|
||||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
|
processNextInQueue();
|
||||||
|
|
||||||
// Open 4-panel modal
|
// Open 4-panel modal
|
||||||
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
|
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
|
||||||
@@ -773,6 +776,29 @@ export default function InsuranceStatusPage() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processNextInQueue = () => {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem("chatbot_eligibility_queue");
|
||||||
|
if (!raw) return;
|
||||||
|
const queue = JSON.parse(raw) as { memberId: string; dob: string; autoCheck: string }[];
|
||||||
|
if (!queue.length) {
|
||||||
|
sessionStorage.removeItem("chatbot_eligibility_queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [next, ...rest] = queue;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_eligibility_queue", JSON.stringify(rest));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem("chatbot_eligibility_queue");
|
||||||
|
}
|
||||||
|
toast({ title: `${rest.length + 1} patient${rest.length > 0 ? "s" : ""} remaining`, description: `Starting next: ${next!.memberId}` });
|
||||||
|
setTimeout(() => {
|
||||||
|
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId: next!.memberId, dob: next!.dob, autoCheck: next!.autoCheck }));
|
||||||
|
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||||
|
}, 1500);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
// Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll
|
// Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -1018,6 +1044,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`);
|
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
processNextInQueue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1039,6 +1066,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`);
|
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
processNextInQueue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1057,6 +1085,7 @@ export default function InsuranceStatusPage() {
|
|||||||
fallbackFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`,
|
fallbackFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`,
|
||||||
);
|
);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
|
processNextInQueue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1081,6 +1110,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`);
|
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
processNextInQueue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1102,6 +1132,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
|
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
processNextInQueue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1123,6 +1154,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_cca_${memberId}.pdf`);
|
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_cca_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
processNextInQueue();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ export const insertPatientSchema = (
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
firstName: z.string().min(1, "First name is required"),
|
firstName: z.string().optional().default(""),
|
||||||
lastName: z.string().min(1, "Last name is required"),
|
lastName: z.string().optional().default(""),
|
||||||
dateOfBirth: z.preprocess(
|
dateOfBirth: z.preprocess(
|
||||||
(val) => (val === null || val === undefined || val === "" ? undefined : val),
|
(val) => (val === null || val === undefined || val === "" ? undefined : val),
|
||||||
z.coerce.date({ required_error: "Date of birth is required" })
|
z.coerce.date({ required_error: "Date of birth is required" })
|
||||||
),
|
),
|
||||||
gender: z.string().optional().nullable(),
|
gender: z.string().optional().nullable(),
|
||||||
phone: z.string().min(1, "Phone number is required"),
|
phone: z.string().optional().nullable(),
|
||||||
insuranceId: insuranceIdSchema,
|
insuranceId: insuranceIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user