import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, CartesianGrid, } from "recharts"; import { DollarSign, CalendarCheck, TrendingUp, TrendingDown, Crown, Star, Scissors, Ticket as TicketIcon, Flame, Trophy, Coins, Wallet, } from "lucide-react"; import { api } from "../lib/api"; import { useAuth } from "../lib/auth"; import { PageHeader, Avatar, Spinner, EmptyState } from "../components/ui"; import { formatCurrency, formatNumber, formatDate, paymentLabel, cn, } from "../lib/format"; const RANGES = [ { value: "7", label: "7 días" }, { value: "30", label: "30 días" }, { value: "60", label: "60 días" }, ]; const PIE_COLORS = ["#3b66ff", "#f17616", "#10b981", "#a855f7", "#ec4899", "#14b8a6", "#f59e0b", "#6366f1"]; export function DashboardPage() { const { business } = useAuth(); const [range, setRange] = useState("30"); const symbol = business?.currency_symbol ?? "$"; const { data: overview, isLoading: ovLoading } = useQuery({ queryKey: ["dashboard", "overview", range], queryFn: () => api.dashboard.overview(Number(range)), }); const { data: emp } = useQuery({ queryKey: ["dashboard", "best-employees", range], queryFn: () => api.dashboard.bestEmployees(range), }); const { data: svc } = useQuery({ queryKey: ["dashboard", "best-services", range], queryFn: () => api.dashboard.bestServices(range), }); const { data: tickets } = useQuery({ queryKey: ["dashboard", "top-tickets", range], queryFn: () => api.dashboard.topTickets(range, 6), }); const { data: topClients } = useQuery({ queryKey: ["dashboard", "top-clients", range], queryFn: () => api.dashboard.topClients(range, 6), }); const { data: freqClients } = useQuery({ queryKey: ["dashboard", "frequent-clients", range], queryFn: () => api.dashboard.frequentClients(range, 6), }); const { data: trend } = useQuery({ queryKey: ["dashboard", "trend", range], queryFn: () => api.dashboard.revenueTrend(Number(range)), }); const { data: cats } = useQuery({ queryKey: ["dashboard", "categories", range], queryFn: () => api.dashboard.revenueByCategory(range), }); const { data: commissions } = useQuery({ queryKey: ["dashboard", "commissions", range], queryFn: () => api.dashboard.commissions(range), }); const trendData = (trend?.points ?? []).map((p) => ({ ...p, label: new Date(p.date).toLocaleDateString("es-MX", { day: "numeric", month: range === "7" ? "short" : undefined }), })); const totalCat = (cats?.slices ?? []).reduce((a, s) => a + s.revenue, 0) || 1; return (
{RANGES.map((r) => ( ))}
} />
{/* KPIs */}
{/* Revenue chart + Category pie */}

Ingresos del período

Tendencia diaria

Ingresos
{!trend ? (
) : ( `${symbol}${v >= 1000 ? `${(v / 1000).toFixed(0)}k` : v}`} /> [formatCurrency(Number(v), symbol), "Ingresos"]} labelStyle={{ fontWeight: 700, color: "#0f172a" }} /> )}

Por categoría

Distribución de ingresos

{!cats ? (
) : cats.slices.length === 0 ? ( ) : (
{cats.slices.map((_, i) => ( ))} [ `${formatCurrency(Number(v), symbol)} (${Math.round((Number(v) / totalCat) * 100)}%)`, p.payload.category, ]} />
{cats.slices.slice(0, 5).map((s, i) => (
{s.category} {formatCurrency(s.revenue, symbol)}
))}
)}
{/* Best employees + best services */}
({ id: r.employee.id, rank: i + 1, title: r.employee.name, subtitle: r.employee.role, value: formatCurrency(r.revenue, symbol), extra: `${r.appointments} citas · ⭐ ${r.avg_rating}`, color: r.employee.color, }))} /> ({ id: r.service.id, rank: i + 1, title: r.service.name, subtitle: r.service.category, value: formatCurrency(r.revenue, symbol), extra: `${r.count} ventas`, color: r.service.color, }))} />
{/* Top tickets */}

Tickets más altos

últimos {range} días
{!tickets ? (
) : tickets.tickets.length === 0 ? ( ) : ( <> {/* Mobile cards */}
{tickets.tickets.map(({ ticket }) => (
{ticket.client?.name}
{ticket.service?.name}
{ticket.employee?.name} · {paymentLabel(ticket.payment_method)}
{formatCurrency(ticket.amount + ticket.tip, symbol)}
))}
{/* Desktop table */}
{tickets.tickets.map(({ ticket }) => ( ))}
Cliente Servicio Especialista Pago Total
{ticket.client?.name}
{ticket.service?.name} {ticket.employee?.name} {paymentLabel(ticket.payment_method)} {formatCurrency(ticket.amount + ticket.tip, symbol)}
)}
{/* Top + frequent clients */}
({ id: r.client.id, rank: i + 1, title: r.client.name, subtitle: r.last_visit ? `Última visita ${formatDate(r.last_visit)}` : "—", value: formatCurrency(r.total_spent, symbol), extra: `${r.visits} visitas`, tags: r.client.tags, }))} /> ({ id: r.client.id, rank: i + 1, title: r.client.name, subtitle: r.last_visit ? `Última visita ${formatDate(r.last_visit)}` : "—", value: `${r.visits} visitas`, extra: formatCurrency(r.total_spent, symbol), tags: r.client.tags, }))} />
{/* Commissions */}

Comisiones del equipo

últimos {range} días
{!commissions ? (
) : (
Total comisiones
{formatCurrency(commissions.total, symbol)}
{(commissions.rows ?? []).slice(0, 6).map((r: any) => (
{r.employee.name}
{r.sales} ventas · {formatCurrency(r.revenue, symbol)}
{formatCurrency(r.commission, symbol)}
comisión
))}
)}
); } function KpiCard({ title, value, sub, delta, deltaSuffix, icon: Icon, color, }: { title: string; value: string; sub?: string; delta?: number; deltaSuffix?: string; icon: React.ComponentType<{ className?: string }>; color: string; }) { const positive = (delta ?? 0) >= 0; return (
{title}
{value}
{sub &&
{sub}
}
{delta !== undefined && (
{positive ? : } {positive ? "+" : ""}{delta}% {deltaSuffix}
)}
); } interface RankItem { id: number; rank: number; title: string; subtitle?: string; value: string; extra?: string; color?: string; tags?: string | null; } function RankCard({ title, subtitle, icon: Icon, iconColor, items, barMode, }: { title: string; subtitle?: string; icon: React.ComponentType<{ className?: string }>; iconColor: string; items: RankItem[]; barMode?: boolean; }) { const maxVal = Math.max(...items.map((i) => parseFloat(i.value.replace(/[^0-9.]/g, "")) || 0), 1); return (

{title}

{subtitle &&

{subtitle}

}
{items.length === 0 ? ( ) : (
{items.map((it) => (
{it.rank === 1 ? : it.rank}
{it.color && } {it.title} {it.tags?.split(",")[0] && ( {it.tags.split(",")[0]} )}
{it.value}
{it.subtitle && {it.subtitle}} {it.extra && {it.extra}}
{barMode && (
)}
))}
)}
); }