feat: add Chart section, colorize sidebar icons, rename nav items, patient action buttons, program bridge
This commit is contained in:
336
apps/Frontend/src/components/settings/program-bridge-table.tsx
Normal file
336
apps/Frontend/src/components/settings/program-bridge-table.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Plus, Pencil, Trash2, Link2 } from "lucide-react";
|
||||
|
||||
type BridgeCategory =
|
||||
| "imaging"
|
||||
| "xray"
|
||||
| "perio"
|
||||
| "lab"
|
||||
| "prescription"
|
||||
| "billing"
|
||||
| "other";
|
||||
|
||||
interface ProgramBridge {
|
||||
id: number;
|
||||
name: string;
|
||||
category: BridgeCategory;
|
||||
executablePath: string;
|
||||
arguments: string;
|
||||
enabled: boolean;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<BridgeCategory, string> = {
|
||||
imaging: "3D / CBCT Imaging",
|
||||
xray: "Digital X-Ray",
|
||||
perio: "Perio Charting",
|
||||
lab: "Lab Software",
|
||||
prescription: "e-Prescriptions",
|
||||
billing: "Billing / Clearinghouse",
|
||||
other: "Other",
|
||||
};
|
||||
|
||||
const CATEGORY_COLORS: Record<BridgeCategory, string> = {
|
||||
imaging: "bg-purple-100 text-purple-700 border-purple-200",
|
||||
xray: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
perio: "bg-green-100 text-green-700 border-green-200",
|
||||
lab: "bg-orange-100 text-orange-700 border-orange-200",
|
||||
prescription: "bg-rose-100 text-rose-700 border-rose-200",
|
||||
billing: "bg-amber-100 text-amber-700 border-amber-200",
|
||||
other: "bg-gray-100 text-gray-600 border-gray-200",
|
||||
};
|
||||
|
||||
const COMMON_PROGRAMS = [
|
||||
"DEXIS",
|
||||
"Sidexis",
|
||||
"Schick CDR",
|
||||
"Apteryx XVWeb",
|
||||
"Carestream CS Imaging",
|
||||
"Dolphin Imaging",
|
||||
"i-CAT Vision",
|
||||
"Romexis",
|
||||
"EagleSoft",
|
||||
"Dentrix",
|
||||
"Eaglesoft Perio",
|
||||
"Florida Probe",
|
||||
"PerioVision",
|
||||
"DrFirst Rcopia",
|
||||
"NewCrop eRx",
|
||||
"DentalConnect",
|
||||
"Custom",
|
||||
];
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
const newBridge = (): ProgramBridge => ({
|
||||
id: nextId++,
|
||||
name: "",
|
||||
category: "other",
|
||||
executablePath: "",
|
||||
arguments: "",
|
||||
enabled: true,
|
||||
notes: "",
|
||||
});
|
||||
|
||||
export function ProgramBridgeTable() {
|
||||
const [bridges, setBridges] = useState<ProgramBridge[]>([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<ProgramBridge>(newBridge());
|
||||
|
||||
const openAdd = () => {
|
||||
setEditing(newBridge());
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (bridge: ProgramBridge) => {
|
||||
setEditing({ ...bridge });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setBridges((prev) => {
|
||||
const idx = prev.findIndex((b) => b.id === editing.id);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = editing;
|
||||
return next;
|
||||
}
|
||||
return [...prev, editing];
|
||||
});
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setBridges((prev) => prev.filter((b) => b.id !== id));
|
||||
};
|
||||
|
||||
const toggleEnabled = (id: number) => {
|
||||
setBridges((prev) =>
|
||||
prev.map((b) => (b.id === id ? { ...b, enabled: !b.enabled } : b))
|
||||
);
|
||||
};
|
||||
|
||||
const enabledCount = bridges.filter((b) => b.enabled).length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3 pt-4 px-4 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4 text-indigo-500" />
|
||||
<CardTitle className="text-base font-semibold">Program Bridge</CardTitle>
|
||||
{bridges.length > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{enabledCount} / {bridges.length} active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button size="sm" onClick={openAdd} className="gap-1.5">
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Bridge
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4">
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="w-10 text-center">On</TableHead>
|
||||
<TableHead>Program</TableHead>
|
||||
<TableHead className="w-36">Category</TableHead>
|
||||
<TableHead>Executable Path</TableHead>
|
||||
<TableHead>Arguments</TableHead>
|
||||
<TableHead className="w-20 text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bridges.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-gray-400 py-10">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Link2 className="h-8 w-8 text-gray-300" />
|
||||
<span>No program bridges configured. Click "Add Bridge" to connect external software.</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
bridges.map((bridge) => (
|
||||
<TableRow key={bridge.id} className={!bridge.enabled ? "opacity-50" : ""}>
|
||||
<TableCell className="text-center">
|
||||
<Switch
|
||||
checked={bridge.enabled}
|
||||
onCheckedChange={() => toggleEnabled(bridge.id)}
|
||||
className="scale-75"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{bridge.name}</p>
|
||||
{bridge.notes && (
|
||||
<p className="text-xs text-gray-400 truncate max-w-xs">{bridge.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={`text-xs border ${CATEGORY_COLORS[bridge.category]}`}
|
||||
variant="outline"
|
||||
>
|
||||
{CATEGORY_LABELS[bridge.category]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600 font-mono truncate max-w-[200px]">
|
||||
{bridge.executablePath || "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500 font-mono truncate max-w-[120px]">
|
||||
{bridge.arguments || "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => openEdit(bridge)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handleDelete(bridge.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Program Bridge</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 py-2">
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Program Name</Label>
|
||||
<Select
|
||||
value={editing.name}
|
||||
onValueChange={(v) => setEditing((d) => ({ ...d, name: v }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select or type program name..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMMON_PROGRAMS.map((p) => (
|
||||
<SelectItem key={p} value={p}>{p}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{editing.name === "Custom" && (
|
||||
<Input
|
||||
className="h-9 text-sm mt-1"
|
||||
placeholder="Enter custom program name"
|
||||
value={editing.name === "Custom" ? "" : editing.name}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, name: e.target.value }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Category</Label>
|
||||
<Select
|
||||
value={editing.category}
|
||||
onValueChange={(v) => setEditing((d) => ({ ...d, category: v as BridgeCategory }))}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(CATEGORY_LABELS) as BridgeCategory[]).map((c) => (
|
||||
<SelectItem key={c} value={c}>{CATEGORY_LABELS[c]}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Executable Path</Label>
|
||||
<Input
|
||||
placeholder="C:\Program Files\Software\app.exe"
|
||||
value={editing.executablePath}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, executablePath: e.target.value }))}
|
||||
className="h-9 text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Launch Arguments</Label>
|
||||
<Input
|
||||
placeholder="-patient {patientId} -mode bridge"
|
||||
value={editing.arguments}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, arguments: e.target.value }))}
|
||||
className="h-9 text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Notes</Label>
|
||||
<Input
|
||||
placeholder="Optional notes or version info"
|
||||
value={editing.notes}
|
||||
onChange={(e) => setEditing((d) => ({ ...d, notes: e.target.value }))}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<Switch
|
||||
checked={editing.enabled}
|
||||
onCheckedChange={(v) => setEditing((d) => ({ ...d, enabled: v }))}
|
||||
/>
|
||||
<Label className="text-sm cursor-pointer">
|
||||
{editing.enabled ? "Enabled" : "Disabled"}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSave} disabled={!editing.name || editing.name === "Custom"}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user