fix - procedure combos file source made

This commit is contained in:
2026-01-19 02:22:44 +05:30
parent c8d2c139c7
commit fd6d55be18
3 changed files with 276 additions and 271 deletions

View File

@@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label";
import { Trash2, Plus, Save, X } from "lucide-react";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { PROCEDURE_COMBOS, COMBO_CATEGORIES } from "@/utils/procedureCombos";
import { PROCEDURE_COMBOS } from "@/utils/procedureCombos";
import {
CODE_MAP,
getPriceForCodeWithAgeFromMap,
@@ -20,6 +20,10 @@ import {
import { Patient, AppointmentProcedure } from "@repo/db/types";
import { useLocation } from "wouter";
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import {
DirectComboButtons,
RegularComboButtons,
} from "@/components/procedure/procedure-combo-buttons";
interface Props {
open: boolean;
@@ -66,13 +70,13 @@ export function AppointmentProceduresDialog({
queryFn: async () => {
const res = await apiRequest(
"GET",
`/api/appointment-procedures/${appointmentId}`
`/api/appointment-procedures/${appointmentId}`,
);
if (!res.ok) throw new Error("Failed to load procedures");
return res.json();
},
enabled: open && !!appointmentId,
}
},
);
// -----------------------------
@@ -94,7 +98,7 @@ export function AppointmentProceduresDialog({
const res = await apiRequest(
"POST",
"/api/appointment-procedures",
payload
payload,
);
if (!res.ok) throw new Error("Failed to add procedure");
return res.json();
@@ -124,7 +128,7 @@ export function AppointmentProceduresDialog({
const res = await apiRequest(
"POST",
"/api/appointment-procedures/bulk",
rows
rows,
);
if (!res.ok) throw new Error("Failed to add combo procedures");
return res.json();
@@ -141,7 +145,7 @@ export function AppointmentProceduresDialog({
mutationFn: async (id: number) => {
const res = await apiRequest(
"DELETE",
`/api/appointment-procedures/${id}`
`/api/appointment-procedures/${id}`,
);
if (!res.ok) throw new Error("Failed to delete");
},
@@ -157,7 +161,7 @@ export function AppointmentProceduresDialog({
mutationFn: async () => {
const res = await apiRequest(
"DELETE",
`/api/appointment-procedures/clear/${appointmentId}`
`/api/appointment-procedures/clear/${appointmentId}`,
);
if (!res.ok) throw new Error("Failed to clear procedures");
},
@@ -183,7 +187,7 @@ export function AppointmentProceduresDialog({
const res = await apiRequest(
"PUT",
`/api/appointment-procedures/${editingId}`,
editRow
editRow,
);
if (!res.ok) throw new Error("Failed to update");
return res.json();
@@ -291,34 +295,18 @@ export function AppointmentProceduresDialog({
</DialogHeader>
{/* ================= COMBOS ================= */}
<div className="space-y-4 pointer-events-auto">
<div className="text-sm font-semibold text-muted-foreground">
Quick Add Combos
</div>
<div className="space-y-8 pointer-events-auto">
<DirectComboButtons
onDirectCombo={(comboKey) => {
handleAddCombo(comboKey);
}}
/>
{Object.entries(COMBO_CATEGORIES).map(([categoryName, comboKeys]) => (
<div key={categoryName} className="space-y-2">
<div className="text-sm font-medium">{categoryName}</div>
<div className="flex flex-wrap gap-2">
{comboKeys.map((comboKey) => {
const combo = PROCEDURE_COMBOS[comboKey];
if (!combo) return null;
return (
<Button
key={comboKey}
variant="secondary"
size="sm"
onClick={() => handleAddCombo(comboKey)}
>
{combo.label}
</Button>
);
})}
</div>
</div>
))}
<RegularComboButtons
onRegularCombo={(comboKey) => {
handleAddCombo(comboKey);
}}
/>
</div>
{/* ================= MANUAL ADD ================= */}

View File

@@ -50,10 +50,14 @@ import {
applyComboToForm,
getDescriptionForCode,
} from "@/utils/procedureCombosMapping";
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos";
import { PROCEDURE_COMBOS } from "@/utils/procedureCombos";
import { DateInput } from "../ui/dateInput";
import { MissingTeethSimple, type MissingMapStrict } from "./tooth-ui";
import { RemarksField } from "./claims-ui";
import {
DirectComboButtons,
RegularComboButtons,
} from "@/components/procedure/procedure-combo-buttons";
interface ClaimFormProps {
patientId: number;
@@ -61,7 +65,7 @@ interface ClaimFormProps {
autoSubmit?: boolean;
onSubmit: (data: ClaimFormData) => Promise<Claim>;
onHandleAppointmentSubmit: (
appointmentData: InsertAppointment | UpdateAppointment
appointmentData: InsertAppointment | UpdateAppointment,
) => Promise<number | { id: number }>;
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
onHandleForMHSeleniumClaim: (data: ClaimFormData) => void;
@@ -123,7 +127,7 @@ export function ClaimForm({
useEffect(() => {
if (staffMembersRaw.length > 0 && !staff) {
const kaiGao = staffMembersRaw.find(
(member) => member.name === "Kai Gao"
(member) => member.name === "Kai Gao",
);
const defaultStaff = kaiGao || staffMembersRaw[0];
if (defaultStaff) setStaff(defaultStaff);
@@ -133,7 +137,7 @@ export function ClaimForm({
// Service date state
const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
const [serviceDate, setServiceDate] = useState<string>(
formatLocalDate(new Date())
formatLocalDate(new Date()),
);
const [serviceDateOpen, setServiceDateOpen] = useState(false);
const [openProcedureDateIndex, setOpenProcedureDateIndex] = useState<
@@ -152,7 +156,7 @@ export function ClaimForm({
try {
const res = await apiRequest(
"GET",
`/api/appointments/${appointmentId}`
`/api/appointments/${appointmentId}`,
);
if (!res.ok) {
let body: any = null;
@@ -191,7 +195,7 @@ export function ClaimForm({
dateVal = new Date(
maybe.getFullYear(),
maybe.getMonth(),
maybe.getDate()
maybe.getDate(),
);
}
@@ -228,7 +232,7 @@ export function ClaimForm({
try {
const res = await apiRequest(
"GET",
`/api/appointment-procedures/prefill-from-appointment/${appointmentId}`
`/api/appointment-procedures/prefill-from-appointment/${appointmentId}`,
);
if (!res.ok) return;
@@ -429,7 +433,7 @@ export function ClaimForm({
const updateServiceLine = (
index: number,
field: keyof InputServiceLine,
value: any
value: any,
) => {
const updatedLines = [...form.serviceLines];
@@ -496,7 +500,7 @@ export function ClaimForm({
mapPricesForForm({
form: prev,
patientDOB: patient?.dateOfBirth ?? "",
})
}),
);
};
@@ -511,7 +515,7 @@ export function ClaimForm({
// 1st Button workflow - Mass Health Button Handler
const handleMHSubmit = async (
formToUse?: ClaimFormData & { uploadedFiles?: File[] }
formToUse?: ClaimFormData & { uploadedFiles?: File[] },
) => {
// Use the passed form, or fallback to current state
const f = formToUse ?? form;
@@ -534,7 +538,7 @@ export function ClaimForm({
// require at least one procedure code before proceeding
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== ""
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
toast({
@@ -620,7 +624,7 @@ export function ClaimForm({
// 2st Button workflow - Mass Health Pre Auth Button Handler
const handleMHPreAuth = async (
formToUse?: ClaimFormData & { uploadedFiles?: File[] }
formToUse?: ClaimFormData & { uploadedFiles?: File[] },
) => {
// Use the passed form, or fallback to current state
const f = formToUse ?? form;
@@ -643,7 +647,7 @@ export function ClaimForm({
// require at least one procedure code before proceeding
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== ""
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
toast({
@@ -707,7 +711,7 @@ export function ClaimForm({
// require at least one procedure code before proceeding
const filteredServiceLines = (form.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== ""
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
toast({
@@ -780,13 +784,13 @@ export function ClaimForm({
// for direct combo button.
const applyComboAndThenMH = async (
comboId: keyof typeof PROCEDURE_COMBOS
comboId: keyof typeof PROCEDURE_COMBOS,
) => {
const nextForm = applyComboToForm(
form,
comboId,
patient?.dateOfBirth ?? "",
{ replaceAll: false, lineDate: form.serviceDate }
{ replaceAll: false, lineDate: form.serviceDate },
);
setForm(nextForm);
@@ -803,7 +807,7 @@ export function ClaimForm({
!!form.patientName?.trim() &&
Array.isArray(form.serviceLines) &&
form.serviceLines.some(
(l) => l.procedureCode && l.procedureCode.trim() !== ""
(l) => l.procedureCode && l.procedureCode.trim() !== "",
)
);
}, [
@@ -963,7 +967,7 @@ export function ClaimForm({
value={staff?.id?.toString() || ""}
onValueChange={(id) => {
const selected = staffMembersRaw.find(
(member) => member.id?.toString() === id
(member) => member.id?.toString() === id,
);
if (selected) {
setStaff(selected);
@@ -1007,163 +1011,11 @@ export function ClaimForm({
</div>
</div>
<div className="space-y-6">
{/* Section Title */}
<div className="text-sm font-semibold text-muted-foreground">
Direct Claim Submission Buttons
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* CHILD RECALL GROUP */}
<div className="space-y-2">
<div className="text-sm font-medium opacity-80">
Child Recall
</div>
<div className="flex flex-wrap gap-2">
{[
"childRecallDirect",
"childRecallDirect2BW",
"childRecallDirect4BW",
"childRecallDirect2PA2BW",
"childRecallDirect2PA4BW",
"childRecallDirect3PA2BW",
"childRecallDirect3PA",
"childRecallDirect4PA",
"childRecallDirectPANO",
].map((comboId) => {
const b = PROCEDURE_COMBOS[comboId];
if (!b) return null;
const codesWithTooth = b.codes.map((code, idx) => {
const tooth = b.toothNumbers?.[idx];
return tooth ? `${code} (tooth ${tooth})` : code;
});
const tooltipText = codesWithTooth.join(", ");
const labelMap: Record<string, string> = {
childRecallDirect: "Direct",
childRecallDirect2BW: "Direct 2BW",
childRecallDirect4BW: "Direct 4BW",
childRecallDirect2PA2BW: "Direct 2PA 2BW",
childRecallDirect2PA4BW: "Direct 2PA 4BW",
childRecallDirect3PA2BW: "Direct 3PA 2BW",
childRecallDirect3PA: "Direct 3PA",
childRecallDirect4PA: "Direct 4PA",
childRecallDirectPANO: "Direct Pano",
};
return (
<Tooltip key={b.id}>
<TooltipTrigger asChild>
<Button
variant="secondary"
onClick={() => applyComboAndThenMH(b.id)}
aria-label={`${b.label} — codes: ${tooltipText}`}
>
{labelMap[comboId] ?? b.label}
</Button>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-sm max-w-xs break-words">
{tooltipText}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</div>
{/* ADULT RECALL GROUP */}
<div className="space-y-2">
<div className="text-sm font-medium opacity-80">
Adult Recall
</div>
<div className="flex flex-wrap gap-2">
{[
"adultRecallDirect",
"adultRecallDirect2BW",
"adultRecallDirect4BW",
"adultRecallDirect2PA2BW",
"adultRecallDirect2PA4BW",
"adultRecallDirect4PA",
"adultRecallDirectPano",
].map((comboId) => {
const b = PROCEDURE_COMBOS[comboId];
if (!b) return null;
const codesWithTooth = b.codes.map((code, idx) => {
const tooth = b.toothNumbers?.[idx];
return tooth ? `${code} (tooth ${tooth})` : code;
});
const tooltipText = codesWithTooth.join(", ");
const labelMap: Record<string, string> = {
adultRecallDirect: "Direct",
adultRecallDirect2BW: "Direct 2BW",
adultRecallDirect4BW: "Direct 4BW",
adultRecallDirect2PA2BW: "Direct 2PA 2BW",
adultRecallDirect2PA4BW: "Direct 2PA 4BW",
adultRecallDirect4PA: "Direct 4PA",
adultRecallDirectPano: "Direct Pano",
};
return (
<Tooltip key={b.id}>
<TooltipTrigger asChild>
<Button
variant="secondary"
onClick={() => applyComboAndThenMH(b.id)}
aria-label={`${b.label} — codes: ${tooltipText}`}
>
{labelMap[comboId] ?? b.label}
</Button>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-sm max-w-xs break-words">
{tooltipText}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</div>
{/* ORTH GROUP */}
<div className="space-y-2">
<div className="text-sm font-medium opacity-80">Orth</div>
<div className="flex flex-wrap gap-2">
{[
"orthPreExamDirect",
"orthRecordDirect",
"orthPerioVisitDirect",
"orthRetentionDirect",
].map((comboId) => {
const b = PROCEDURE_COMBOS[comboId];
if (!b) return null;
const tooltipText = b.codes.join(", ");
return (
<Tooltip key={b.id}>
<TooltipTrigger asChild>
<Button
variant="secondary"
onClick={() => applyComboAndThenMH(b.id)}
aria-label={`${b.label} — codes: ${tooltipText}`}
>
{b.label}
</Button>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-sm max-w-xs break-words">
{tooltipText}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</div>
</div>
</div>
<DirectComboButtons
onDirectCombo={(comboKey) =>
applyComboAndThenMH(comboKey as any)
}
/>
</div>
{/* Header */}
@@ -1221,7 +1073,7 @@ export function ClaimForm({
updateServiceLine(
i,
"procedureCode",
e.target.value.toUpperCase()
e.target.value.toUpperCase(),
)
}
/>
@@ -1308,7 +1160,7 @@ export function ClaimForm({
updateServiceLine(
i,
"totalBilled",
isNaN(rounded) ? 0 : rounded
isNaN(rounded) ? 0 : rounded,
);
}}
/>
@@ -1341,66 +1193,24 @@ export function ClaimForm({
+ Add Service Line
</Button>
<div className="space-y-4 mt-8">
{Object.entries(COMBO_CATEGORIES).map(([section, ids]) => (
<div key={section}>
<div className="mb-3 text-sm font-semibold opacity-70">
{section}
</div>
<div className="flex flex-wrap gap-1">
{ids.map((id) => {
const b = PROCEDURE_COMBOS[id];
if (!b) {
return;
}
// Build a human readable string for the tooltip
const codesWithTooth = b.codes.map((code, idx) => {
const tooth = b.toothNumbers?.[idx];
return tooth ? `${code} (tooth ${tooth})` : code;
});
const tooltipText = codesWithTooth.join(", ");
<RegularComboButtons
onRegularCombo={(comboKey) => {
setForm((prev) => {
const next = applyComboToForm(
prev,
comboKey as any,
patient?.dateOfBirth ?? "",
{
replaceAll: false,
lineDate: prev.serviceDate,
},
);
return (
<Tooltip key={b.id}>
<TooltipTrigger asChild>
<Button
key={b.id}
variant="secondary"
onClick={() =>
setForm((prev) => {
const next = applyComboToForm(
prev,
b.id as any,
patient?.dateOfBirth ?? "",
{
replaceAll: false,
lineDate: prev.serviceDate,
}
);
setTimeout(() => scrollToLine(0), 0);
return next;
})
}
aria-label={`${b.label} — codes: ${tooltipText}`}
>
{b.label}
</Button>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-sm max-w-xs break-words">
{tooltipText}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</div>
))}
</div>
setTimeout(() => scrollToLine(0), 0);
return next;
});
}}
/>
</div>
{/* File Upload Section */}

View File

@@ -0,0 +1,207 @@
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
PROCEDURE_COMBOS,
COMBO_CATEGORIES,
} from "@/utils/procedureCombos";
/* =========================================================
DIRECT COMBO BUTTONS (TOP SECTION)
========================================================= */
export function DirectComboButtons({
onDirectCombo,
}: {
onDirectCombo: (comboKey: string) => void;
}) {
return (
<div className="space-y-6">
{/* Section Title */}
<div className="text-sm font-semibold text-muted-foreground">
Direct Claim Submission Buttons
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* CHILD RECALL */}
<DirectGroup
title="Child Recall"
combos={[
"childRecallDirect",
"childRecallDirect2BW",
"childRecallDirect4BW",
"childRecallDirect2PA2BW",
"childRecallDirect2PA4BW",
"childRecallDirect3PA2BW",
"childRecallDirect3PA",
"childRecallDirect4PA",
"childRecallDirectPANO",
]}
labelMap={{
childRecallDirect: "Direct",
childRecallDirect2BW: "Direct 2BW",
childRecallDirect4BW: "Direct 4BW",
childRecallDirect2PA2BW: "Direct 2PA 2BW",
childRecallDirect2PA4BW: "Direct 2PA 4BW",
childRecallDirect3PA2BW: "Direct 3PA 2BW",
childRecallDirect3PA: "Direct 3PA",
childRecallDirect4PA: "Direct 4PA",
childRecallDirectPANO: "Direct Pano",
}}
onSelect={onDirectCombo}
/>
{/* ADULT RECALL */}
<DirectGroup
title="Adult Recall"
combos={[
"adultRecallDirect",
"adultRecallDirect2BW",
"adultRecallDirect4BW",
"adultRecallDirect2PA2BW",
"adultRecallDirect2PA4BW",
"adultRecallDirect4PA",
"adultRecallDirectPano",
]}
labelMap={{
adultRecallDirect: "Direct",
adultRecallDirect2BW: "Direct 2BW",
adultRecallDirect4BW: "Direct 4BW",
adultRecallDirect2PA2BW: "Direct 2PA 2BW",
adultRecallDirect2PA4BW: "Direct 2PA 4BW",
adultRecallDirect4PA: "Direct 4PA",
adultRecallDirectPano: "Direct Pano",
}}
onSelect={onDirectCombo}
/>
{/* ORTH */}
<DirectGroup
title="Orth"
combos={[
"orthPreExamDirect",
"orthRecordDirect",
"orthPerioVisitDirect",
"orthRetentionDirect",
]}
onSelect={onDirectCombo}
/>
</div>
</div>
);
}
/* =========================================================
REGULAR COMBO BUTTONS (BOTTOM SECTION)
========================================================= */
export function RegularComboButtons({
onRegularCombo,
}: {
onRegularCombo: (comboKey: string) => void;
}) {
return (
<div className="space-y-4 mt-8">
{Object.entries(COMBO_CATEGORIES).map(([section, ids]) => (
<div key={section}>
<div className="mb-3 text-sm font-semibold opacity-70">
{section}
</div>
<div className="flex flex-wrap gap-1">
{ids.map((id) => {
const b = PROCEDURE_COMBOS[id];
if (!b) return null;
const tooltipText = b.codes
.map((code, idx) => {
const tooth = b.toothNumbers?.[idx];
return tooth ? `${code} (tooth ${tooth})` : code;
})
.join(", ");
return (
<Tooltip key={id}>
<TooltipTrigger asChild>
<Button
variant="secondary"
onClick={() => onRegularCombo(id)}
aria-label={`${b.label} — codes: ${tooltipText}`}
>
{b.label}
</Button>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-sm max-w-xs break-words">
{tooltipText}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</div>
))}
</div>
);
}
/* =========================================================
INTERNAL HELPERS
========================================================= */
function DirectGroup({
title,
combos,
labelMap,
onSelect,
}: {
title: string;
combos: string[];
labelMap?: Record<string, string>;
onSelect: (id: string) => void;
}) {
return (
<div className="space-y-2">
<div className="text-sm font-medium opacity-80">{title}</div>
<div className="flex flex-wrap gap-2">
{combos.map((id) => {
const b = PROCEDURE_COMBOS[id];
if (!b) return null;
const tooltipText = b.codes
.map((code, idx) => {
const tooth = b.toothNumbers?.[idx];
return tooth ? `${code} (tooth ${tooth})` : code;
})
.join(", ");
return (
<Tooltip key={id}>
<TooltipTrigger asChild>
<Button
variant="secondary"
onClick={() => onSelect(id)}
aria-label={`${b.label} — codes: ${tooltipText}`}
>
{labelMap?.[id] ?? b.label}
</Button>
</TooltipTrigger>
<TooltipContent side="top" align="center">
<div className="text-sm max-w-xs break-words">
{tooltipText}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</div>
);
}