feat(procedureCodes-dialog) - v2 done
This commit is contained in:
@@ -17,22 +17,9 @@ import {
|
||||
CODE_MAP,
|
||||
getPriceForCodeWithAgeFromMap,
|
||||
} from "@/utils/procedureCombosMapping";
|
||||
import { Patient } from "@repo/db/types";
|
||||
|
||||
interface AppointmentProcedure {
|
||||
id: number;
|
||||
appointmentId: number;
|
||||
patientId: number;
|
||||
procedureCode: string;
|
||||
procedureLabel?: string | null;
|
||||
fee?: number | null;
|
||||
isDirect: boolean;
|
||||
toothNumber?: string | null;
|
||||
toothSurface?: string | null;
|
||||
oralCavityArea?: string | null;
|
||||
source: "COMBO" | "MANUAL";
|
||||
comboKey?: string | null;
|
||||
}
|
||||
import { Patient, AppointmentProcedure } from "@repo/db/types";
|
||||
import { useLocation } from "wouter";
|
||||
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -65,6 +52,10 @@ export function AppointmentProceduresDialog({
|
||||
// -----------------------------
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editRow, setEditRow] = useState<Partial<AppointmentProcedure>>({});
|
||||
const [clearAllOpen, setClearAllOpen] = useState(false);
|
||||
|
||||
// for redirection to claim submission
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
// -----------------------------
|
||||
// fetch procedures
|
||||
@@ -98,7 +89,6 @@ export function AppointmentProceduresDialog({
|
||||
toothNumber: manualTooth || null,
|
||||
toothSurface: manualSurface || null,
|
||||
source: "MANUAL",
|
||||
isDirect: false,
|
||||
};
|
||||
|
||||
const res = await apiRequest(
|
||||
@@ -163,6 +153,30 @@ export function AppointmentProceduresDialog({
|
||||
},
|
||||
});
|
||||
|
||||
const clearAllMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await apiRequest(
|
||||
"DELETE",
|
||||
`/api/appointment-procedures/clear/${appointmentId}`
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to clear procedures");
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "All procedures cleared" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
});
|
||||
setClearAllOpen(false);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: err.message ?? "Failed to clear procedures",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!editingId) return;
|
||||
@@ -184,30 +198,6 @@ export function AppointmentProceduresDialog({
|
||||
},
|
||||
});
|
||||
|
||||
const markClaimModeMutation = useMutation({
|
||||
mutationFn: async (mode: "DIRECT" | "MANUAL") => {
|
||||
const payload = {
|
||||
mode,
|
||||
appointmentId,
|
||||
};
|
||||
const res = await apiRequest(
|
||||
"POST",
|
||||
"/api/appointment-procedures/mark-claim-mode",
|
||||
payload
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to mark claim mode");
|
||||
},
|
||||
onSuccess: (_, mode) => {
|
||||
toast({
|
||||
title:
|
||||
mode === "DIRECT" ? "Direct claim selected" : "Manual claim selected",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// handlers
|
||||
// -----------------------------
|
||||
@@ -242,7 +232,6 @@ export function AppointmentProceduresDialog({
|
||||
source: "COMBO",
|
||||
comboKey: comboKey,
|
||||
toothNumber: combo.toothNumbers?.[idx] ?? null,
|
||||
isDirect: false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -250,6 +239,8 @@ export function AppointmentProceduresDialog({
|
||||
};
|
||||
|
||||
const startEdit = (row: AppointmentProcedure) => {
|
||||
if (!row.id) return;
|
||||
|
||||
setEditingId(row.id);
|
||||
setEditRow({
|
||||
procedureCode: row.procedureCode,
|
||||
@@ -265,12 +256,34 @@ export function AppointmentProceduresDialog({
|
||||
setEditRow({});
|
||||
};
|
||||
|
||||
const handleDirectClaim = () => {
|
||||
setLocation(`/claims?appointmentId=${appointmentId}&mode=direct`);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleManualClaim = () => {
|
||||
setLocation(`/claims?appointmentId=${appointmentId}&mode=manual`);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// UI
|
||||
// -----------------------------
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent
|
||||
className="max-w-6xl max-h-[90vh] overflow-y-auto pointer-events-none"
|
||||
onPointerDownOutside={(e) => {
|
||||
if (clearAllOpen) {
|
||||
e.preventDefault(); // block only when delete dialog is open
|
||||
}
|
||||
}}
|
||||
onInteractOutside={(e) => {
|
||||
if (clearAllOpen) {
|
||||
e.preventDefault(); // block only when delete dialog is open
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
Appointment Procedures
|
||||
@@ -278,7 +291,7 @@ export function AppointmentProceduresDialog({
|
||||
</DialogHeader>
|
||||
|
||||
{/* ================= COMBOS ================= */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 pointer-events-auto">
|
||||
<div className="text-sm font-semibold text-muted-foreground">
|
||||
Quick Add Combos
|
||||
</div>
|
||||
@@ -374,7 +387,18 @@ export function AppointmentProceduresDialog({
|
||||
|
||||
{/* ================= LIST ================= */}
|
||||
<div className="mt-8 space-y-2">
|
||||
<div className="text-sm font-semibold">Selected Procedures</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-semibold">Selected Procedures</div>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={!procedures.length}
|
||||
onClick={() => setClearAllOpen(true)}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg divide-y bg-white">
|
||||
{isLoading && (
|
||||
@@ -418,11 +442,16 @@ export function AppointmentProceduresDialog({
|
||||
/>
|
||||
<Input
|
||||
className="w-[90px]"
|
||||
value={editRow.fee ?? ""}
|
||||
value={
|
||||
editRow.fee !== undefined && editRow.fee !== null
|
||||
? String(editRow.fee)
|
||||
: ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setEditRow({ ...editRow, fee: Number(e.target.value) })
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="w-[80px]"
|
||||
value={editRow.toothNumber ?? ""}
|
||||
@@ -464,20 +493,15 @@ export function AppointmentProceduresDialog({
|
||||
<div className="flex-1 text-muted-foreground">
|
||||
{p.procedureLabel}
|
||||
</div>
|
||||
<div className="w-[90px]">{p.fee}</div>
|
||||
<div className="w-[90px]">
|
||||
{p.fee !== null && p.fee !== undefined
|
||||
? String(p.fee)
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
<div className="w-[80px]">{p.toothNumber}</div>
|
||||
<div className="w-[80px]">{p.toothSurface}</div>
|
||||
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${
|
||||
p.isDirect
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-blue-100 text-blue-700"
|
||||
}`}
|
||||
>
|
||||
{p.isDirect ? "Direct" : "Manual"}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
@@ -489,7 +513,7 @@ export function AppointmentProceduresDialog({
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => deleteMutation.mutate(p.id)}
|
||||
onClick={() => deleteMutation.mutate(p.id!)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
@@ -505,8 +529,8 @@ export function AppointmentProceduresDialog({
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => markClaimModeMutation.mutate("DIRECT")}
|
||||
disabled={!procedures.length}
|
||||
onClick={handleDirectClaim}
|
||||
>
|
||||
Direct Claim
|
||||
</Button>
|
||||
@@ -514,8 +538,8 @@ export function AppointmentProceduresDialog({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-blue-500 text-blue-600 hover:bg-blue-50"
|
||||
onClick={() => markClaimModeMutation.mutate("MANUAL")}
|
||||
disabled={!procedures.length}
|
||||
onClick={handleManualClaim}
|
||||
>
|
||||
Manual Claim
|
||||
</Button>
|
||||
@@ -526,6 +550,16 @@ export function AppointmentProceduresDialog({
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={clearAllOpen}
|
||||
entityName="all procedures for this appointment"
|
||||
onCancel={() => setClearAllOpen(false)}
|
||||
onConfirm={() => {
|
||||
setClearAllOpen(false);
|
||||
clearAllMutation.mutate();
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user