queue-med/client/src/pages/PrintTicket.tsx

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>
);
}