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

View File

@@ -50,10 +50,14 @@ import {
applyComboToForm, applyComboToForm,
getDescriptionForCode, getDescriptionForCode,
} from "@/utils/procedureCombosMapping"; } from "@/utils/procedureCombosMapping";
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos"; import { PROCEDURE_COMBOS } from "@/utils/procedureCombos";
import { DateInput } from "../ui/dateInput"; import { DateInput } from "../ui/dateInput";
import { MissingTeethSimple, type MissingMapStrict } from "./tooth-ui"; import { MissingTeethSimple, type MissingMapStrict } from "./tooth-ui";
import { RemarksField } from "./claims-ui"; import { RemarksField } from "./claims-ui";
import {
DirectComboButtons,
RegularComboButtons,
} from "@/components/procedure/procedure-combo-buttons";
interface ClaimFormProps { interface ClaimFormProps {
patientId: number; patientId: number;
@@ -61,7 +65,7 @@ interface ClaimFormProps {
autoSubmit?: boolean; autoSubmit?: boolean;
onSubmit: (data: ClaimFormData) => Promise<Claim>; onSubmit: (data: ClaimFormData) => Promise<Claim>;
onHandleAppointmentSubmit: ( onHandleAppointmentSubmit: (
appointmentData: InsertAppointment | UpdateAppointment appointmentData: InsertAppointment | UpdateAppointment,
) => Promise<number | { id: number }>; ) => Promise<number | { id: number }>;
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void; onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
onHandleForMHSeleniumClaim: (data: ClaimFormData) => void; onHandleForMHSeleniumClaim: (data: ClaimFormData) => void;
@@ -123,7 +127,7 @@ export function ClaimForm({
useEffect(() => { useEffect(() => {
if (staffMembersRaw.length > 0 && !staff) { if (staffMembersRaw.length > 0 && !staff) {
const kaiGao = staffMembersRaw.find( const kaiGao = staffMembersRaw.find(
(member) => member.name === "Kai Gao" (member) => member.name === "Kai Gao",
); );
const defaultStaff = kaiGao || staffMembersRaw[0]; const defaultStaff = kaiGao || staffMembersRaw[0];
if (defaultStaff) setStaff(defaultStaff); if (defaultStaff) setStaff(defaultStaff);
@@ -133,7 +137,7 @@ export function ClaimForm({
// Service date state // Service date state
const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date()); const [serviceDateValue, setServiceDateValue] = useState<Date>(new Date());
const [serviceDate, setServiceDate] = useState<string>( const [serviceDate, setServiceDate] = useState<string>(
formatLocalDate(new Date()) formatLocalDate(new Date()),
); );
const [serviceDateOpen, setServiceDateOpen] = useState(false); const [serviceDateOpen, setServiceDateOpen] = useState(false);
const [openProcedureDateIndex, setOpenProcedureDateIndex] = useState< const [openProcedureDateIndex, setOpenProcedureDateIndex] = useState<
@@ -152,7 +156,7 @@ export function ClaimForm({
try { try {
const res = await apiRequest( const res = await apiRequest(
"GET", "GET",
`/api/appointments/${appointmentId}` `/api/appointments/${appointmentId}`,
); );
if (!res.ok) { if (!res.ok) {
let body: any = null; let body: any = null;
@@ -191,7 +195,7 @@ export function ClaimForm({
dateVal = new Date( dateVal = new Date(
maybe.getFullYear(), maybe.getFullYear(),
maybe.getMonth(), maybe.getMonth(),
maybe.getDate() maybe.getDate(),
); );
} }
@@ -228,7 +232,7 @@ export function ClaimForm({
try { try {
const res = await apiRequest( const res = await apiRequest(
"GET", "GET",
`/api/appointment-procedures/prefill-from-appointment/${appointmentId}` `/api/appointment-procedures/prefill-from-appointment/${appointmentId}`,
); );
if (!res.ok) return; if (!res.ok) return;
@@ -429,7 +433,7 @@ export function ClaimForm({
const updateServiceLine = ( const updateServiceLine = (
index: number, index: number,
field: keyof InputServiceLine, field: keyof InputServiceLine,
value: any value: any,
) => { ) => {
const updatedLines = [...form.serviceLines]; const updatedLines = [...form.serviceLines];
@@ -496,7 +500,7 @@ export function ClaimForm({
mapPricesForForm({ mapPricesForForm({
form: prev, form: prev,
patientDOB: patient?.dateOfBirth ?? "", patientDOB: patient?.dateOfBirth ?? "",
}) }),
); );
}; };
@@ -511,7 +515,7 @@ export function ClaimForm({
// 1st Button workflow - Mass Health Button Handler // 1st Button workflow - Mass Health Button Handler
const handleMHSubmit = async ( const handleMHSubmit = async (
formToUse?: ClaimFormData & { uploadedFiles?: File[] } formToUse?: ClaimFormData & { uploadedFiles?: File[] },
) => { ) => {
// Use the passed form, or fallback to current state // Use the passed form, or fallback to current state
const f = formToUse ?? form; const f = formToUse ?? form;
@@ -534,7 +538,7 @@ export function ClaimForm({
// require at least one procedure code before proceeding // require at least one procedure code before proceeding
const filteredServiceLines = (f.serviceLines || []).filter( const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "" (line) => (line.procedureCode ?? "").trim() !== "",
); );
if (filteredServiceLines.length === 0) { if (filteredServiceLines.length === 0) {
toast({ toast({
@@ -620,7 +624,7 @@ export function ClaimForm({
// 2st Button workflow - Mass Health Pre Auth Button Handler // 2st Button workflow - Mass Health Pre Auth Button Handler
const handleMHPreAuth = async ( const handleMHPreAuth = async (
formToUse?: ClaimFormData & { uploadedFiles?: File[] } formToUse?: ClaimFormData & { uploadedFiles?: File[] },
) => { ) => {
// Use the passed form, or fallback to current state // Use the passed form, or fallback to current state
const f = formToUse ?? form; const f = formToUse ?? form;
@@ -643,7 +647,7 @@ export function ClaimForm({
// require at least one procedure code before proceeding // require at least one procedure code before proceeding
const filteredServiceLines = (f.serviceLines || []).filter( const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "" (line) => (line.procedureCode ?? "").trim() !== "",
); );
if (filteredServiceLines.length === 0) { if (filteredServiceLines.length === 0) {
toast({ toast({
@@ -707,7 +711,7 @@ export function ClaimForm({
// require at least one procedure code before proceeding // require at least one procedure code before proceeding
const filteredServiceLines = (form.serviceLines || []).filter( const filteredServiceLines = (form.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "" (line) => (line.procedureCode ?? "").trim() !== "",
); );
if (filteredServiceLines.length === 0) { if (filteredServiceLines.length === 0) {
toast({ toast({
@@ -780,13 +784,13 @@ export function ClaimForm({
// for direct combo button. // for direct combo button.
const applyComboAndThenMH = async ( const applyComboAndThenMH = async (
comboId: keyof typeof PROCEDURE_COMBOS comboId: keyof typeof PROCEDURE_COMBOS,
) => { ) => {
const nextForm = applyComboToForm( const nextForm = applyComboToForm(
form, form,
comboId, comboId,
patient?.dateOfBirth ?? "", patient?.dateOfBirth ?? "",
{ replaceAll: false, lineDate: form.serviceDate } { replaceAll: false, lineDate: form.serviceDate },
); );
setForm(nextForm); setForm(nextForm);
@@ -803,7 +807,7 @@ export function ClaimForm({
!!form.patientName?.trim() && !!form.patientName?.trim() &&
Array.isArray(form.serviceLines) && Array.isArray(form.serviceLines) &&
form.serviceLines.some( 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() || ""} value={staff?.id?.toString() || ""}
onValueChange={(id) => { onValueChange={(id) => {
const selected = staffMembersRaw.find( const selected = staffMembersRaw.find(
(member) => member.id?.toString() === id (member) => member.id?.toString() === id,
); );
if (selected) { if (selected) {
setStaff(selected); setStaff(selected);
@@ -1007,163 +1011,11 @@ export function ClaimForm({
</div> </div>
</div> </div>
<div className="space-y-6"> <DirectComboButtons
{/* Section Title */} onDirectCombo={(comboKey) =>
<div className="text-sm font-semibold text-muted-foreground"> applyComboAndThenMH(comboKey as any)
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>
</div> </div>
{/* Header */} {/* Header */}
@@ -1221,7 +1073,7 @@ export function ClaimForm({
updateServiceLine( updateServiceLine(
i, i,
"procedureCode", "procedureCode",
e.target.value.toUpperCase() e.target.value.toUpperCase(),
) )
} }
/> />
@@ -1308,7 +1160,7 @@ export function ClaimForm({
updateServiceLine( updateServiceLine(
i, i,
"totalBilled", "totalBilled",
isNaN(rounded) ? 0 : rounded isNaN(rounded) ? 0 : rounded,
); );
}} }}
/> />
@@ -1341,66 +1193,24 @@ export function ClaimForm({
+ Add Service Line + Add Service Line
</Button> </Button>
<div className="space-y-4 mt-8"> <RegularComboButtons
{Object.entries(COMBO_CATEGORIES).map(([section, ids]) => ( onRegularCombo={(comboKey) => {
<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(", ");
return (
<Tooltip key={b.id}>
<TooltipTrigger asChild>
<Button
key={b.id}
variant="secondary"
onClick={() =>
setForm((prev) => { setForm((prev) => {
const next = applyComboToForm( const next = applyComboToForm(
prev, prev,
b.id as any, comboKey as any,
patient?.dateOfBirth ?? "", patient?.dateOfBirth ?? "",
{ {
replaceAll: false, replaceAll: false,
lineDate: prev.serviceDate, lineDate: prev.serviceDate,
} },
); );
setTimeout(() => scrollToLine(0), 0); setTimeout(() => scrollToLine(0), 0);
return next; 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>
</div> </div>
{/* File Upload Section */} {/* 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>
);
}