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' && (