feat(procedureCodes-dialog) - v2 done

This commit is contained in:
2026-01-15 02:50:46 +05:30
parent c53dfd544d
commit a0b3189430
10 changed files with 301 additions and 122 deletions

View File

@@ -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>
);
}