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 { 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 ================= */}
|
||||||
|
|||||||
@@ -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}>
|
setForm((prev) => {
|
||||||
<div className="mb-3 text-sm font-semibold opacity-70">
|
const next = applyComboToForm(
|
||||||
{section}
|
prev,
|
||||||
</div>
|
comboKey as any,
|
||||||
<div className="flex flex-wrap gap-1">
|
patient?.dateOfBirth ?? "",
|
||||||
{ids.map((id) => {
|
{
|
||||||
const b = PROCEDURE_COMBOS[id];
|
replaceAll: false,
|
||||||
if (!b) {
|
lineDate: prev.serviceDate,
|
||||||
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 (
|
setTimeout(() => scrollToLine(0), 0);
|
||||||
<Tooltip key={b.id}>
|
return next;
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Upload Section */}
|
{/* 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