import { useEffect, useRef, useState } from "react"; import { useParams, useLocation } from "wouter"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { io, Socket } from "socket.io-client"; import { ChevronLeft, Play, UserX, Trash2, QrCode, Monitor, Users, Clock, Printer, RefreshCw, Loader2, Power, PowerOff } from "lucide-react"; import { toast } from "sonner"; type EntryStatus = "waiting" | "called" | "in_consultation" | "done" | "absent" | "canceled"; interface QueueEntry { id: number; ticketNumber: number; patientName: string | null; status: EntryStatus; position: number; joinedAt: Date; estimatedWaitMinutes: number | null; isPrinted: boolean; } export default function QueueManagement() { const params = useParams<{ clinicId: string }>(); const [, navigate] = useLocation(); const clinicId = parseInt(params.clinicId || "0"); const socketRef = useRef(null); const [liveQueue, setLiveQueue] = useState(null); const clinicQuery = trpc.clinic.get.useQuery({ id: clinicId }, { enabled: !!clinicId }); const queueQuery = trpc.queue.getQueue.useQuery({ clinicId }, { enabled: !!clinicId, refetchInterval: 10000 }); const qrQuery = trpc.clinic.getQrCode.useQuery({ id: clinicId }, { enabled: !!clinicId }); const callNext = trpc.queue.callNext.useMutation({ onSuccess: (data) => { toast.success(`Ticket #${data.calledTicket} appelé !`); queueQuery.refetch(); }, onError: (e) => toast.error(e.message), }); const markAbsent = trpc.queue.markAbsent.useMutation({ onSuccess: () => { toast.success("Patient marqué absent"); queueQuery.refetch(); }, onError: (e) => toast.error(e.message), }); const removeEntry = trpc.queue.remove.useMutation({ onSuccess: () => { toast.success("Patient retiré de la file"); queueQuery.refetch(); }, onError: (e) => toast.error(e.message), }); const toggleQueue = trpc.clinic.toggleQueue.useMutation({ onSuccess: () => { toast.success("Statut de la file mis à jour"); clinicQuery.refetch(); }, onError: (e) => toast.error(e.message), }); const resetQueue = trpc.queue.reset.useMutation({ onSuccess: () => { toast.success("File réinitialisée"); queueQuery.refetch(); clinicQuery.refetch(); }, onError: (e) => toast.error(e.message), }); const printTicket = trpc.queue.printTicket.useMutation({ onSuccess: (data) => { toast.success(`Ticket #${data.ticketNumber} créé`); window.open(data.printUrl, "_blank"); queueQuery.refetch(); }, onError: (e) => toast.error(e.message), }); // WebSocket for live updates useEffect(() => { if (!clinicId) return undefined; const socket = io("/", { path: "/api/socket.io", transports: ["websocket", "polling"] }); socketRef.current = socket; socket.emit("doctor:join", { clinicId }); socket.on("queue:update", (data: { waiting: QueueEntry[] }) => { if (data.waiting) setLiveQueue(data.waiting); }); return () => { socket.disconnect(); }; }, [clinicId]); const queue = liveQueue ?? (queueQuery.data as QueueEntry[] | undefined) ?? []; const clinic = clinicQuery.data; const waiting = queue.filter((e) => e.status === "waiting"); const called = queue.filter((e) => e.status === "called"); const statusBadge = (status: EntryStatus) => { const map: Record = { waiting: "badge-waiting", called: "badge-called", in_consultation: "badge-called", done: "badge-done", absent: "badge-absent", canceled: "badge-absent", }; const labels: Record = { waiting: "En attente", called: "Appelé", in_consultation: "En consultation", done: "Terminé", absent: "Absent", canceled: "Annulé", }; return {labels[status]}; }; return (
{/* Header */}

{clinic?.name ?? "Chargement..."}

{waiting.length} en attente · {called.length} appelé

{/* Left: Controls */}
{/* Call next */}

Actions

{/* QR Code */}

QR Code

{qrQuery.data ? (
QR Code

Expire : {qrQuery.data.expiresAt ? new Date(qrQuery.data.expiresAt).toLocaleTimeString("fr-FR") : "—"}

) : (
)}
{/* Stats */}

Statistiques

{[ { label: "En attente", value: waiting.length, icon: Users }, { label: "Appelé", value: called.length, icon: Play }, { label: "Attente moy.", value: `~${clinic?.avgConsultationMinutes ?? 15} min`, icon: Clock }, ].map((s) => (
{s.label}
{s.value}
))}
{/* Right: Queue list */}

File d'attente

{queue.length} patient{queue.length > 1 ? "s" : ""}
{queueQuery.isLoading ? (
) : queue.length === 0 ? (

Aucun patient en file d'attente

{!clinic?.isQueueOpen && (

Ouvrez la file pour commencer à accepter des patients

)}
) : (
{queue.map((entry) => (
{/* Ticket number */}
{String(entry.ticketNumber).padStart(3, "0")}
{/* Info */}
{entry.patientName ?? `Patient #${entry.ticketNumber}`} {entry.isPrinted && Ticket imprimé}
Pos. {entry.position} · ~{entry.estimatedWaitMinutes ?? "?"} min · {new Date(entry.joinedAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
{/* Status */}
{statusBadge(entry.status)}
{/* Actions */} {(entry.status === "waiting" || entry.status === "called") && (
)}
))}
)}
); }