import { useState, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { Patient } from "@repo/db/types"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Plus, Pencil, Trash2, Printer, X, Check, FlaskConical, ChevronUp, ChevronDown } from "lucide-react"; type LabRxTemplate = { id: number; name: string; labName?: string | null; labPhone?: string | null; labFax?: string | null; labAddress?: string | null; labAccount?: string | null; caseType?: string | null; material?: string | null; instructions?: string | null; sortOrder: number; }; type TemplateFormState = { name: string; labName: string; labPhone: string; labFax: string; labAddress: string; labAccount: string; caseType: string; material: string; instructions: string; }; type RxFields = { toothNumbers: string; shade: string; dueDate: string; doctorName: string; age: string; officeName: string; additionalNotes: string; }; const EMPTY_TEMPLATE: TemplateFormState = { name: "", labName: "", labPhone: "", labFax: "", labAddress: "", labAccount: "", caseType: "", material: "", instructions: "Please make\nThank you!", }; const EMPTY_RX: RxFields = { toothNumbers: "", shade: "", dueDate: "", doctorName: "", age: "", officeName: "", additionalNotes: "Please make\nThank you!" }; const calcAge = (dob: string | Date | null | undefined): string => { if (!dob) return ""; const birth = new Date(dob); const today = new Date(); let age = today.getFullYear() - birth.getFullYear(); const m = today.getMonth() - birth.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--; return String(age); }; const CASE_TYPES = ["Crown", "Bridge", "Veneer", "Inlay/Onlay", "Full Denture", "Partial Denture", "Night Guard", "Retainer", "Implant Crown", "Other"]; const MATERIALS = ["Zirconia", "PFM (Porcelain-Fused-to-Metal)", "E-Max", "Acrylic", "Cast Metal", "Composite", "PMMA", "Other"]; interface Props { open: boolean; onClose: () => void; patient: Patient; } export function LabRxModal({ open, onClose, patient }: Props) { const { toast } = useToast(); const queryClient = useQueryClient(); const printRef = useRef(null); // RX popup const [rxTemplate, setRxTemplate] = useState(null); const [rx, setRx] = useState(EMPTY_RX); // Inline rename (Tab 1 cards) const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(""); // Manage Templates form const [showTemplateForm, setShowTemplateForm] = useState(false); const [editingTemplateId, setEditingTemplateId] = useState(null); const [templateForm, setTemplateForm] = useState(EMPTY_TEMPLATE); const { data: providers = [] } = useQuery<{ id: number; providerName: string }[]>({ queryKey: ["/api/npiProviders"], queryFn: async () => { const res = await apiRequest("GET", "/api/npiProviders"); if (!res.ok) throw new Error("Failed to fetch providers"); return res.json(); }, enabled: open, }); const { data: officeContact } = useQuery<{ officeName?: string } | null>({ queryKey: ["/api/office-contact"], queryFn: async () => { const res = await apiRequest("GET", "/api/office-contact"); if (!res.ok) return null; return res.json(); }, enabled: open, }); const { data: templates = [], isLoading } = useQuery({ queryKey: ["/api/lab-rx/templates"], queryFn: async () => { const res = await apiRequest("GET", "/api/lab-rx/templates"); if (!res.ok) throw new Error("Failed to fetch"); return res.json(); }, enabled: open, }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ["/api/lab-rx/templates"] }); const createMutation = useMutation({ mutationFn: async (data: TemplateFormState) => { const res = await apiRequest("POST", "/api/lab-rx/templates", data); if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to save"); } return res.json(); }, onSuccess: () => { invalidate(); setShowTemplateForm(false); setTemplateForm(EMPTY_TEMPLATE); toast({ title: "Template saved" }); }, onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }), }); const updateMutation = useMutation({ mutationFn: async ({ id, data }: { id: number; data: Partial }) => { const res = await apiRequest("PUT", `/api/lab-rx/templates/${id}`, data); if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to update"); } return res.json(); }, onSuccess: () => { invalidate(); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); setShowTemplateForm(false); toast({ title: "Template updated" }); }, onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }), }); const renameMutation = useMutation({ mutationFn: async ({ id, name }: { id: number; name: string }) => { const res = await apiRequest("PUT", `/api/lab-rx/templates/${id}`, { name }); if (!res.ok) { const e = await res.json().catch(() => null); throw new Error(e?.message || "Failed to rename"); } return res.json(); }, onSuccess: () => { invalidate(); setRenamingId(null); }, onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }), }); const deleteMutation = useMutation({ mutationFn: async (id: number) => { const res = await apiRequest("DELETE", `/api/lab-rx/templates/${id}`); if (!res.ok) throw new Error("Failed to delete"); }, onSuccess: () => { invalidate(); toast({ title: "Template deleted" }); }, onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }), }); const reorderMutation = useMutation({ mutationFn: async (orderedIds: number[]) => { const res = await apiRequest("POST", "/api/lab-rx/templates/reorder", { orderedIds }); if (!res.ok) throw new Error("Failed to reorder"); }, onSuccess: () => invalidate(), onError: (e: any) => toast({ title: "Error", description: e.message, variant: "destructive" }), }); const moveTemplate = (index: number, direction: "up" | "down") => { const swapIndex = direction === "up" ? index - 1 : index + 1; const reordered = [...templates]; const a = reordered[index]!; const b = reordered[swapIndex]!; reordered[index] = b; reordered[swapIndex] = a; queryClient.setQueryData(["/api/lab-rx/templates"], reordered); reorderMutation.mutate(reordered.map((t) => t.id)); }; const openRx = (t: LabRxTemplate) => { if (renamingId !== null) return; setRxTemplate(t); setRx({ ...EMPTY_RX, doctorName: providers[0]?.providerName ?? "", age: calcAge(patient.dateOfBirth), officeName: officeContact?.officeName ?? "" }); }; const startRename = (e: React.MouseEvent, t: LabRxTemplate) => { e.stopPropagation(); setRenamingId(t.id); setRenameValue(t.name); }; const commitRename = () => { if (!renameValue.trim()) { toast({ title: "Name cannot be empty", variant: "destructive" }); return; } if (renamingId !== null) renameMutation.mutate({ id: renamingId, name: renameValue.trim() }); }; const cancelRename = () => setRenamingId(null); const openAddTemplate = () => { setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); setShowTemplateForm(true); }; const openEditTemplate = (t: LabRxTemplate) => { setEditingTemplateId(t.id); setTemplateForm({ name: t.name, labName: t.labName ?? "", labPhone: t.labPhone ?? "", labFax: t.labFax ?? "", labAddress: t.labAddress ?? "", labAccount: t.labAccount ?? "", caseType: t.caseType ?? "", material: t.material ?? "", instructions: t.instructions ?? "" }); setShowTemplateForm(true); }; const handleSaveTemplate = () => { if (!templateForm.name.trim()) { toast({ title: "Template name is required", variant: "destructive" }); return; } if (editingTemplateId !== null) updateMutation.mutate({ id: editingTemplateId, data: templateForm }); else createMutation.mutate(templateForm); }; const isSaving = createMutation.isPending || updateMutation.isPending; const handlePrint = () => { const content = printRef.current; if (!content || !rxTemplate) return; const win = window.open("", "_blank", "width=800,height=900"); if (!win) return; win.document.write(`Lab RX ${content.innerHTML}`); win.document.close(); win.focus(); setTimeout(() => { win.print(); win.close(); }, 300); }; const patientName = `${patient.firstName} ${patient.lastName}`; const today = new Date().toLocaleDateString("en-US"); return ( <> {/* ── Main modal ── */} !v && onClose()}> Lab RX — {patientName} Create Lab RX Manage Templates {/* ── Tab 1: pick template ── */}

Click a template to create a Lab RX

{isLoading ? (

Loading templates…

) : templates.length === 0 ? (

No templates yet. Go to "Manage Templates" to add one.

) : (
{templates.map((t) => (
openRx(t)} className="group relative border rounded-lg p-3 bg-white cursor-pointer hover:border-blue-400 hover:shadow-sm transition-all" > {renamingId === t.id ? (
e.stopPropagation()}> setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") commitRename(); if (e.key === "Escape") cancelRename(); }} className="h-7 text-sm font-medium" />
) : (

{t.name}

{t.labName &&

{t.labName}

}
{t.caseType && {t.caseType}} {t.material && {t.material}}
)}
))}
)}
{/* ── Tab 2: manage templates ── */}

{templates.length} template{templates.length !== 1 ? "s" : ""}

{showTemplateForm && (

{editingTemplateId !== null ? "Edit Template" : "New Template"}

{/* Template Name section */}

Template Name

setTemplateForm((f) => ({ ...f, name: e.target.value }))} className="h-9 text-sm" />

This name appears on the template picker button.

{/* Lab Information section */}

Lab Information

setTemplateForm((f) => ({ ...f, labName: e.target.value }))} className="h-9 text-sm" />
setTemplateForm((f) => ({ ...f, labAccount: e.target.value }))} className="h-9 text-sm" />
setTemplateForm((f) => ({ ...f, labPhone: e.target.value }))} className="h-9 text-sm" />
setTemplateForm((f) => ({ ...f, labFax: e.target.value }))} className="h-9 text-sm" />
setTemplateForm((f) => ({ ...f, labAddress: e.target.value }))} className="h-9 text-sm" />
{/* Default Case Details section */}

Default Case Details