200 lines
11 KiB
TypeScript
200 lines
11 KiB
TypeScript
import { useAuth } from "@/_core/hooks/useAuth";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useLocation } from "wouter";
|
|
import { getLoginUrl } from "@/const";
|
|
import {
|
|
Users, Building2, BarChart3, CreditCard, ChevronRight,
|
|
Clock, TrendingUp, Activity, Plus, LogOut, Loader2,
|
|
HelpCircle, Sparkles, QrCode
|
|
} from "lucide-react";
|
|
|
|
export default function Dashboard() {
|
|
const { user, isAuthenticated, loading, logout } = useAuth();
|
|
const [, navigate] = useLocation();
|
|
|
|
const clinicsQuery = trpc.clinic.list.useQuery(undefined, { enabled: isAuthenticated });
|
|
const subQuery = trpc.subscription.get.useQuery(undefined, { enabled: isAuthenticated });
|
|
const analyticsQuery = trpc.analytics.getAll.useQuery({ days: 7 }, { enabled: isAuthenticated });
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center p-4">
|
|
<div className="glass-card rounded-3xl p-10 text-center max-w-sm w-full">
|
|
<div className="w-16 h-16 rounded-2xl bg-primary/20 border border-primary/40 flex items-center justify-center mx-auto mb-6 glow-teal">
|
|
<Users className="w-8 h-8 text-primary" />
|
|
</div>
|
|
<h1 className="font-display text-2xl font-bold mb-3">Espace Médecin</h1>
|
|
<p className="text-muted-foreground text-sm mb-8">Connectez-vous pour accéder à votre tableau de bord.</p>
|
|
<Button onClick={() => window.location.href = getLoginUrl()} className="w-full bg-primary text-primary-foreground hover:bg-primary/90 glow-teal h-12 font-semibold">
|
|
Se connecter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const sub = subQuery.data;
|
|
const clinics = clinicsQuery.data ?? [];
|
|
const analytics = analyticsQuery.data ?? [];
|
|
|
|
const totalPatients = analytics.reduce((sum, a) => sum + a.totalPatients, 0);
|
|
const avgWait = analytics.length > 0
|
|
? Math.round(analytics.reduce((sum, a) => sum + a.avgWait, 0) / analytics.length)
|
|
: 0;
|
|
|
|
const isTrialing = sub?.status === "trialing";
|
|
const trialDaysLeft = sub?.trialEndsAt
|
|
? Math.max(0, Math.ceil((new Date(sub.trialEndsAt).getTime() - Date.now()) / 86400000))
|
|
: 0;
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
{/* Background */}
|
|
<div className="absolute inset-0 pointer-events-none">
|
|
<div className="absolute top-0 left-0 w-full h-64 bg-gradient-to-b from-primary/5 to-transparent" />
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<header className="relative z-10 border-b border-border/50 backdrop-blur-xl bg-background/60 sticky top-0">
|
|
<div className="container flex items-center justify-between h-16">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-primary/20 border border-primary/40 flex items-center justify-center">
|
|
<Users className="w-4 h-4 text-primary" />
|
|
</div>
|
|
<span className="font-display font-bold text-lg gradient-text">QueueMed</span>
|
|
</div>
|
|
<nav className="hidden md:flex items-center gap-6">
|
|
<button onClick={() => navigate("/dashboard")} className="text-sm font-medium text-foreground">Accueil</button>
|
|
<button onClick={() => navigate("/dashboard/clinics")} className="text-sm text-muted-foreground hover:text-foreground transition-colors">Cabinets</button>
|
|
<button onClick={() => navigate("/dashboard/analytics")} className="text-sm text-muted-foreground hover:text-foreground transition-colors">Analytics</button>
|
|
<button onClick={() => navigate("/dashboard/subscription")} className="text-sm text-muted-foreground hover:text-foreground transition-colors">Abonnement</button>
|
|
</nav>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm text-muted-foreground hidden md:block">{user?.name}</span>
|
|
<Button variant="ghost" size="sm" onClick={logout} className="text-muted-foreground hover:text-foreground">
|
|
<LogOut className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="relative z-10 container py-8">
|
|
{/* Welcome + trial banner */}
|
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-8 gap-4">
|
|
<div>
|
|
<h1 className="font-display text-3xl font-bold mb-1">
|
|
Bonjour, <span className="gradient-text">{user?.name?.split(" ")[0] ?? "Docteur"}</span>
|
|
</h1>
|
|
<p className="text-muted-foreground">Gérez vos files d'attente en temps réel</p>
|
|
</div>
|
|
{isTrialing && (
|
|
<div className={`px-4 py-2 rounded-xl border text-sm font-medium ${trialDaysLeft > 7 ? "bg-teal-500/10 border-teal-500/30 text-teal-300" : "bg-amber-500/10 border-amber-500/30 text-amber-300"}`}>
|
|
{trialDaysLeft > 0 ? `Essai gratuit : ${trialDaysLeft} jour${trialDaysLeft > 1 ? "s" : ""} restant${trialDaysLeft > 1 ? "s" : ""}` : "Essai expiré"}
|
|
{trialDaysLeft <= 7 && (
|
|
<button onClick={() => navigate("/dashboard/subscription")} className="ml-2 underline">S'abonner</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
{[
|
|
{ label: "Cabinets actifs", value: clinics.length, icon: Building2, color: "text-teal-400" },
|
|
{ label: "Patients (7j)", value: totalPatients, icon: Users, color: "text-orange-400" },
|
|
{ label: "Attente moy.", value: `${avgWait} min`, icon: Clock, color: "text-cyan-400" },
|
|
{ label: "Plan", value: sub?.plan ?? "—", icon: CreditCard, color: "text-violet-400" },
|
|
].map((stat) => (
|
|
<div key={stat.label} className="glass-card rounded-2xl p-5">
|
|
<div className={`w-8 h-8 rounded-lg bg-card border border-border flex items-center justify-center mb-3 ${stat.color}`}>
|
|
<stat.icon className="w-4 h-4" />
|
|
</div>
|
|
<div className="font-display font-bold text-2xl text-foreground capitalize">{stat.value}</div>
|
|
<div className="text-muted-foreground text-xs mt-1">{stat.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Clinics quick access */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="font-display font-bold text-xl">Vos cabinets</h2>
|
|
<Button variant="outline" size="sm" onClick={() => navigate("/dashboard/clinics")} className="border-border/60 text-muted-foreground hover:text-foreground">
|
|
<Plus className="w-4 h-4 mr-2" /> Gérer
|
|
</Button>
|
|
</div>
|
|
|
|
{clinicsQuery.isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
|
</div>
|
|
) : clinics.length === 0 ? (
|
|
<div className="glass-card rounded-2xl p-8 text-center border-primary/20">
|
|
<div className="w-16 h-16 rounded-2xl bg-primary/15 border border-primary/30 flex items-center justify-center mx-auto mb-4 glow-teal">
|
|
<Sparkles className="w-8 h-8 text-primary" />
|
|
</div>
|
|
<h3 className="font-display font-bold text-xl mb-2">Bienvenue sur QueueMed !</h3>
|
|
<p className="text-muted-foreground text-sm mb-6 max-w-sm mx-auto">Configurez votre premier cabinet en 2 minutes avec notre assistant de démarrage.</p>
|
|
<div className="flex gap-3 justify-center flex-wrap">
|
|
<Button onClick={() => navigate("/onboarding")} className="bg-primary text-primary-foreground hover:bg-primary/90 glow-teal">
|
|
<Sparkles className="w-4 h-4 mr-2" /> Démarrer la configuration
|
|
</Button>
|
|
<Button variant="outline" onClick={() => navigate("/dashboard/clinics")} className="border-border/60">
|
|
<Plus className="w-4 h-4 mr-2" /> Créer manuellement
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{clinics.map((clinic) => (
|
|
<div key={clinic.id} className="glass-card rounded-2xl p-5 hover:border-primary/30 transition-all cursor-pointer group" onClick={() => navigate(`/dashboard/queue/${clinic.id}`)}>
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: `${clinic.color}20`, border: `1px solid ${clinic.color}40` }}>
|
|
<Building2 className="w-5 h-5" style={{ color: clinic.color ?? "#0d9488" }} />
|
|
</div>
|
|
<div className={`px-2 py-1 rounded-full text-xs font-medium ${clinic.isQueueOpen ? "badge-called" : "bg-muted text-muted-foreground border border-border"}`}>
|
|
{clinic.isQueueOpen ? "Ouvert" : "Fermé"}
|
|
</div>
|
|
</div>
|
|
<h3 className="font-display font-semibold mb-1">{clinic.name}</h3>
|
|
{clinic.address && <p className="text-muted-foreground text-xs mb-3 truncate">{clinic.address}</p>}
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground text-xs">~{clinic.avgConsultationMinutes} min/patient</span>
|
|
<ChevronRight className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick links */}
|
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{ icon: BarChart3, label: "Analytics", desc: "Statistiques et prédictions", path: "/dashboard/analytics", color: "text-pink-400" },
|
|
{ icon: TrendingUp, label: "Abonnement", desc: "Gérer votre plan", path: "/dashboard/subscription", color: "text-violet-400" },
|
|
{ icon: Activity, label: "Affichage", desc: "Écran salle d'attente", path: clinics[0] ? `/display/${clinics[0].id}` : "/dashboard", color: "text-cyan-400" },
|
|
{ icon: HelpCircle, label: "Aide", desc: "Centre d'aide & FAQ", path: "/help", color: "text-amber-400" },
|
|
].map((item) => (
|
|
<button key={item.label} onClick={() => navigate(item.path)} className="glass-card rounded-2xl p-5 text-left hover:border-primary/30 transition-all group">
|
|
<div className={`w-8 h-8 rounded-lg bg-card border border-border flex items-center justify-center mb-3 ${item.color}`}>
|
|
<item.icon className="w-4 h-4" />
|
|
</div>
|
|
<div className="font-display font-semibold text-sm mb-1">{item.label}</div>
|
|
<div className="text-muted-foreground text-xs">{item.desc}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|