233 lines
8.5 KiB
TypeScript
233 lines
8.5 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useParams, useLocation } from "wouter";
|
|
import { Helmet } from "react-helmet-async";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
Stethoscope, Building2, MapPin, Clock, Hash,
|
|
Printer, ChevronLeft, Loader2, XCircle,
|
|
} from "lucide-react";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { Button } from "@/components/ui/button";
|
|
import { formatTicket, formatTime, formatDate } from "@/lib/utils";
|
|
|
|
export default function PrintTicket() {
|
|
const { t } = useTranslation();
|
|
const params = useParams<{ entryId: string }>();
|
|
const [, navigate] = useLocation();
|
|
const entryId = parseInt(params.entryId ?? "0", 10);
|
|
|
|
const ticketQuery = trpc.queue.getEntryById.useQuery(
|
|
{ id: entryId },
|
|
{ enabled: entryId > 0 }
|
|
);
|
|
|
|
// Auto-trigger print dialog once data is loaded (helpful when opened from doctor UI)
|
|
useEffect(() => {
|
|
if (ticketQuery.data && typeof window !== "undefined") {
|
|
const t = setTimeout(() => {
|
|
try { window.print(); } catch {}
|
|
}, 600);
|
|
return () => clearTimeout(t);
|
|
}
|
|
}, [ticketQuery.data]);
|
|
|
|
if (ticketQuery.isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<Helmet>
|
|
<title>{t("ticket.metaTitle")}</title>
|
|
<meta name="description" content={t("ticket.metaDescription")} />
|
|
</Helmet>
|
|
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (ticketQuery.error || !ticketQuery.data) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center p-4">
|
|
<Helmet>
|
|
<title>{t("ticket.metaTitle")}</title>
|
|
<meta name="description" content={t("ticket.metaDescription")} />
|
|
</Helmet>
|
|
<div className="glass-card rounded-3xl p-10 text-center max-w-md">
|
|
<XCircle className="w-12 h-12 text-red-400 mx-auto mb-4" />
|
|
<h1 className="font-bold text-2xl mb-2">{t("ticket.notFound")}</h1>
|
|
<p className="text-slate-500 text-sm mb-6">
|
|
{t("ticket.notFoundDesc")}
|
|
</p>
|
|
<Button variant="gradient" onClick={() => navigate("/")}>
|
|
{t("common.backToHome")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { entry, clinic } = ticketQuery.data;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
<Helmet>
|
|
<title>{t("ticket.metaTitle")}</title>
|
|
<meta name="description" content={t("ticket.metaDescription")} />
|
|
</Helmet>
|
|
{/* Controls — hidden when printing */}
|
|
<div className="print:hidden max-w-2xl mx-auto px-4 py-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<button
|
|
onClick={() => navigate("/dashboard")}
|
|
className="flex items-center gap-2 text-slate-500 hover:text-emerald-700 transition-colors text-sm"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
{t("common.back")}
|
|
</button>
|
|
<Button
|
|
onClick={() => window.print()}
|
|
variant="gradient"
|
|
className="font-semibold"
|
|
>
|
|
<Printer className="w-4 h-4 mr-2" />
|
|
{t("ticket.printTicket")}
|
|
</Button>
|
|
</div>
|
|
<div className="glass-card rounded-2xl p-4 text-sm text-slate-600 flex items-start gap-3">
|
|
<Printer className="w-5 h-5 text-emerald-600 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<strong className="text-slate-900">{t("ticket.tipLabel")} :</strong> {t("ticket.tipText")}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Printable ticket */}
|
|
<div className="max-w-md mx-auto px-4 pb-12 print:p-0 print:max-w-none print:mx-0">
|
|
<div
|
|
className="bg-white rounded-3xl print:rounded-none overflow-hidden border border-slate-200 print:border-0 shadow-xl print:shadow-none"
|
|
style={{ fontFamily: "'Inter', system-ui, sans-serif" }}
|
|
>
|
|
{/* Header band */}
|
|
<div
|
|
style={{
|
|
background: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
padding: "20px 28px",
|
|
color: "white",
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<div
|
|
style={{
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 8,
|
|
background: "rgba(255,255,255,0.22)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<Stethoscope className="w-4 h-4" />
|
|
</div>
|
|
<span style={{ fontSize: 18, fontWeight: 800, letterSpacing: "-0.02em" }}>
|
|
QueueMed
|
|
</span>
|
|
</div>
|
|
<p style={{ fontSize: 12, opacity: 0.9, margin: 0 }}>
|
|
{t("ticket.subtitle")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Clinic */}
|
|
<div className="px-7 pt-6 text-center">
|
|
{clinic?.name && (
|
|
<h1 className="font-bold text-xl text-slate-900 flex items-center justify-center gap-2">
|
|
<Building2 className="w-4 h-4 text-emerald-600" />
|
|
{clinic.name}
|
|
</h1>
|
|
)}
|
|
{clinic?.address && (
|
|
<p className="text-slate-500 text-xs mt-1 flex items-center justify-center gap-1">
|
|
<MapPin className="w-3 h-3" />
|
|
{clinic.address}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Ticket number */}
|
|
<div className="px-7 py-8 text-center">
|
|
<div className="text-xs uppercase tracking-widest text-slate-500 font-bold mb-2">
|
|
{t("ticket.yourNumber")}
|
|
</div>
|
|
<div
|
|
className="font-black leading-none"
|
|
style={{
|
|
fontSize: 96,
|
|
background: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
|
|
WebkitBackgroundClip: "text",
|
|
backgroundClip: "text",
|
|
color: "transparent",
|
|
WebkitTextFillColor: "transparent",
|
|
}}
|
|
>
|
|
{formatTicket(entry.ticketNumber)}
|
|
</div>
|
|
{entry.patientName && (
|
|
<div className="text-sm text-slate-600 mt-3">
|
|
{entry.patientName}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="px-7 pb-6 grid grid-cols-2 gap-3">
|
|
<div className="rounded-2xl p-4 text-center bg-emerald-50 border border-emerald-200">
|
|
<div className="text-[10px] uppercase tracking-wider text-emerald-700 font-bold flex items-center justify-center gap-1 mb-1">
|
|
<Hash className="w-3 h-3" />
|
|
{t("ticket.position")}
|
|
</div>
|
|
<div className="font-black text-2xl text-emerald-900">
|
|
{entry.position ?? "—"}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl p-4 text-center bg-cyan-50 border border-cyan-200">
|
|
<div className="text-[10px] uppercase tracking-wider text-cyan-700 font-bold flex items-center justify-center gap-1 mb-1">
|
|
<Clock className="w-3 h-3" />
|
|
{t("ticket.wait")}
|
|
</div>
|
|
<div className="font-black text-2xl text-cyan-900">
|
|
~{entry.estimatedWaitMinutes ?? "?"}
|
|
<span className="text-xs font-bold ml-1">{t("ticket.minShort")}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Instructions */}
|
|
<div className="px-7 pb-6">
|
|
<div className="rounded-xl p-4 bg-slate-50 border border-slate-200 text-xs text-slate-600 leading-relaxed">
|
|
<strong className="text-slate-900 block mb-1">
|
|
{t("ticket.howItWorks")}
|
|
</strong>
|
|
{t("ticket.howItWorksDesc")}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div
|
|
className="border-t border-slate-200 px-7 py-3 flex items-center justify-between text-[10px] text-slate-400"
|
|
style={{ background: "#f8fafc" }}
|
|
>
|
|
<span>{t("ticket.issuedAt", { date: formatDate(entry.joinedAt), time: formatTime(entry.joinedAt) })}</span>
|
|
<span>queuemed.fr</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>{`
|
|
@media print {
|
|
html, body { background: white !important; }
|
|
.print\\:hidden { display: none !important; }
|
|
@page { margin: 8mm; size: A6; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|