183 lines
7.8 KiB
TypeScript
183 lines
7.8 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();
|
|
|
|
// 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>
|
|
);
|
|
}
|