queue-med/src_ref/pages/Onboarding.tsx

324 lines
13 KiB
TypeScript

import { useState } from "react";
import { useLocation } from "wouter";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import {
Building2, Clock, QrCode, CheckCircle2,
ChevronRight, ChevronLeft, Stethoscope, Loader2
} from "lucide-react";
const STEPS = [
{
id: 1,
title: "Votre cabinet",
description: "Commençons par les informations de base de votre cabinet médical.",
icon: Building2,
},
{
id: 2,
title: "Paramètres de la file",
description: "Configurez le comportement de votre salle d'attente virtuelle.",
icon: Clock,
},
{
id: 3,
title: "Votre QR code est prêt",
description: "Tout est configuré ! Voici comment démarrer.",
icon: QrCode,
},
];
export default function Onboarding() {
const [, navigate] = useLocation();
const [step, setStep] = useState(1);
const [clinicId, setClinicId] = useState<number | null>(null);
// Form state
const [name, setName] = useState("");
const [address, setAddress] = useState("");
const [phone, setPhone] = useState("");
const [avgConsultation, setAvgConsultation] = useState(15);
const [maxQueue, setMaxQueue] = useState(30);
const [qrRotation, setQrRotation] = useState(60);
const createClinic = trpc.clinic.create.useMutation({
onSuccess: (data) => {
setClinicId(data.id);
setStep(3);
toast.success("Cabinet créé avec succès !");
},
onError: (e) => toast.error(e.message),
});
const handleNext = () => {
if (step === 1) {
if (!name.trim()) { toast.error("Le nom du cabinet est requis."); return; }
setStep(2);
} else if (step === 2) {
createClinic.mutate({
name: name.trim(),
address: address.trim() || undefined,
phone: phone.trim() || undefined,
avgConsultationMinutes: avgConsultation,
maxQueueSize: maxQueue,
qrRotationMinutes: qrRotation,
});
}
};
const currentStep = STEPS.find(s => s.id === step)!;
const StepIcon = currentStep.icon;
return (
<div className="min-h-screen flex items-center justify-center p-4">
{/* Background blobs */}
<div className="fixed inset-0 pointer-events-none overflow-hidden">
<div className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full bg-primary/10 blur-3xl animate-pulse-glow" />
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 rounded-full bg-secondary/10 blur-3xl animate-pulse-glow" style={{ animationDelay: "1s" }} />
</div>
<div className="relative z-10 w-full max-w-lg">
{/* Header */}
<div className="text-center mb-8">
<div className="flex items-center justify-center gap-2 mb-4">
<div className="w-10 h-10 rounded-xl bg-primary/20 border border-primary/40 flex items-center justify-center glow-teal">
<Stethoscope className="w-5 h-5 text-primary" />
</div>
<span className="font-display text-xl font-bold gradient-text">QueueMed</span>
</div>
<h1 className="font-display text-2xl font-bold mb-2">Configuration initiale</h1>
<p className="text-muted-foreground text-sm">Configurez votre premier cabinet en 2 minutes</p>
</div>
{/* Step indicators */}
<div className="flex items-center justify-center gap-2 mb-8">
{STEPS.map((s, i) => (
<div key={s.id} className="flex items-center gap-2">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all duration-300 ${
s.id < step ? "bg-primary text-primary-foreground" :
s.id === step ? "bg-primary/20 border-2 border-primary text-primary" :
"bg-muted text-muted-foreground"
}`}>
{s.id < step ? <CheckCircle2 className="w-4 h-4" /> : s.id}
</div>
{i < STEPS.length - 1 && (
<div className={`w-12 h-0.5 transition-all duration-300 ${s.id < step ? "bg-primary" : "bg-border"}`} />
)}
</div>
))}
</div>
{/* Card */}
<div className="glass-card rounded-3xl p-8">
{/* Step header */}
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 rounded-2xl bg-primary/20 border border-primary/40 flex items-center justify-center glow-teal flex-shrink-0">
<StepIcon className="w-6 h-6 text-primary" />
</div>
<div>
<h2 className="font-display text-xl font-bold">{currentStep.title}</h2>
<p className="text-muted-foreground text-sm">{currentStep.description}</p>
</div>
</div>
{/* Step 1 — Cabinet info */}
{step === 1 && (
<div className="space-y-4">
<div>
<Label htmlFor="name" className="text-sm font-medium mb-1.5 block">
Nom du cabinet <span className="text-destructive">*</span>
</Label>
<Input
id="name"
placeholder="Ex: Cabinet Dr. Martin"
value={name}
onChange={e => setName(e.target.value)}
className="bg-muted/50 border-border/60 focus:border-primary"
onKeyDown={e => e.key === "Enter" && handleNext()}
/>
</div>
<div>
<Label htmlFor="address" className="text-sm font-medium mb-1.5 block">
Adresse <span className="text-muted-foreground text-xs">(optionnel)</span>
</Label>
<Input
id="address"
placeholder="Ex: 12 rue de la Paix, Paris"
value={address}
onChange={e => setAddress(e.target.value)}
className="bg-muted/50 border-border/60 focus:border-primary"
/>
</div>
<div>
<Label htmlFor="phone" className="text-sm font-medium mb-1.5 block">
Téléphone <span className="text-muted-foreground text-xs">(optionnel)</span>
</Label>
<Input
id="phone"
placeholder="Ex: 01 23 45 67 89"
value={phone}
onChange={e => setPhone(e.target.value)}
className="bg-muted/50 border-border/60 focus:border-primary"
/>
</div>
</div>
)}
{/* Step 2 — Queue settings */}
{step === 2 && (
<div className="space-y-6">
<div>
<Label className="text-sm font-medium mb-1.5 block">
Durée moyenne de consultation
</Label>
<div className="flex items-center gap-4">
<input
type="range"
min={5} max={60} step={5}
value={avgConsultation}
onChange={e => setAvgConsultation(Number(e.target.value))}
className="flex-1 accent-primary"
/>
<span className="text-primary font-bold w-16 text-right">{avgConsultation} min</span>
</div>
<p className="text-muted-foreground text-xs mt-1">Utilisé pour estimer le temps d'attente des patients.</p>
</div>
<div>
<Label className="text-sm font-medium mb-1.5 block">
Taille maximale de la file
</Label>
<div className="flex items-center gap-4">
<input
type="range"
min={5} max={100} step={5}
value={maxQueue}
onChange={e => setMaxQueue(Number(e.target.value))}
className="flex-1 accent-primary"
/>
<span className="text-primary font-bold w-16 text-right">{maxQueue} patients</span>
</div>
<p className="text-muted-foreground text-xs mt-1">Au-delà, les nouveaux patients ne peuvent plus rejoindre.</p>
</div>
<div>
<Label className="text-sm font-medium mb-1.5 block">
Rotation du QR code (anti-triche)
</Label>
<div className="flex gap-2 flex-wrap">
{[0, 30, 60, 120, 240].map(v => (
<button
key={v}
onClick={() => setQrRotation(v)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-all ${
qrRotation === v
? "bg-primary text-primary-foreground border-primary glow-teal"
: "bg-muted/50 border-border/60 text-muted-foreground hover:border-primary/50"
}`}
>
{v === 0 ? "Désactivé" : v < 60 ? `${v} min` : `${v / 60}h`}
</button>
))}
</div>
<p className="text-muted-foreground text-xs mt-2">
Le QR code change de token automatiquement pour éviter les partages frauduleux.
</p>
</div>
</div>
)}
{/* Step 3 — Success */}
{step === 3 && (
<div className="text-center space-y-6">
<div className="w-20 h-20 rounded-full bg-green-500/20 border-2 border-green-500/40 flex items-center justify-center mx-auto">
<CheckCircle2 className="w-10 h-10 text-green-400" />
</div>
<div>
<h3 className="font-display text-xl font-bold text-green-400 mb-2">Cabinet créé !</h3>
<p className="text-muted-foreground text-sm leading-relaxed">
Votre cabinet <strong className="text-foreground">"{name}"</strong> est configuré.
Voici les prochaines étapes pour démarrer.
</p>
</div>
<div className="space-y-3 text-left">
{[
{ num: "1", text: "Imprimez ou affichez le QR code à l'accueil", color: "text-primary" },
{ num: "2", text: "Ouvrez la file d'attente depuis le tableau de bord", color: "text-primary" },
{ num: "3", text: "Configurez l'écran d'affichage sur votre tablette", color: "text-primary" },
].map(item => (
<div key={item.num} className="flex items-start gap-3 p-3 rounded-xl bg-muted/30 border border-border/40">
<span className={`w-6 h-6 rounded-full bg-primary/20 border border-primary/40 flex items-center justify-center text-xs font-bold ${item.color} flex-shrink-0 mt-0.5`}>
{item.num}
</span>
<span className="text-sm text-foreground/80">{item.text}</span>
</div>
))}
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-3 mt-8">
{step > 1 && step < 3 && (
<Button
variant="outline"
onClick={() => setStep(step - 1)}
className="flex-1"
disabled={createClinic.isPending}
>
<ChevronLeft className="w-4 h-4 mr-1" />
Retour
</Button>
)}
{step < 3 ? (
<Button
onClick={handleNext}
disabled={createClinic.isPending}
className="flex-1 bg-primary text-primary-foreground hover:bg-primary/90 glow-teal font-semibold h-12"
>
{createClinic.isPending ? (
<><Loader2 className="w-4 h-4 mr-2 animate-spin" />Création...</>
) : (
<>{step === 2 ? "Créer le cabinet" : "Continuer"}<ChevronRight className="w-4 h-4 ml-1" /></>
)}
</Button>
) : (
<div className="flex gap-3 w-full">
<Button
variant="outline"
onClick={() => navigate(`/dashboard/queue/${clinicId}`)}
className="flex-1"
>
<QrCode className="w-4 h-4 mr-2" />
Voir le QR code
</Button>
<Button
onClick={() => navigate("/dashboard")}
className="flex-1 bg-primary text-primary-foreground hover:bg-primary/90 glow-teal font-semibold"
>
Tableau de bord
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
)}
</div>
</div>
{/* Skip link */}
{step < 3 && (
<p className="text-center mt-4 text-sm text-muted-foreground">
<button
onClick={() => navigate("/dashboard")}
className="underline underline-offset-2 hover:text-foreground transition-colors"
>
Passer pour l'instant
</button>
</p>
)}
</div>
</div>
);
}