Files
NaMesa_site/reserva-mesa-dashboard/app/(dashboard)/page.tsx
2026-05-26 16:40:01 +01:00

192 lines
8.0 KiB
TypeScript

"use client";
import { useAuth } from "@/hooks/useAuth";
import { useReservas } from "@/hooks/useReservas";
import { useMesas } from "@/hooks/useMesas";
import { useStaff } from "@/hooks/useStaff";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { OverviewChart } from "@/components/dashboard/OverviewChart";
import { OccupancyPieChart } from "@/components/dashboard/OccupancyPieChart";
import {
Users,
CalendarCheck,
Clock,
TrendingUp,
UserCheck
} from "lucide-react";
export default function DashboardHomePage() {
const { user } = useAuth();
const { reservas, loading: loadingReservas } = useReservas();
const { mesas, loading: loadingMesas } = useMesas();
const { staff } = useStaff();
// Guard against missing user data
if (!user) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-primary font-display text-2xl animate-pulse">A carregar...</div>
</div>
);
}
// 1. Calculate top stats
const todayStr = new Date().toISOString().split('T')[0];
const todayReservas = reservas.filter(r => r.data === todayStr || r.estado.startsWith("Confirmada"));
const activeReservas = todayReservas.filter(r => r.estado.startsWith("Confirmada")).length;
const pendingReservas = todayReservas.filter(r => r.estado === "Pendente").length;
const totalMesas = mesas.length;
const occupiedMesas = mesas.filter(m => m.estado === "Ocupada").length;
const reservedMesas = mesas.filter(m => m.estado === "Reservada").length;
const freeMesas = totalMesas - occupiedMesas - reservedMesas;
const occupancyRate = totalMesas > 0 ? Math.round(((occupiedMesas + reservedMesas) / totalMesas) * 100) : 0;
const stats = [
{ name: "Reservas Hoje", value: todayReservas.length.toString(), icon: CalendarCheck, trend: `+${pendingReservas} pendentes` },
{ name: "Mesas Ocupadas", value: `${occupiedMesas} / ${totalMesas}`, icon: Clock, trend: `${freeMesas} livres` },
{ name: "Staff Ativo", value: staff.length.toString(), icon: UserCheck, trend: "Equipa total" },
{ name: "Ocupação", value: `${occupancyRate}%`, icon: TrendingUp, trend: "Tempo real" },
];
// 2. Process data for Overview Chart (Last 7 days)
const last7Days = Array.from({ length: 7 }, (_, i) => {
const d = new Date();
d.setDate(d.getDate() - i);
return d.toISOString().split('T')[0];
}).reverse();
const chartData = last7Days.map(date => {
// Usar formato YYYY/MM/DD para compatibilidade total entre browsers
const safeDate = date.replace(/-/g, '/');
const dayLabel = new Date(safeDate).toLocaleDateString('pt-PT', { weekday: 'short' });
const count = reservas.filter(r => r.data === date).length;
return { name: dayLabel, total: count };
});
// 3. Process data for Pie Chart
const pieData = [
{ name: "Livre", value: freeMesas, color: "#2A261E" },
{ name: "Ocupada", value: occupiedMesas, color: "#D4891A" },
{ name: "Reservada", value: reservedMesas, color: "#E8A832" },
];
return (
<div className="space-y-8">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-3xl font-display font-bold text-foreground">
Bem-vindo, {user?.establishmentName || "Restaurante"}
</h1>
<p className="text-muted-foreground mt-1 text-lg">
Monitorize o desempenho do seu estabelecimento em tempo real.
</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => (
<Card key={stat.name} className="overflow-hidden border-border/50 shadow-sm hover:shadow-md transition-shadow duration-200">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
{stat.name}
</CardTitle>
<stat.icon className="h-5 w-5 text-primary opacity-80" />
</CardHeader>
<CardContent>
<div className="text-3xl font-display font-bold">
{loadingReservas || loadingMesas ? "..." : stat.value}
</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
<span className="text-primary font-medium">{stat.trend}</span>
</p>
</CardContent>
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Volume de Reservas</CardTitle>
<CardDescription>Fluxo de clientes nos últimos 7 dias</CardDescription>
</CardHeader>
<CardContent className="pl-2">
<OverviewChart data={chartData} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Ocupação das Mesas</CardTitle>
<CardDescription>Estado atual do restaurante</CardDescription>
</CardHeader>
<CardContent>
<OccupancyPieChart data={pieData} />
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="col-span-1">
<CardHeader>
<CardTitle>Últimas Reservas</CardTitle>
<CardDescription>Atividade mais recente</CardDescription>
</CardHeader>
<CardContent>
{reservas.length > 0 ? (
<div className="space-y-4">
{reservas.slice(0, 5).map((r) => (
<div key={r.id} className="flex items-center justify-between border-b border-border/50 pb-3 last:border-0 last:pb-0">
<div>
<p className="font-medium">{r.clienteEmail}</p>
<p className="text-xs text-muted-foreground">{r.data} às {r.hora} {r.pessoas} pessoas</p>
</div>
<div className={`px-2 py-1 rounded-full text-[10px] font-bold uppercase ${
r.estado.startsWith("Confirmada") ? "bg-green-500/10 text-green-500" :
r.estado === "Pendente" ? "bg-amber-500/10 text-amber-500" :
"bg-muted text-muted-foreground"
}`}>
{r.estado}
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-10 text-center text-muted-foreground">
<p>Nenhuma atividade registada.</p>
</div>
)}
</CardContent>
</Card>
<Card className="col-span-1">
<CardHeader>
<CardTitle>Mesas Críticas</CardTitle>
<CardDescription>Mesas que requerem atenção</CardDescription>
</CardHeader>
<CardContent>
{mesas.filter(m => m.estado !== "Livre").length > 0 ? (
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
{mesas.filter(m => m.estado !== "Livre").map((m) => (
<div key={m.id} className={`flex flex-col items-center justify-center p-3 rounded-lg border transition-colors ${
m.estado === "Ocupada" ? "bg-primary/10 border-primary/30 text-primary shadow-sm" :
"bg-amber-500/10 border-amber-500/30 text-amber-500"
}`}>
<span className="text-xs font-bold uppercase tracking-tighter">Mesa</span>
<span className="text-xl font-display font-bold">{m.numero}</span>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-10 text-center text-muted-foreground">
<p>Todas as mesas estão livres.</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}