feat: Select Procedures flow, batch-column NPI provider fix, auto PDF save
- Add 'Select Procedures' right-click option on appointment page (separate from Claims/PreAuth) - Select Procedures form saves CDT codes + NPI provider to AppointmentProcedure storage - Remove Save button from insurance claim form; Claims/PreAuth opens for insurance submission only - Claims/PreAuth auto-prefills from saved procedures including NPI provider - Batch-column: procedures npiProviderId takes priority over stale claim npiProviderId - Batch-column: auto-save PDF to patient Documents after successful submission (no socket needed) - Add npiProviderId column to AppointmentProcedure table (prisma db push) - Fix 'invalid db creation invocation': guard staffId, npiProviderId, procedureDate as Date object, totalBilled NaN guard - Add full error logging to batch-column catch block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Trash2, Plus, Save, X } from "lucide-react";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -17,7 +24,7 @@ import {
|
||||
CODE_MAP,
|
||||
getPriceForCodeWithAgeFromMap,
|
||||
} from "@/utils/procedureCombosMapping";
|
||||
import { Patient, AppointmentProcedure } from "@repo/db/types";
|
||||
import { Patient, AppointmentProcedure, NpiProvider } from "@repo/db/types";
|
||||
import { useLocation } from "wouter";
|
||||
import { DeleteConfirmationDialog } from "../ui/deleteDialog";
|
||||
import {
|
||||
@@ -31,6 +38,7 @@ interface Props {
|
||||
appointmentId: number;
|
||||
patientId: number;
|
||||
patient: Patient;
|
||||
serviceDate?: string;
|
||||
}
|
||||
|
||||
export function AppointmentProceduresDialog({
|
||||
@@ -39,54 +47,88 @@ export function AppointmentProceduresDialog({
|
||||
appointmentId,
|
||||
patientId,
|
||||
patient,
|
||||
serviceDate,
|
||||
}: Props) {
|
||||
const { toast } = useToast();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
// -----------------------------
|
||||
// state for manual add
|
||||
// -----------------------------
|
||||
// NPI provider state — stored per-appointment on the procedure rows
|
||||
const [selectedNpiProviderId, setSelectedNpiProviderId] = useState<number | null>(null);
|
||||
|
||||
// manual add row state
|
||||
const [manualCode, setManualCode] = useState("");
|
||||
const [manualLabel, setManualLabel] = useState("");
|
||||
const [manualFee, setManualFee] = useState("");
|
||||
const [manualTooth, setManualTooth] = useState("");
|
||||
const [manualSurface, setManualSurface] = useState("");
|
||||
|
||||
// -----------------------------
|
||||
// state for inline edit
|
||||
// -----------------------------
|
||||
// inline edit state
|
||||
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
|
||||
// -----------------------------
|
||||
const { data: procedures = [], isLoading } = useQuery<AppointmentProcedure[]>(
|
||||
{
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest(
|
||||
"GET",
|
||||
`/api/appointment-procedures/${appointmentId}`,
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to load procedures");
|
||||
return res.json();
|
||||
},
|
||||
enabled: open && !!appointmentId,
|
||||
// ── NPI Providers ──────────────────────────────────────────────
|
||||
const { data: npiProviders = [] } = useQuery<NpiProvider[]>({
|
||||
queryKey: ["/api/npiProviders/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/npiProviders/");
|
||||
if (!res.ok) throw new Error("Failed to fetch NPI providers");
|
||||
return res.json();
|
||||
},
|
||||
);
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
// ── Procedures ─────────────────────────────────────────────────
|
||||
const { data: procedures = [], isLoading } = useQuery<AppointmentProcedure[]>({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", `/api/appointment-procedures/${appointmentId}`);
|
||||
if (!res.ok) throw new Error("Failed to load procedures");
|
||||
return res.json();
|
||||
},
|
||||
enabled: open && !!appointmentId,
|
||||
});
|
||||
|
||||
// Sync NPI provider from saved procedures when they load
|
||||
useEffect(() => {
|
||||
if (!procedures.length) return;
|
||||
const saved = (procedures[0] as any)?.npiProviderId ?? null;
|
||||
if (saved != null) setSelectedNpiProviderId(Number(saved));
|
||||
}, [procedures]);
|
||||
|
||||
// Default NPI provider to Mary Scannell / first when none saved yet
|
||||
useEffect(() => {
|
||||
if (selectedNpiProviderId != null || !npiProviders.length) return;
|
||||
const mary = npiProviders.find((p) => p.providerName.toLowerCase() === "mary scannell");
|
||||
setSelectedNpiProviderId((mary ?? npiProviders[0])?.id ?? null);
|
||||
}, [npiProviders, selectedNpiProviderId]);
|
||||
|
||||
// ── Mutations ──────────────────────────────────────────────────
|
||||
|
||||
const setNpiMutation = useMutation({
|
||||
mutationFn: async (npiProviderId: number | null) => {
|
||||
const res = await apiRequest(
|
||||
"PUT",
|
||||
`/api/appointment-procedures/set-npi-provider/${appointmentId}`,
|
||||
{ npiProviderId },
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to update provider");
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "Rendering provider saved" });
|
||||
queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({ title: "Error", description: err.message, variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// mutations
|
||||
// -----------------------------
|
||||
const addManualMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const payload = {
|
||||
appointmentId,
|
||||
patientId,
|
||||
npiProviderId: selectedNpiProviderId ?? null,
|
||||
procedureCode: manualCode,
|
||||
procedureLabel: manualLabel || null,
|
||||
fee: manualFee ? Number(manualFee) : null,
|
||||
@@ -94,101 +136,63 @@ export function AppointmentProceduresDialog({
|
||||
toothSurface: manualSurface || null,
|
||||
source: "MANUAL",
|
||||
};
|
||||
|
||||
const res = await apiRequest(
|
||||
"POST",
|
||||
"/api/appointment-procedures",
|
||||
payload,
|
||||
);
|
||||
const res = await apiRequest("POST", "/api/appointment-procedures", payload);
|
||||
if (!res.ok) throw new Error("Failed to add procedure");
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "Procedure added" });
|
||||
setManualCode("");
|
||||
setManualLabel("");
|
||||
setManualFee("");
|
||||
setManualTooth("");
|
||||
setManualSurface("");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
});
|
||||
setManualCode(""); setManualLabel(""); setManualFee("");
|
||||
setManualTooth(""); setManualSurface("");
|
||||
queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: err.message ?? "Failed to add procedure",
|
||||
variant: "destructive",
|
||||
});
|
||||
toast({ title: "Error", description: err.message ?? "Failed to add procedure", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const bulkAddMutation = useMutation({
|
||||
mutationFn: async (rows: any[]) => {
|
||||
const res = await apiRequest(
|
||||
"POST",
|
||||
"/api/appointment-procedures/bulk",
|
||||
rows,
|
||||
);
|
||||
const res = await apiRequest("POST", "/api/appointment-procedures/bulk", rows);
|
||||
if (!res.ok) throw new Error("Failed to add combo procedures");
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "Combo added" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const res = await apiRequest(
|
||||
"DELETE",
|
||||
`/api/appointment-procedures/${id}`,
|
||||
);
|
||||
const res = await apiRequest("DELETE", `/api/appointment-procedures/${id}`);
|
||||
if (!res.ok) throw new Error("Failed to delete");
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "Deleted" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] });
|
||||
},
|
||||
});
|
||||
|
||||
const clearAllMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await apiRequest(
|
||||
"DELETE",
|
||||
`/api/appointment-procedures/clear/${appointmentId}`,
|
||||
);
|
||||
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],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] });
|
||||
setClearAllOpen(false);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: err.message ?? "Failed to clear procedures",
|
||||
variant: "destructive",
|
||||
});
|
||||
toast({ title: "Error", description: err.message ?? "Failed to clear procedures", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!editingId) return;
|
||||
const res = await apiRequest(
|
||||
"PUT",
|
||||
`/api/appointment-procedures/${editingId}`,
|
||||
editRow,
|
||||
);
|
||||
const res = await apiRequest("PUT", `/api/appointment-procedures/${editingId}`, editRow);
|
||||
if (!res.ok) throw new Error("Failed to update");
|
||||
return res.json();
|
||||
},
|
||||
@@ -196,55 +200,42 @@ export function AppointmentProceduresDialog({
|
||||
toast({ title: "Updated" });
|
||||
setEditingId(null);
|
||||
setEditRow({});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["appointment-procedures", appointmentId],
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["appointment-procedures", appointmentId] });
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// handlers
|
||||
// -----------------------------
|
||||
// ── Handlers ───────────────────────────────────────────────────
|
||||
|
||||
const handleAddCombo = (comboKey: string) => {
|
||||
const combo = PROCEDURE_COMBOS[comboKey];
|
||||
if (!combo || !patient?.dateOfBirth) return;
|
||||
|
||||
const serviceDate = new Date();
|
||||
const dob = patient.dateOfBirth;
|
||||
const ref = new Date();
|
||||
const birth = new Date(dob as any);
|
||||
let age = ref.getFullYear() - birth.getFullYear();
|
||||
const hadBirthday =
|
||||
ref.getMonth() > birth.getMonth() ||
|
||||
(ref.getMonth() === birth.getMonth() && ref.getDate() >= birth.getDate());
|
||||
if (!hadBirthday) age -= 1;
|
||||
|
||||
const age = (() => {
|
||||
const birth = new Date(dob);
|
||||
const ref = new Date(serviceDate);
|
||||
let a = ref.getFullYear() - birth.getFullYear();
|
||||
const hadBirthday =
|
||||
ref.getMonth() > birth.getMonth() ||
|
||||
(ref.getMonth() === birth.getMonth() &&
|
||||
ref.getDate() >= birth.getDate());
|
||||
if (!hadBirthday) a -= 1;
|
||||
return a;
|
||||
})();
|
||||
|
||||
const rows = combo.codes.map((code: string, idx: number) => {
|
||||
const priceDecimal = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age);
|
||||
|
||||
return {
|
||||
appointmentId,
|
||||
patientId,
|
||||
procedureCode: code,
|
||||
procedureLabel: combo.label,
|
||||
fee: priceDecimal.toNumber(),
|
||||
source: "COMBO",
|
||||
comboKey: comboKey,
|
||||
toothNumber: combo.toothNumbers?.[idx] ?? null,
|
||||
};
|
||||
});
|
||||
const rows = combo.codes.map((code: string, idx: number) => ({
|
||||
appointmentId,
|
||||
patientId,
|
||||
npiProviderId: selectedNpiProviderId ?? null,
|
||||
procedureCode: code,
|
||||
procedureLabel: combo.label,
|
||||
fee: getPriceForCodeWithAgeFromMap(CODE_MAP, code, age).toNumber(),
|
||||
source: "COMBO",
|
||||
comboKey,
|
||||
toothNumber: combo.toothNumbers?.[idx] ?? null,
|
||||
}));
|
||||
|
||||
bulkAddMutation.mutate(rows);
|
||||
};
|
||||
|
||||
const startEdit = (row: AppointmentProcedure) => {
|
||||
if (!row.id) return;
|
||||
|
||||
setEditingId(row.id);
|
||||
setEditRow({
|
||||
procedureCode: row.procedureCode,
|
||||
@@ -255,10 +246,7 @@ export function AppointmentProceduresDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setEditRow({});
|
||||
};
|
||||
const cancelEdit = () => { setEditingId(null); setEditRow({}); };
|
||||
|
||||
const handleDirectClaim = () => {
|
||||
setLocation(`/claims?appointmentId=${appointmentId}&mode=direct`);
|
||||
@@ -270,256 +258,146 @@ export function AppointmentProceduresDialog({
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// UI
|
||||
// -----------------------------
|
||||
const selectedProvider = npiProviders.find((p) => p.id === selectedNpiProviderId);
|
||||
|
||||
// ── UI ─────────────────────────────────────────────────────────
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<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
|
||||
}
|
||||
}}
|
||||
onPointerDownOutside={(e) => { if (clearAllOpen) e.preventDefault(); }}
|
||||
onInteractOutside={(e) => { if (clearAllOpen) e.preventDefault(); }}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
Appointment Procedures
|
||||
{serviceDate && <span className="ml-3 text-base font-normal text-muted-foreground">{serviceDate}</span>}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* ================= COMBOS ================= */}
|
||||
<div className="space-y-8 pointer-events-auto">
|
||||
<DirectComboButtons
|
||||
onDirectCombo={(comboKey) => {
|
||||
handleAddCombo(comboKey);
|
||||
}}
|
||||
/>
|
||||
|
||||
<RegularComboButtons
|
||||
onRegularCombo={(comboKey) => {
|
||||
handleAddCombo(comboKey);
|
||||
}}
|
||||
/>
|
||||
{/* ── Rendering Provider ─────────────────────────────── */}
|
||||
<div className="flex items-end gap-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<Label className="text-sm font-medium text-blue-800">Rendering Provider (NPI)</Label>
|
||||
<Select
|
||||
value={selectedNpiProviderId?.toString() ?? ""}
|
||||
onValueChange={(v) => setSelectedNpiProviderId(v ? Number(v) : null)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 bg-white">
|
||||
<SelectValue placeholder="Select NPI Provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{npiProviders.map((p) => (
|
||||
<SelectItem key={p.id} value={String(p.id)}>
|
||||
{p.npiNumber} — {p.providerName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mb-0.5"
|
||||
onClick={() => setNpiMutation.mutate(selectedNpiProviderId)}
|
||||
disabled={setNpiMutation.isPending || !procedures.length}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
Set for All
|
||||
</Button>
|
||||
{selectedProvider && (
|
||||
<span className="text-sm text-blue-700 mb-1 whitespace-nowrap">
|
||||
✓ {selectedProvider.providerName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ================= MANUAL ADD ================= */}
|
||||
{/* ── Combos ─────────────────────────────────────────── */}
|
||||
<div className="space-y-8 pointer-events-auto">
|
||||
<DirectComboButtons onDirectCombo={handleAddCombo} />
|
||||
<RegularComboButtons onRegularCombo={handleAddCombo} />
|
||||
</div>
|
||||
|
||||
{/* ── Manual Add ─────────────────────────────────────── */}
|
||||
<div className="mt-8 border rounded-lg p-4 bg-muted/20 space-y-3">
|
||||
<div className="font-medium text-sm">Add Manual Procedure</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||
<div>
|
||||
<Label>Code</Label>
|
||||
<Input
|
||||
value={manualCode}
|
||||
onChange={(e) => setManualCode(e.target.value)}
|
||||
placeholder="D0120"
|
||||
/>
|
||||
<Input value={manualCode} onChange={(e) => setManualCode(e.target.value)} placeholder="D0120" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Label</Label>
|
||||
<Input
|
||||
value={manualLabel}
|
||||
onChange={(e) => setManualLabel(e.target.value)}
|
||||
placeholder="Exam"
|
||||
/>
|
||||
<Input value={manualLabel} onChange={(e) => setManualLabel(e.target.value)} placeholder="Exam" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Fee</Label>
|
||||
<Input
|
||||
value={manualFee}
|
||||
onChange={(e) => setManualFee(e.target.value)}
|
||||
placeholder="100"
|
||||
type="number"
|
||||
/>
|
||||
<Input value={manualFee} onChange={(e) => setManualFee(e.target.value)} placeholder="100" type="number" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Tooth</Label>
|
||||
<Input
|
||||
value={manualTooth}
|
||||
onChange={(e) => setManualTooth(e.target.value)}
|
||||
placeholder="14"
|
||||
/>
|
||||
<Input value={manualTooth} onChange={(e) => setManualTooth(e.target.value)} placeholder="14" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Surface</Label>
|
||||
<Input
|
||||
value={manualSurface}
|
||||
onChange={(e) => setManualSurface(e.target.value)}
|
||||
placeholder="MO"
|
||||
/>
|
||||
<Input value={manualSurface} onChange={(e) => setManualSurface(e.target.value)} placeholder="MO" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => addManualMutation.mutate()}
|
||||
disabled={!manualCode || addManualMutation.isPending}
|
||||
>
|
||||
<Button size="sm" onClick={() => addManualMutation.mutate()} disabled={!manualCode || addManualMutation.isPending}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Procedure
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ================= LIST ================= */}
|
||||
{/* ── Procedures List ─────────────────────────────────── */}
|
||||
<div className="mt-8 space-y-2">
|
||||
<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)}
|
||||
>
|
||||
<div className="text-sm font-semibold">Saved Procedures ({procedures.length})</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">
|
||||
{/* ===== TABLE HEADER ===== */}
|
||||
<div className="grid grid-cols-[90px_1fr_90px_80px_80px_72px_72px] gap-2 px-3 py-2 text-xs font-semibold text-muted-foreground bg-muted/40">
|
||||
<div>Code</div>
|
||||
<div>Label</div>
|
||||
<div>Fee</div>
|
||||
<div>Tooth</div>
|
||||
<div>Surface</div>
|
||||
<div className="text-center">Edit</div>
|
||||
<div className="text-center">Delete</div>
|
||||
<div>Code</div><div>Label</div><div>Fee</div><div>Tooth</div><div>Surface</div>
|
||||
<div className="text-center">Edit</div><div className="text-center">Delete</div>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && <div className="p-4 text-sm text-muted-foreground">Loading...</div>}
|
||||
{!isLoading && procedures.length === 0 && (
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
No procedures added
|
||||
</div>
|
||||
<div className="p-4 text-sm text-muted-foreground">No procedures added yet</div>
|
||||
)}
|
||||
|
||||
{procedures.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="grid grid-cols-[90px_1fr_90px_80px_80px_72px_72px] gap-2 px-3 py-3 text-sm hover:bg-muted/40 transition"
|
||||
>
|
||||
<div key={p.id} className="grid grid-cols-[90px_1fr_90px_80px_80px_72px_72px] gap-2 px-3 py-3 text-sm hover:bg-muted/40 transition">
|
||||
{editingId === p.id ? (
|
||||
<>
|
||||
<Input
|
||||
className="w-[90px]"
|
||||
value={editRow.procedureCode ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditRow({
|
||||
...editRow,
|
||||
procedureCode: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="flex-1"
|
||||
value={editRow.procedureLabel ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditRow({
|
||||
...editRow,
|
||||
procedureLabel: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="w-[90px]"
|
||||
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 ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditRow({
|
||||
...editRow,
|
||||
toothNumber: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="w-[80px]"
|
||||
value={editRow.toothSurface ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditRow({
|
||||
...editRow,
|
||||
toothSurface: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input className="w-[90px]" value={editRow.procedureCode ?? ""} onChange={(e) => setEditRow({ ...editRow, procedureCode: e.target.value })} />
|
||||
<Input className="flex-1" value={editRow.procedureLabel ?? ""} onChange={(e) => setEditRow({ ...editRow, procedureLabel: e.target.value })} />
|
||||
<Input className="w-[90px]" 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 ?? ""} onChange={(e) => setEditRow({ ...editRow, toothNumber: e.target.value })} />
|
||||
<Input className="w-[80px]" value={editRow.toothSurface ?? ""} onChange={(e) => setEditRow({ ...editRow, toothSurface: e.target.value })} />
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => updateMutation.mutate()}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => updateMutation.mutate()}><Save className="h-4 w-4" /></Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button size="icon" variant="ghost" onClick={cancelEdit}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={cancelEdit}><X className="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-[90px] font-medium">
|
||||
{p.procedureCode}
|
||||
</div>
|
||||
<div className="flex-1 text-muted-foreground">
|
||||
{p.procedureLabel}
|
||||
</div>
|
||||
<div className="w-[90px]">
|
||||
{p.fee !== null && p.fee !== undefined
|
||||
? String(p.fee)
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
<div className="w-[90px] font-medium">{p.procedureCode}</div>
|
||||
<div className="flex-1 text-muted-foreground">{p.procedureLabel}</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>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => startEdit(p)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => startEdit(p)}>Edit</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => deleteMutation.mutate(p.id!)}
|
||||
>
|
||||
<Button size="icon" variant="ghost" onClick={() => deleteMutation.mutate(p.id!)}>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -530,7 +408,7 @@ export function AppointmentProceduresDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ================= FOOTER ================= */}
|
||||
{/* ── Footer ─────────────────────────────────────────── */}
|
||||
<div className="flex justify-between items-center gap-2 mt-8 pt-4 border-t">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -540,7 +418,6 @@ export function AppointmentProceduresDialog({
|
||||
>
|
||||
Direct Claim
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-blue-500 text-blue-600 hover:bg-blue-50"
|
||||
@@ -550,10 +427,7 @@ export function AppointmentProceduresDialog({
|
||||
Manual Claim
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>Close</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -561,10 +435,7 @@ export function AppointmentProceduresDialog({
|
||||
isOpen={clearAllOpen}
|
||||
entityName="all procedures for this appointment"
|
||||
onCancel={() => setClearAllOpen(false)}
|
||||
onConfirm={() => {
|
||||
setClearAllOpen(false);
|
||||
clearAllMutation.mutate();
|
||||
}}
|
||||
onConfirm={() => { setClearAllOpen(false); clearAllMutation.mutate(); }}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user