diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx
index 41546eb..479d07f 100644
--- a/web/src/pages/Dashboard.tsx
+++ b/web/src/pages/Dashboard.tsx
@@ -14,7 +14,7 @@ import { currency } from '../lib/format';
import { useApp } from '../context/AppContext';
import { supabase } from '../lib/supabase';
import { Product, BarberShop, ShopSchedule } from '../types';
-import { BarChart, Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis } from 'recharts';
+import { BarChart, Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, PieChart, Pie, Cell, Legend, LineChart, Line } from 'recharts';
import { CalendarWeekView } from '../components/CalendarWeekView';
import {
BarChart3,
@@ -50,7 +50,7 @@ import {
CheckCircle2,
} from 'lucide-react';
-type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings';
+type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings' | 'stats';
interface SidebarTab {
id: TabId;
@@ -375,6 +375,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
const sidebarTabs: SidebarTab[] = [
{ id: 'overview', label: 'Visão Geral', icon: BarChart3 },
+ { id: 'stats', label: 'Estatísticas', icon: TrendingUp },
{ id: 'appointments', label: 'Agendamentos', icon: Calendar, badge: pendingAppts > 0 ? pendingAppts : undefined },
{ id: 'history', label: 'Histórico', icon: History },
{ id: 'orders', label: 'Pedidos', icon: ShoppingBag, badge: pendingOrders > 0 ? pendingOrders : undefined },
@@ -431,46 +432,43 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
- {/* Stats Cards Principais */}
-
-
-
-
-
-
+ {/* Stats Cards Rápidos */}
+
+
+
- Total de reservas
- {allShopAppointments.length}
- Reservas da plataforma: {allShopAppointments.length}
+ Total de Reservas
+ {allShopAppointments.length}
-
-
-
-
-
-
+
+
- Faturação (Serviços + Produtos)
-
- {currency(totalRevenue + allShopAppointments.filter(a => a.status === 'concluido' || a.status === 'confirmado').reduce((s, a) => s + (a.total || 0), 0))}
-
- Receita no período selecionado
+ Concluídas
+ {allShopAppointments.filter(a => a.status === 'concluido').length}
-
-
-
-
-
-
+
+
- Novos clientes
-
- {new Set(allShopAppointments.map(a => a.customerId)).size}
-
- Clientes únicos
+ Pendentes
+ {pendingAppts}
+
+
+
+ Clientes Únicos
+ {new Set(allShopAppointments.map(a => a.customerId)).size}
@@ -527,35 +525,6 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
- {/* Serviços Mais Procurados */}
-
- Top Serviços
- {(() => {
- const map = new Map();
- allShopAppointments.forEach(a => {
- if (a.status === 'cancelado') return;
- const svc = shop.services.find(s => s.id === a.serviceId);
- if (!svc) return;
- const prev = map.get(a.serviceId)?.qty ?? 0;
- map.set(a.serviceId, { name: svc.name, qty: prev + 1 });
- });
- const top = Array.from(map.values()).sort((a, b) => b.qty - a.qty).slice(0, 4);
-
- if (top.length === 0) return Sem dados suficientes
;
-
- return (
-
- {top.map((t, i) => (
-
- {t.name}
- {t.qty} reservas
-
- ))}
-
- );
- })()}
-
-
{/* Próximos Agendamentos */}
Seguinte
@@ -610,6 +579,206 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
)}
+ {activeTab === 'stats' && (() => {
+ // --- Dados para gráficos ---
+ const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
+
+ // 1. Reservas por dia da semana (últimos 90 dias)
+ const diasPT = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
+ const byDayOfWeek = Array.from({ length: 7 }, (_, i) => ({ name: diasPT[i], reservas: 0 }));
+ appointments.filter(a => a.shopId === shop.id && a.status !== 'cancelado').forEach(a => {
+ const d = new Date(a.date.replace(' ', 'T')).getDay();
+ byDayOfWeek[d].reservas++;
+ });
+
+ // 2. Serviços mais marcados (todos os tempos)
+ const svcMap = new Map
();
+ appointments.filter(a => a.shopId === shop.id && a.status !== 'cancelado').forEach(a => {
+ const svc = shop.services.find(s => s.id === a.serviceId);
+ if (!svc) return;
+ const prev = svcMap.get(a.serviceId)?.value ?? 0;
+ svcMap.set(a.serviceId, { name: svc.name, value: prev + 1 });
+ });
+ const topSvcsPie = Array.from(svcMap.values()).sort((a, b) => b.value - a.value).slice(0, 5);
+
+ // 3. Evolução semanal de reservas (últimas 8 semanas)
+ const weeklyData: { name: string; reservas: number; concluidas: number }[] = [];
+ for (let i = 7; i >= 0; i--) {
+ const wStart = new Date();
+ wStart.setDate(wStart.getDate() - i * 7 - 6);
+ wStart.setHours(0, 0, 0, 0);
+ const wEnd = new Date();
+ wEnd.setDate(wEnd.getDate() - i * 7);
+ wEnd.setHours(23, 59, 59, 999);
+ const label = wStart.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' });
+ const weekAppts = appointments.filter(a => {
+ if (a.shopId !== shop.id) return false;
+ const d = new Date(a.date.replace(' ', 'T'));
+ return d >= wStart && d <= wEnd;
+ });
+ weeklyData.push({
+ name: label,
+ reservas: weekAppts.length,
+ concluidas: weekAppts.filter(a => a.status === 'concluido').length,
+ });
+ }
+
+ // 4. Resumo KPI (todos os tempos)
+ const totalAppts = appointments.filter(a => a.shopId === shop.id);
+ const totalConcluidas = totalAppts.filter(a => a.status === 'concluido').length;
+ const totalCanceladas = totalAppts.filter(a => a.status === 'cancelado').length;
+ const taxaConclusao = totalAppts.length > 0 ? Math.round((totalConcluidas / totalAppts.length) * 100) : 0;
+ const revenueServicos = totalAppts.filter(a => a.status === 'concluido').reduce((s, a) => s + (a.total || 0), 0);
+ const revenueProdutos = orders.filter(o => o.shopId === shop.id && o.status === 'concluido').reduce((s, o) => s + o.total, 0);
+
+ // 5. Top barbeiros
+ const barberMap = new Map();
+ totalAppts.filter(a => a.status !== 'cancelado').forEach(a => {
+ const b = shop.barbers.find(x => x.id === a.barberId);
+ if (!b) return;
+ const prev = barberMap.get(a.barberId)?.qty ?? 0;
+ barberMap.set(a.barberId, { name: b.name, qty: prev + 1 });
+ });
+ const topBarbers = Array.from(barberMap.values()).sort((a, b) => b.qty - a.qty);
+
+ return (
+
+
+
Estatísticas
+
Análise completa do desempenho da barbearia (todos os dados históricos)
+
+
+ {/* KPI Cards */}
+
+ {[
+ { label: 'Total de Reservas', value: totalAppts.length, color: 'bg-indigo-500', icon: Calendar },
+ { label: 'Concluídas', value: totalConcluidas, color: 'bg-green-500', icon: CheckCircle2 },
+ { label: 'Canceladas', value: totalCanceladas, color: 'bg-red-400', icon: AlertTriangle },
+ { label: 'Taxa Conclusão', value: `${taxaConclusao}%`, color: 'bg-amber-500', icon: TrendingUp },
+ ].map((kpi, i) => (
+
+
+
+
+ {kpi.label}
+ {kpi.value}
+
+ ))}
+
+
+ {/* Receita */}
+
+
+
+
+
Receita de Serviços
+
+ {currency(revenueServicos)}
+ Baseado nos agendamentos concluídos
+
+
+
+
+
Receita de Produtos
+
+ {currency(revenueProdutos)}
+ Baseado nos pedidos concluídos
+
+
+
+ {/* Gráfico: Evolução semanal */}
+
+ Evolução de Reservas (últimas 8 semanas)
+ Total vs. concluídas por semana
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Gráfico: Por dia da semana */}
+
+ Reservas por Dia da Semana
+ Distribuição histórica
+
+
+
+
+
+
+
+
+
+
+
+ {/* Gráfico: Serviços (Pizza) */}
+
+ Serviços Mais Populares
+ Distribuição por serviço
+ {topSvcsPie.length > 0 ? (
+
+
+
+ {topSvcsPie.map((_, index) => (
+ |
+ ))}
+
+ [v + ' reservas', n]} contentStyle={{ borderRadius: 8, border: '1px solid #e2e8f0', fontSize: 12 }} />
+
+
+
+ ) : (
+ Sem dados suficientes
+ )}
+
+
+
+ {/* Top Barbeiros */}
+ {topBarbers.length > 0 && (
+
+ Ranking de Profissionais
+
+ {topBarbers.map((b, i) => {
+ const maxQty = topBarbers[0].qty;
+ const pct = maxQty > 0 ? Math.round((b.qty / maxQty) * 100) : 0;
+ return (
+
+
#{i + 1}
+
{b.name}
+
+
{b.qty} reservas
+
+ );
+ })}
+
+
+ )}
+
+ );
+ })()}
+
{activeTab === 'appointments' && (