fix - procedure combos file source made
This commit is contained in:
@@ -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 ================= */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user