Compare commits
2
Commits
fd2d1e6aba
...
e8889c0ff8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8889c0ff8 | ||
|
|
00a81b3fda |
@@ -354,6 +354,30 @@ function parseSrpCode(input: string): CdtMatch | null {
|
|||||||
return { code: codeStr, description: row.Description, input, quad: quadrant };
|
return { code: codeStr, description: row.Description, input, quad: quadrant };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse re-cement wording regardless of word order or tooth-number format:
|
||||||
|
* "recement crown #7", "#7 crown recement", "recement crown 7". "recement"
|
||||||
|
* outranks "crown" whenever both appear — this is a re-cement (D2920), not
|
||||||
|
* a new crown (D2740).
|
||||||
|
*/
|
||||||
|
function parseRecementCode(input: string): CdtMatch | null {
|
||||||
|
const lower = input.toLowerCase();
|
||||||
|
if (!/\bre[\s-]?cement(?:ed|ing)?\b/.test(lower)) return null;
|
||||||
|
|
||||||
|
let toothNumber: string | undefined;
|
||||||
|
const hashMatch = input.match(/#\s*(\d{1,2})\b/);
|
||||||
|
const toothWordMatch = input.match(/\btooth\s+(\d{1,2})\b/i);
|
||||||
|
const bareNumMatch = input.match(/\b(\d{1,2})\b/);
|
||||||
|
const m = hashMatch ?? toothWordMatch ?? bareNumMatch;
|
||||||
|
if (m) {
|
||||||
|
const n = parseInt(m[1]!, 10);
|
||||||
|
if (n >= 1 && n <= 32) toothNumber = String(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = ALL_CODES.find((r) => r["Procedure Code"] === "D2920");
|
||||||
|
return { code: "D2920", description: row?.Description ?? "D2920", input, ...(toothNumber ? { toothNumber } : {}) };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse RCT/root-canal notation like "rct #29", "#14 rct", "root canal #6".
|
* Parse RCT/root-canal notation like "rct #29", "#14 rct", "root canal #6".
|
||||||
* Returns D3310 (anterior), D3320 (premolar), or D3330 (molar) based on tooth number.
|
* Returns D3310 (anterior), D3320 (premolar), or D3330 (molar) based on tooth number.
|
||||||
@@ -545,23 +569,28 @@ export function lookupCdtCodes(
|
|||||||
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Multi-PA shorthand: "4PA", "4PA #3 #14 #19 #30", etc.
|
// 2. Re-cement wording — outranks "crown"/etc. elsewhere in the same entry,
|
||||||
|
// regardless of word order or tooth-number format (e.g. "#7 crown recement").
|
||||||
|
const recementMatch = parseRecementCode(name);
|
||||||
|
if (recementMatch) return recementMatch;
|
||||||
|
|
||||||
|
// 3. Multi-PA shorthand: "4PA", "4PA #3 #14 #19 #30", etc.
|
||||||
const multiPaMatches = parseMultiPaCode(name);
|
const multiPaMatches = parseMultiPaCode(name);
|
||||||
if (multiPaMatches) return multiPaMatches;
|
if (multiPaMatches) return multiPaMatches;
|
||||||
|
|
||||||
// 3. Composite filling by tooth# and surfaces — also determines CDT code from tooth position
|
// 4. Composite filling by tooth# and surfaces — also determines CDT code from tooth position
|
||||||
const compositeMatch = parseCompositeCode(name);
|
const compositeMatch = parseCompositeCode(name);
|
||||||
if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface
|
if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface
|
||||||
|
|
||||||
// 4. RCT by tooth# — D3310/D3320/D3330 based on tooth position
|
// 5. RCT by tooth# — D3310/D3320/D3330 based on tooth position
|
||||||
const rctMatch = parseRctCode(name);
|
const rctMatch = parseRctCode(name);
|
||||||
if (rctMatch) return rctMatch;
|
if (rctMatch) return rctMatch;
|
||||||
|
|
||||||
// 5. SRP + quadrant — D4341/D4342 with UL/UR/LL/LR
|
// 6. SRP + quadrant — D4341/D4342 with UL/UR/LL/LR
|
||||||
const srpMatch = parseSrpCode(name);
|
const srpMatch = parseSrpCode(name);
|
||||||
if (srpMatch) return srpMatch;
|
if (srpMatch) return srpMatch;
|
||||||
|
|
||||||
// 6. Hardcoded direct code map (short abbreviations / codes not in MH schedule)
|
// 7. Hardcoded direct code map (short abbreviations / codes not in MH schedule)
|
||||||
const directKey = DIRECT_CODE_MAP[cleaned] ? cleaned : DIRECT_CODE_MAP[strippedCleaned] ? strippedCleaned : null;
|
const directKey = DIRECT_CODE_MAP[cleaned] ? cleaned : DIRECT_CODE_MAP[strippedCleaned] ? strippedCleaned : null;
|
||||||
if (directKey) {
|
if (directKey) {
|
||||||
const code = DIRECT_CODE_MAP[directKey]!;
|
const code = DIRECT_CODE_MAP[directKey]!;
|
||||||
@@ -569,7 +598,7 @@ export function lookupCdtCodes(
|
|||||||
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Hardcoded alias + keyword search (try stripped name so tooth# doesn't break matching)
|
// 8. Hardcoded alias + keyword search (try stripped name so tooth# doesn't break matching)
|
||||||
const match = matchOne(strippedCleaned !== cleaned ? strippedCleaned : name);
|
const match = matchOne(strippedCleaned !== cleaned ? strippedCleaned : name);
|
||||||
if (match) return { ...match, input: name, ...toothInfo };
|
if (match) return { ...match, input: name, ...toothInfo };
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ import {
|
|||||||
applyComboToForm,
|
applyComboToForm,
|
||||||
getDescriptionForCode,
|
getDescriptionForCode,
|
||||||
findPriceMismatches,
|
findPriceMismatches,
|
||||||
|
getCodeMap,
|
||||||
|
getPriceForCodeWithAgeFromMap,
|
||||||
|
ageOnDate,
|
||||||
type PriceMismatch,
|
type PriceMismatch,
|
||||||
} from "@/utils/procedureCombosMapping";
|
} from "@/utils/procedureCombosMapping";
|
||||||
import { PROCEDURE_COMBOS } from "@/utils/procedureCombos";
|
import { PROCEDURE_COMBOS } from "@/utils/procedureCombos";
|
||||||
@@ -624,7 +627,11 @@ export function ClaimForm({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const filled = { ...prev, insuranceSiteKey, serviceLines: updatedLines };
|
const filled = { ...prev, insuranceSiteKey, serviceLines: updatedLines };
|
||||||
return mapPricesForForm({ form: filled, patientDOB: "", insuranceSiteKey });
|
return mapPricesForForm({
|
||||||
|
form: filled,
|
||||||
|
patientDOB: patient?.dateOfBirth ?? "",
|
||||||
|
insuranceSiteKey,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
setPrefillDone(true);
|
setPrefillDone(true);
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -845,6 +852,22 @@ export function ClaimForm({
|
|||||||
} else {
|
} else {
|
||||||
updatedLines[index][field] = value;
|
updatedLines[index][field] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-fill the price as soon as a recognized, fully-typed CDT code is entered,
|
||||||
|
// so users don't have to remember to click "Map Price" for a single line.
|
||||||
|
if (field === "procedureCode") {
|
||||||
|
const code = String(value || "").trim().toUpperCase();
|
||||||
|
if (/^D\d{4}$/.test(code)) {
|
||||||
|
const line = updatedLines[index];
|
||||||
|
const map = getCodeMap(form.insuranceSiteKey);
|
||||||
|
const age = ageOnDate(
|
||||||
|
patient?.dateOfBirth ?? "",
|
||||||
|
line.procedureDate || form.serviceDate,
|
||||||
|
);
|
||||||
|
const price = getPriceForCodeWithAgeFromMap(map, code, age);
|
||||||
|
updatedLines[index] = { ...line, totalBilled: price };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setForm({ ...form, serviceLines: updatedLines });
|
setForm({ ...form, serviceLines: updatedLines });
|
||||||
@@ -1873,7 +1896,17 @@ export function ClaimForm({
|
|||||||
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
|
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
|
||||||
handleUnitedDHPreAuth();
|
handleUnitedDHPreAuth();
|
||||||
} else {
|
} else {
|
||||||
handleMHPreAuth();
|
// Re-apply fee-schedule prices with the now-loaded patient DOB before submitting —
|
||||||
|
// the earlier chatbot-prefill pass may have run before `patient` finished loading,
|
||||||
|
// leaving age-tiered codes (e.g. D2740) unpriced.
|
||||||
|
const siteKeyForPricing =
|
||||||
|
form.insuranceSiteKey || deriveInsuranceSiteKey(form.insuranceProvider || "");
|
||||||
|
const pricedForm = mapPricesForForm({
|
||||||
|
form: { ...form, insuranceSiteKey: siteKeyForPricing },
|
||||||
|
patientDOB: patient?.dateOfBirth ?? "",
|
||||||
|
insuranceSiteKey: siteKeyForPricing,
|
||||||
|
});
|
||||||
|
handleMHPreAuth(pricedForm);
|
||||||
}
|
}
|
||||||
}, [autoSubmitPreauth, prefillDone, isFormReady]);
|
}, [autoSubmitPreauth, prefillDone, isFormReady]);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export default function ClaimsPage() {
|
|||||||
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
||||||
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
|
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
|
||||||
const [chatbotAutoSubmitSiteKey, setChatbotAutoSubmitSiteKey] = useState<string | undefined>(undefined);
|
const [chatbotAutoSubmitSiteKey, setChatbotAutoSubmitSiteKey] = useState<string | undefined>(undefined);
|
||||||
|
// Mirrors chatbotAutoSubmitSiteKey but survives closeClaim(), which resets the state
|
||||||
|
// synchronously right after the selenium job is kicked off — well before the job's async
|
||||||
|
// completion handlers (which gate the PDF preview popup) actually run.
|
||||||
|
const chatbotAutoSubmitSiteKeyRef = useRef<string | undefined>(undefined);
|
||||||
const [chatbotInitialTab, setChatbotInitialTab] = useState<string | undefined>(undefined);
|
const [chatbotInitialTab, setChatbotInitialTab] = useState<string | undefined>(undefined);
|
||||||
const [chatbotAutoSubmitPreauthSiteKey, setChatbotAutoSubmitPreauthSiteKey] = useState<string | undefined>(undefined);
|
const [chatbotAutoSubmitPreauthSiteKey, setChatbotAutoSubmitPreauthSiteKey] = useState<string | undefined>(undefined);
|
||||||
// for redirect from appointment page directly, then passing to claimform
|
// for redirect from appointment page directly, then passing to claimform
|
||||||
@@ -156,11 +160,12 @@ export default function ClaimsPage() {
|
|||||||
if (jobResult.pdfFileId) {
|
if (jobResult.pdfFileId) {
|
||||||
advanceAiClaimQueue();
|
advanceAiClaimQueue();
|
||||||
const advancedToNext = advanceChatbotClaimQueue();
|
const advancedToNext = advanceChatbotClaimQueue();
|
||||||
if (!advancedToNext && !chatbotAutoSubmitSiteKey) {
|
if (!advancedToNext && !chatbotAutoSubmitSiteKeyRef.current) {
|
||||||
setPreviewPdfId(jobResult.pdfFileId);
|
setPreviewPdfId(jobResult.pdfFileId);
|
||||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
chatbotAutoSubmitSiteKeyRef.current = undefined;
|
||||||
dispatch(setTaskStatus({
|
dispatch(setTaskStatus({
|
||||||
key: "claimSubmit",
|
key: "claimSubmit",
|
||||||
status: "success",
|
status: "success",
|
||||||
@@ -240,6 +245,7 @@ export default function ClaimsPage() {
|
|||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (parsed?.autoSubmit && parsed?.siteKey) {
|
if (parsed?.autoSubmit && parsed?.siteKey) {
|
||||||
setChatbotAutoSubmitSiteKey(parsed.siteKey);
|
setChatbotAutoSubmitSiteKey(parsed.siteKey);
|
||||||
|
chatbotAutoSubmitSiteKeyRef.current = parsed.siteKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -308,6 +314,7 @@ export default function ClaimsPage() {
|
|||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (parsed?.autoSubmit && parsed?.siteKey) {
|
if (parsed?.autoSubmit && parsed?.siteKey) {
|
||||||
setChatbotAutoSubmitSiteKey(parsed.siteKey);
|
setChatbotAutoSubmitSiteKey(parsed.siteKey);
|
||||||
|
chatbotAutoSubmitSiteKeyRef.current = parsed.siteKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -994,11 +1001,12 @@ export default function ClaimsPage() {
|
|||||||
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
||||||
// (skip if we're about to navigate to the next queued patient's claim,
|
// (skip if we're about to navigate to the next queued patient's claim,
|
||||||
// or if this claim was submitted via the AI chat column)
|
// or if this claim was submitted via the AI chat column)
|
||||||
if (result.pdfFileId && !advancedToNext && !chatbotAutoSubmitSiteKey) {
|
if (result.pdfFileId && !advancedToNext && !chatbotAutoSubmitSiteKeyRef.current) {
|
||||||
setPreviewPdfId(result.pdfFileId);
|
setPreviewPdfId(result.pdfFileId);
|
||||||
setPreviewFallbackFilename(result.fileName ?? null);
|
setPreviewFallbackFilename(result.fileName ?? null);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
|
chatbotAutoSubmitSiteKeyRef.current = undefined;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -303,6 +303,11 @@ export const PROCEDURE_COMBOS: Record<
|
|||||||
label: "Crown",
|
label: "Crown",
|
||||||
codes: ["D2740"],
|
codes: ["D2740"],
|
||||||
},
|
},
|
||||||
|
recementCrown: {
|
||||||
|
id: "recementCrown",
|
||||||
|
label: "Recement Crown",
|
||||||
|
codes: ["D2920"],
|
||||||
|
},
|
||||||
deepCleaning: {
|
deepCleaning: {
|
||||||
id: "deepCleaning",
|
id: "deepCleaning",
|
||||||
label: "Deep Cleaning",
|
label: "Deep Cleaning",
|
||||||
@@ -396,7 +401,7 @@ export const COMBO_CATEGORIES: Record<
|
|||||||
],
|
],
|
||||||
Implants: ["implantFull", "implantFixture", "implantAbutment", "implantCrown"],
|
Implants: ["implantFull", "implantFixture", "implantAbutment", "implantCrown"],
|
||||||
Endodontics: ["rctAnterior", "rctAnteriorPostCrown", "rctPremolar", "rctPremolarPostCrown", "rctMolar", "rctMolarPostCrown", "postCore", "coreBU"],
|
Endodontics: ["rctAnterior", "rctAnteriorPostCrown", "rctPremolar", "rctPremolarPostCrown", "rctMolar", "rctMolarPostCrown", "postCore", "coreBU"],
|
||||||
Prosthodontics: ["crown"],
|
Prosthodontics: ["crown", "recementCrown"],
|
||||||
Periodontics: ["deepCleaning"],
|
Periodontics: ["deepCleaning"],
|
||||||
Extractions: [
|
Extractions: [
|
||||||
"simpleExtraction",
|
"simpleExtraction",
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const parseDate = (d: DateInput): Date => {
|
|||||||
return new Date(s);
|
return new Date(s);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ageOnDate = (dob: DateInput, on: DateInput): number => {
|
export const ageOnDate = (dob: DateInput, on: DateInput): number => {
|
||||||
const birth = parseDate(dob);
|
const birth = parseDate(dob);
|
||||||
const ref = parseDate(on);
|
const ref = parseDate(on);
|
||||||
let age = ref.getFullYear() - birth.getFullYear();
|
let age = ref.getFullYear() - birth.getFullYear();
|
||||||
|
|||||||
@@ -637,14 +637,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
|
|
||||||
print(f"[DDMA step2] PDF saved at: {pdf_path}")
|
print(f"[DDMA step2] PDF saved at: {pdf_path}")
|
||||||
|
|
||||||
# Close the browser window after PDF (session preserved in profile)
|
|
||||||
try:
|
|
||||||
from ddma_browser_manager import get_browser_manager
|
|
||||||
get_browser_manager().quit_driver()
|
|
||||||
print("[step2] Browser closed - session preserved in profile")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[step2] Error closing browser: {e}")
|
|
||||||
|
|
||||||
print(f"[DDMA step2] Final — PatientName: '{patientName}', MemberID: '{foundMemberId}'")
|
print(f"[DDMA step2] Final — PatientName: '{patientName}', MemberID: '{foundMemberId}'")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user