fix: update lab-management-tab.jsx and add lab-rx-modal.jsx
JSX files were missing the Lab RX feature — lab-management-tab.jsx still had the old lab order table UI, and lab-rx-modal.jsx didn't exist. The app on other PCs runs .jsx files, so git pull wasn't showing the new Lab RX feature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,258 +1,81 @@
|
||||
import { useState } from "react";
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog";
|
||||
import { Plus, Pencil, Trash2, Package } from "lucide-react";
|
||||
const STATUS_COLORS = {
|
||||
pending: "bg-yellow-100 text-yellow-700 border-yellow-200",
|
||||
"in-lab": "bg-blue-100 text-blue-700 border-blue-200",
|
||||
received: "bg-green-100 text-green-700 border-green-200",
|
||||
delivered: "bg-gray-100 text-gray-600 border-gray-200",
|
||||
cancelled: "bg-red-100 text-red-600 border-red-200",
|
||||
};
|
||||
const STATUS_LABELS = {
|
||||
pending: "Pending",
|
||||
"in-lab": "In Lab",
|
||||
received: "Received",
|
||||
delivered: "Delivered",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
const CASE_TYPES = [
|
||||
"Crown – PFM",
|
||||
"Crown – All Ceramic",
|
||||
"Crown – Zirconia",
|
||||
"Crown – Gold",
|
||||
"Bridge – PFM",
|
||||
"Bridge – Zirconia",
|
||||
"Implant Crown",
|
||||
"Implant Abutment",
|
||||
"Veneer",
|
||||
"Inlay / Onlay",
|
||||
"Full Denture (Upper)",
|
||||
"Full Denture (Lower)",
|
||||
"Partial Denture",
|
||||
"Night Guard",
|
||||
"Bleaching Tray",
|
||||
"Retainer",
|
||||
"Diagnostic Model",
|
||||
"Other",
|
||||
];
|
||||
const COMMON_LABS = [
|
||||
"Dental Arts Lab",
|
||||
"National Dentex",
|
||||
"Glidewell Dental",
|
||||
"Henry Schein Lab",
|
||||
"Affordable Dentures Lab",
|
||||
"Local Lab",
|
||||
];
|
||||
const SHADES = [
|
||||
"A1", "A2", "A3", "A3.5", "A4",
|
||||
"B1", "B2", "B3", "B4",
|
||||
"C1", "C2", "C3", "C4",
|
||||
"D2", "D3", "D4",
|
||||
"BL1", "BL2", "BL3", "BL4",
|
||||
"Custom",
|
||||
];
|
||||
let nextId = 1;
|
||||
const newOrder = () => ({
|
||||
id: nextId++,
|
||||
orderDate: new Date().toISOString().substring(0, 10),
|
||||
dueDate: "",
|
||||
tooth: "",
|
||||
caseType: "",
|
||||
lab: "",
|
||||
shade: "",
|
||||
status: "pending",
|
||||
rush: false,
|
||||
notes: "",
|
||||
trackingNumber: "",
|
||||
});
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { BarcodeModal } from "@/components/chart/barcode-modal";
|
||||
import { LabRxModal } from "@/components/chart/lab-rx-modal";
|
||||
import { Barcode, FileText } from "lucide-react";
|
||||
|
||||
export function LabManagementTab() {
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(newOrder());
|
||||
const openAdd = () => {
|
||||
setEditing(newOrder());
|
||||
setDialogOpen(true);
|
||||
};
|
||||
const openEdit = (order) => {
|
||||
setEditing({ ...order });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
const handleSave = () => {
|
||||
setOrders((prev) => {
|
||||
const idx = prev.findIndex((o) => o.id === editing.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = editing;
|
||||
return next;
|
||||
}
|
||||
return [...prev, editing];
|
||||
});
|
||||
setDialogOpen(false);
|
||||
};
|
||||
const handleDelete = (id) => {
|
||||
setOrders((prev) => prev.filter((o) => o.id !== id));
|
||||
};
|
||||
const pending = orders.filter((o) => o.status === "pending" || o.status === "in-lab").length;
|
||||
const rush = orders.filter((o) => o.rush && o.status !== "delivered" && o.status !== "cancelled").length;
|
||||
return (<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-4 text-sm text-gray-600">
|
||||
{pending > 0 && <span>Open orders: <strong>{pending}</strong></span>}
|
||||
{rush > 0 && <span className="text-red-600">Rush: <strong>{rush}</strong></span>}
|
||||
{orders.length === 0 && <span>No lab orders yet</span>}
|
||||
</div>
|
||||
<Button size="sm" onClick={openAdd} className="gap-1.5">
|
||||
<Plus className="h-4 w-4"/>
|
||||
New Lab Order
|
||||
</Button>
|
||||
</div>
|
||||
const [selectedPatient, setSelectedPatient] = useState(null);
|
||||
const [barcodeOpen, setBarcodeOpen] = useState(false);
|
||||
const [labRxOpen, setLabRxOpen] = useState(false);
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="w-24">Order Date</TableHead>
|
||||
<TableHead className="w-16">Tooth</TableHead>
|
||||
<TableHead>Case Type</TableHead>
|
||||
<TableHead>Lab</TableHead>
|
||||
<TableHead className="w-16 text-center">Shade</TableHead>
|
||||
<TableHead className="w-24">Due</TableHead>
|
||||
<TableHead className="w-28">Status</TableHead>
|
||||
<TableHead className="w-20 text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{orders.length === 0 ? (<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-gray-400 py-10">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Package className="h-8 w-8 text-gray-300"/>
|
||||
<span>No lab orders yet. Click "New Lab Order" to add one.</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>) : (orders.map((order) => (<TableRow key={order.id} className={order.rush ? "bg-red-50/40" : ""}>
|
||||
<TableCell className="text-sm">{order.orderDate}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{order.tooth || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium">{order.caseType}</span>
|
||||
{order.rush && (<Badge className="text-[10px] bg-red-100 text-red-600 border-red-200 px-1 py-0" variant="outline">
|
||||
RUSH
|
||||
</Badge>)}
|
||||
</div>
|
||||
{order.notes && (<p className="text-xs text-gray-400 truncate max-w-xs mt-0.5">{order.notes}</p>)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{order.lab || "—"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{order.shade || "—"}</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{order.dueDate || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`text-xs border ${STATUS_COLORS[order.status]}`} variant="outline">
|
||||
{STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEdit(order)}>
|
||||
<Pencil className="h-3.5 w-3.5"/>
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lab Actions</CardTitle>
|
||||
<CardDescription>
|
||||
{selectedPatient
|
||||
? `Selected patient: ${selectedPatient.firstName} ${selectedPatient.lastName}`
|
||||
: "Select a patient below to get started."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!selectedPatient}
|
||||
onClick={() => setBarcodeOpen(true)}
|
||||
>
|
||||
<Barcode className="h-4 w-4" />
|
||||
Create a Barcode
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50" onClick={() => handleDelete(order.id)}>
|
||||
<Trash2 className="h-3.5 w-3.5"/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={!selectedPatient}
|
||||
onClick={() => setLabRxOpen(true)}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Lab RX
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lab Order</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 py-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Order Date</Label>
|
||||
<Input type="date" value={editing.orderDate} onChange={(e) => setEditing((d) => ({ ...d, orderDate: e.target.value }))} className="h-9 text-sm"/>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>Select a patient by clicking the checkbox.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowView={true}
|
||||
allowDelete={false}
|
||||
allowCheckbox={true}
|
||||
allowEdit={false}
|
||||
onSelectPatient={setSelectedPatient}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{selectedPatient && (
|
||||
<>
|
||||
<BarcodeModal
|
||||
open={barcodeOpen}
|
||||
onClose={() => setBarcodeOpen(false)}
|
||||
patient={selectedPatient}
|
||||
/>
|
||||
<LabRxModal
|
||||
open={labRxOpen}
|
||||
onClose={() => setLabRxOpen(false)}
|
||||
patient={selectedPatient}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Due Date</Label>
|
||||
<Input type="date" value={editing.dueDate} onChange={(e) => setEditing((d) => ({ ...d, dueDate: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Tooth #</Label>
|
||||
<Input placeholder="e.g. 14" value={editing.tooth} onChange={(e) => setEditing((d) => ({ ...d, tooth: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Shade</Label>
|
||||
<Select value={editing.shade} onValueChange={(v) => setEditing((d) => ({ ...d, shade: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select..."/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SHADES.map((s) => <SelectItem key={s} value={s}>{s}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Case Type</Label>
|
||||
<Select value={editing.caseType} onValueChange={(v) => setEditing((d) => ({ ...d, caseType: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select case type..."/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CASE_TYPES.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Lab</Label>
|
||||
<Select value={editing.lab} onValueChange={(v) => setEditing((d) => ({ ...d, lab: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select lab..."/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_LABS.map((l) => <SelectItem key={l} value={l}>{l}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Status</Label>
|
||||
<Select value={editing.status} onValueChange={(v) => setEditing((d) => ({ ...d, status: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(STATUS_LABELS).map((s) => (<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Tracking #</Label>
|
||||
<Input placeholder="Optional" value={editing.trackingNumber} onChange={(e) => setEditing((d) => ({ ...d, trackingNumber: e.target.value }))} className="h-9 text-sm"/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<input type="checkbox" id="rush" checked={editing.rush} onChange={(e) => setEditing((d) => ({ ...d, rush: e.target.checked }))} className="h-4 w-4 rounded border-gray-300"/>
|
||||
<Label htmlFor="rush" className="text-sm cursor-pointer text-red-600 font-medium">
|
||||
Rush Order
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Notes</Label>
|
||||
<Textarea placeholder="Special instructions, shade details..." value={editing.notes} onChange={(e) => setEditing((d) => ({ ...d, notes: e.target.value }))} className="text-sm resize-none" rows={2}/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSave} disabled={!editing.caseType}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
|
||||
596
apps/Frontend/src/components/chart/lab-rx-modal.jsx
Normal file
596
apps/Frontend/src/components/chart/lab-rx-modal.jsx
Normal file
@@ -0,0 +1,596 @@
|
||||
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 {
|
||||
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";
|
||||
|
||||
const EMPTY_TEMPLATE = {
|
||||
name: "", labName: "", labPhone: "", labFax: "",
|
||||
labAddress: "", labAccount: "", caseType: "", material: "", instructions: "Please make\nThank you!",
|
||||
};
|
||||
const EMPTY_RX = { toothNumbers: "", shade: "", dueDate: "", doctorName: "", additionalNotes: "Please make\nThank you!" };
|
||||
|
||||
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"];
|
||||
|
||||
export function LabRxModal({ open, onClose, patient }) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const printRef = useRef(null);
|
||||
|
||||
const [rxTemplate, setRxTemplate] = useState(null);
|
||||
const [rx, setRx] = useState(EMPTY_RX);
|
||||
|
||||
const [renamingId, setRenamingId] = useState(null);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
|
||||
const [showTemplateForm, setShowTemplateForm] = useState(false);
|
||||
const [editingTemplateId, setEditingTemplateId] = useState(null);
|
||||
const [templateForm, setTemplateForm] = useState(EMPTY_TEMPLATE);
|
||||
|
||||
const { data: providers = [] } = useQuery({
|
||||
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: 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) => {
|
||||
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) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async ({ id, data }) => {
|
||||
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) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: async ({ id, name }) => {
|
||||
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) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id) => {
|
||||
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) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const reorderMutation = useMutation({
|
||||
mutationFn: async (orderedIds) => {
|
||||
const res = await apiRequest("POST", "/api/lab-rx/templates/reorder", { orderedIds });
|
||||
if (!res.ok) throw new Error("Failed to reorder");
|
||||
},
|
||||
onSuccess: () => invalidate(),
|
||||
onError: (e) => toast({ title: "Error", description: e.message, variant: "destructive" }),
|
||||
});
|
||||
|
||||
const moveTemplate = (index, direction) => {
|
||||
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) => {
|
||||
if (renamingId !== null) return;
|
||||
setRxTemplate(t);
|
||||
setRx({ ...EMPTY_RX, doctorName: providers[0]?.providerName ?? "" });
|
||||
};
|
||||
|
||||
const startRename = (e, t) => {
|
||||
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) => {
|
||||
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(`<!DOCTYPE html><html><head><title>Lab RX</title>
|
||||
<style>
|
||||
body{font-family:Arial,sans-serif;margin:0;padding:24px;font-size:13px;color:#111}
|
||||
h1{font-size:22px;margin:0 0 4px;text-align:center}
|
||||
.subtitle{text-align:center;color:#555;font-size:12px;margin-bottom:12px}
|
||||
hr{border:none;border-top:2px solid #000;margin:10px 0}
|
||||
.section{margin-bottom:14px}
|
||||
.section-title{font-weight:bold;font-size:11px;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #bbb;margin-bottom:8px;padding-bottom:3px;color:#333}
|
||||
.row{display:flex;gap:24px;margin-bottom:6px}
|
||||
.field{flex:1}
|
||||
.field label{display:block;font-size:10px;color:#777;margin-bottom:1px}
|
||||
.field span{display:block;border-bottom:1px solid #aaa;padding-bottom:2px;min-height:18px;font-size:13px}
|
||||
.instructions-box{border:1px solid #bbb;padding:8px;min-height:64px;white-space:pre-wrap;font-size:12px}
|
||||
.sig-area{display:flex;gap:48px;margin-top:32px}
|
||||
.sig-line{border-top:1px solid #000;padding-top:3px;font-size:10px;color:#777;min-width:180px}
|
||||
@media print{body{margin:0}}
|
||||
</style>
|
||||
</head><body>${content.innerHTML}</body></html>`);
|
||||
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 */}
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FlaskConical className="h-5 w-5" />
|
||||
Lab RX — {patientName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="rx">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="rx">Create Lab RX</TabsTrigger>
|
||||
<TabsTrigger value="templates">Manage Templates</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab 1: pick template */}
|
||||
<TabsContent value="rx" className="space-y-4">
|
||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Click a template to create a Lab RX
|
||||
</p>
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-gray-400">Loading templates…</p>
|
||||
) : templates.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No templates yet. Go to "Manage Templates" to add one.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{templates.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
onClick={() => 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 ? (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") commitRename(); if (e.key === "Escape") cancelRename(); }}
|
||||
className="h-7 text-sm font-medium"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-green-600" onClick={commitRename} disabled={renameMutation.isPending}>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0" onClick={cancelRename}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-sm text-gray-900 truncate">{t.name}</p>
|
||||
{t.labName && <p className="text-xs text-gray-500 mt-0.5">{t.labName}</p>}
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 mt-1 text-xs text-gray-400">
|
||||
{t.caseType && <span>{t.caseType}</span>}
|
||||
{t.material && <span>{t.material}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
title="Rename template"
|
||||
onClick={(e) => startRename(e, t)}
|
||||
className="shrink-0 p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-gray-100 transition-opacity"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab 2: manage templates */}
|
||||
<TabsContent value="templates" className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">{templates.length} template{templates.length !== 1 ? "s" : ""}</p>
|
||||
<Button size="sm" onClick={openAddTemplate} className="gap-1.5">
|
||||
<Plus className="h-4 w-4" /> Add Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showTemplateForm && (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-100 border-b px-4 py-2.5">
|
||||
<p className="text-sm font-semibold text-gray-800">
|
||||
{editingTemplateId !== null ? "Edit Template" : "New Template"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 space-y-5 bg-white">
|
||||
|
||||
{/* Template Name section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-600">Template Name</p>
|
||||
<div className="flex-1 h-px bg-blue-100" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Name <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
placeholder="e.g. Glidewell Crown, Argen Bridge…"
|
||||
value={templateForm.name}
|
||||
onChange={(e) => setTemplateForm((f) => ({ ...f, name: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-400">This name appears on the template picker button.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lab Information section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Lab Information</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Lab Name</Label>
|
||||
<Input placeholder="e.g. Glidewell Dental" value={templateForm.labName} onChange={(e) => setTemplateForm((f) => ({ ...f, labName: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Account #</Label>
|
||||
<Input placeholder="e.g. 123456" value={templateForm.labAccount} onChange={(e) => setTemplateForm((f) => ({ ...f, labAccount: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Phone</Label>
|
||||
<Input placeholder="(800) 000-0000" value={templateForm.labPhone} onChange={(e) => setTemplateForm((f) => ({ ...f, labPhone: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Fax</Label>
|
||||
<Input placeholder="(800) 000-0001" value={templateForm.labFax} onChange={(e) => setTemplateForm((f) => ({ ...f, labFax: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Address</Label>
|
||||
<Input placeholder="123 Lab St, City, State 00000" value={templateForm.labAddress} onChange={(e) => setTemplateForm((f) => ({ ...f, labAddress: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default Case Details section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Default Case Details</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Case Type</Label>
|
||||
<Select value={templateForm.caseType} onValueChange={(v) => setTemplateForm((f) => ({ ...f, caseType: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select…" /></SelectTrigger>
|
||||
<SelectContent>{CASE_TYPES.map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Material</Label>
|
||||
<Select value={templateForm.material} onValueChange={(v) => setTemplateForm((f) => ({ ...f, material: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select…" /></SelectTrigger>
|
||||
<SelectContent>{MATERIALS.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs text-gray-600">Instructions</Label>
|
||||
<Textarea placeholder="Standard instructions for this lab…" value={templateForm.instructions} onChange={(e) => setTemplateForm((f) => ({ ...f, instructions: e.target.value }))} className="text-sm resize-none" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-t px-4 py-3 flex gap-2">
|
||||
<Button size="sm" onClick={handleSaveTemplate} disabled={isSaving}>
|
||||
<Check className="h-3.5 w-3.5 mr-1" />{isSaving ? "Saving…" : "Save Template"}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => { setShowTemplateForm(false); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE); }} disabled={isSaving}>
|
||||
<X className="h-3.5 w-3.5 mr-1" /> Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-gray-400 py-4 text-center">Loading…</p>
|
||||
) : templates.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 py-8 text-center">No templates yet. Click "Add Template" to create one.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{templates.map((t, index) => (
|
||||
<div key={t.id} className="border rounded-lg p-3 bg-white flex items-center gap-2">
|
||||
<div className="flex flex-col shrink-0">
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-700 disabled:opacity-20"
|
||||
disabled={index === 0 || reorderMutation.isPending}
|
||||
onClick={() => moveTemplate(index, "up")}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon" className="h-6 w-6 text-gray-400 hover:text-gray-700 disabled:opacity-20"
|
||||
disabled={index === templates.length - 1 || reorderMutation.isPending}
|
||||
onClick={() => moveTemplate(index, "down")}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm text-gray-900">{t.name}</p>
|
||||
{t.labName && <p className="text-xs text-gray-500">{t.labName}</p>}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-0.5 text-xs text-gray-400 mt-0.5">
|
||||
{t.caseType && <span>Case: {t.caseType}</span>}
|
||||
{t.material && <span>Material: {t.material}</span>}
|
||||
{t.labPhone && <span>Ph: {t.labPhone}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEditTemplate(t)}>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-500" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => deleteMutation.mutate(t.id)} disabled={deleteMutation.isPending}>
|
||||
<Trash2 className="h-3.5 w-3.5 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* RX popup */}
|
||||
<Dialog open={!!rxTemplate} onOpenChange={(v) => { if (!v) setRxTemplate(null); }}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Printer className="h-5 w-5" />
|
||||
Lab RX — {rxTemplate?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{rxTemplate && (
|
||||
<>
|
||||
<div className="space-y-5 py-2">
|
||||
|
||||
{/* Tooth Number section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-600">Tooth Number</p>
|
||||
<div className="flex-1 h-px bg-blue-100" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Tooth Number(s)</Label>
|
||||
<Input placeholder="e.g. 3, 14, 19" value={rx.toothNumbers} onChange={(e) => setRx((r) => ({ ...r, toothNumbers: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Shade</Label>
|
||||
<Input placeholder="e.g. A2, B1" value={rx.shade} onChange={(e) => setRx((r) => ({ ...r, shade: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Due Date</Label>
|
||||
<Input type="date" value={rx.dueDate} onChange={(e) => setRx((r) => ({ ...r, dueDate: e.target.value }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Doctor's Name</Label>
|
||||
<Select value={rx.doctorName} onValueChange={(v) => setRx((r) => ({ ...r, doctorName: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select provider…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers.map((p) => (
|
||||
<SelectItem key={p.id} value={p.providerName}>{p.providerName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">Instructions</p>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
{rxTemplate.instructions && (
|
||||
<div className="rounded-md bg-gray-50 border px-3 py-2 text-xs text-gray-600 whitespace-pre-wrap">
|
||||
<span className="font-medium text-gray-400 block mb-1">From template</span>
|
||||
{rxTemplate.instructions}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-600">Additional Notes</Label>
|
||||
<Textarea placeholder="Any extra instructions for this case…" value={rx.additionalNotes} onChange={(e) => setRx((r) => ({ ...r, additionalNotes: e.target.value }))} className="text-sm resize-none" rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RX preview */}
|
||||
<div className="border rounded-lg p-4 bg-gray-50 text-sm space-y-3">
|
||||
<div className="text-center border-b pb-3">
|
||||
<p className="font-bold text-base">Dental Lab Prescription</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Date: {today}{rx.dueDate ? ` · Due: ${new Date(rx.dueDate + "T00:00").toLocaleDateString("en-US")}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1">
|
||||
<div><span className="text-gray-400 text-xs">Patient</span><p className="font-medium">{patientName}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">DOB</span><p>{patient.dateOfBirth ? new Date(patient.dateOfBirth).toLocaleDateString("en-US") : "—"}</p></div>
|
||||
<div className="col-span-2 mt-1 border-t pt-2"><span className="text-gray-400 text-xs">Lab</span><p className="font-medium">{rxTemplate.labName || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Phone</span><p>{rxTemplate.labPhone || "—"}</p></div>
|
||||
{rxTemplate.labFax && <div><span className="text-gray-400 text-xs">Fax</span><p>{rxTemplate.labFax}</p></div>}
|
||||
{rxTemplate.labAddress && <div className="col-span-2"><span className="text-gray-400 text-xs">Address</span><p>{rxTemplate.labAddress}</p></div>}
|
||||
{rxTemplate.labAccount && <div><span className="text-gray-400 text-xs">Account #</span><p>{rxTemplate.labAccount}</p></div>}
|
||||
<div className="col-span-2 mt-1 border-t pt-2 grid grid-cols-2 gap-x-6">
|
||||
<div><span className="text-gray-400 text-xs">Case Type</span><p>{rxTemplate.caseType || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Material</span><p>{rxTemplate.material || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Tooth #</span><p>{rx.toothNumbers || "—"}</p></div>
|
||||
<div><span className="text-gray-400 text-xs">Shade</span><p>{rx.shade || "—"}</p></div>
|
||||
{rx.doctorName && <div><span className="text-gray-400 text-xs">Doctor</span><p>{rx.doctorName}</p></div>}
|
||||
</div>
|
||||
{(rxTemplate.instructions || rx.additionalNotes) && (
|
||||
<div className="col-span-2 mt-1 border-t pt-2">
|
||||
<span className="text-gray-400 text-xs">Instructions</span>
|
||||
<p className="whitespace-pre-wrap text-xs mt-1 text-gray-700">
|
||||
{[rxTemplate.instructions, rx.additionalNotes].filter(Boolean).join("\n\n")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Hidden print DOM */}
|
||||
<div className="hidden">
|
||||
<div ref={printRef}>
|
||||
<h1>Dental Lab Prescription</h1>
|
||||
<div className="subtitle">
|
||||
Date: {today}{rx.dueDate ? ` · Due: ${new Date(rx.dueDate + "T00:00").toLocaleDateString("en-US")}` : ""}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="section">
|
||||
<div className="section-title">Patient Information</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Patient Name</label><span>{patientName}</span></div>
|
||||
<div className="field"><label>Date of Birth</label><span>{patient.dateOfBirth ? new Date(patient.dateOfBirth).toLocaleDateString("en-US") : ""}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Dental Lab</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Lab Name</label><span>{rxTemplate.labName ?? ""}</span></div>
|
||||
<div className="field"><label>Account #</label><span>{rxTemplate.labAccount ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Phone</label><span>{rxTemplate.labPhone ?? ""}</span></div>
|
||||
<div className="field"><label>Fax</label><span>{rxTemplate.labFax ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Address</label><span>{rxTemplate.labAddress ?? ""}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Prescription Details</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Case Type</label><span>{rxTemplate.caseType ?? ""}</span></div>
|
||||
<div className="field"><label>Material</label><span>{rxTemplate.material ?? ""}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Tooth Number(s)</label><span>{rx.toothNumbers}</span></div>
|
||||
<div className="field"><label>Shade</label><span>{rx.shade}</span></div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field"><label>Doctor's Name</label><span>{rx.doctorName}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="section-title">Instructions</div>
|
||||
<div className="instructions-box">
|
||||
{[rxTemplate.instructions, rx.additionalNotes].filter(Boolean).join("\n\n")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sig-area">
|
||||
<div className="sig-line">Doctor Signature</div>
|
||||
<div className="sig-line">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRxTemplate(null)}>Close</Button>
|
||||
<Button onClick={handlePrint} className="gap-2">
|
||||
<Printer className="h-4 w-4" /> Print Lab RX
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user