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
{!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 */}
| Cliente |
Servicio |
Especialista |
Pago |
Total |
{tickets.tickets.map(({ ticket }) => (
|
|
{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 && (
)}
))}
)}
);
}