import { useEffect, useState } from "react"; // ─── Types ──────────────────────────────────────────────────────────────────── interface Summary { from: string; to: string; revenue: { totalCents: number; paidInvoices: number }; appointments: { total: number; completed: number; cancelled: number; noShow: number }; clients: { total: number; new: number }; } interface RevenuePeriod { period: string; totalCents: number; invoiceCount: number; } interface RevenueByGroomer { staffId: string; staffName: string; totalCents: number; invoiceCount: number; } interface RevenueReport { byPeriod: RevenuePeriod[]; byGroomer: RevenueByGroomer[]; } interface ApptPeriod { period: string; total: number; completed: number; cancelled: number; noShow: number; } interface ServiceRow { serviceId: string; serviceName: string; appointmentCount: number; completedCount: number; revenueCents: number; } interface ChurnClient { clientId: string; clientName: string; lastAppointmentAt: string | null; } interface ClientReport { newClients: { clientId: string; clientName: string; createdAt: string }[]; activeInPeriodCount: number; churnRisk: ChurnClient[]; churnRiskTotal: number; } // ─── Helpers ────────────────────────────────────────────────────────────────── function fmtMoney(cents: number) { return `$${(cents / 100).toFixed(2)}`; } function fmtDate(iso: string | null) { if (!iso) return "Never"; return new Date(iso).toLocaleDateString(); } function toInputDate(d: Date): string { return d.toISOString().slice(0, 10); } function buildQuery(from: string, to: string, extra: Record = {}) { const params = new URLSearchParams({ from: `${from}T00:00:00Z`, to: `${to}T23:59:59Z`, ...extra }); return params.toString(); } // ─── Sub-components ─────────────────────────────────────────────────────────── function StatCard({ label, value, sub }: { label: string; value: string; sub?: string }) { return (
{label}
{value}
{sub &&
{sub}
}
); } function SectionHeader({ children }: { children: React.ReactNode }) { return (

{children}

); } function Table({ headers, rows }: { headers: string[]; rows: (string | number)[][] }) { return (
{headers.map((h) => ( ))} {rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))} {rows.length === 0 && ( )}
{h}
{cell}
No data for this period.
); } // ─── Main Page ──────────────────────────────────────────────────────────────── export function ReportsPage() { const today = new Date(); const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(today.getDate() - 30); const [fromDate, setFromDate] = useState(toInputDate(thirtyDaysAgo)); const [toDate, setToDate] = useState(toInputDate(today)); const [groupBy, setGroupBy] = useState<"day" | "week" | "month">("day"); const [summary, setSummary] = useState(null); const [revenue, setRevenue] = useState(null); const [apptTrends, setApptTrends] = useState([]); const [services, setServices] = useState([]); const [clientReport, setClientReport] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function loadAll() { setLoading(true); setError(null); try { const qs = buildQuery(fromDate, toDate); const qsGroup = buildQuery(fromDate, toDate, { groupBy }); const [summRes, revRes, apptRes, svcRes, clientRes] = await Promise.all([ fetch(`/api/reports/summary?${qs}`), fetch(`/api/reports/revenue?${qsGroup}`), fetch(`/api/reports/appointments?${qsGroup}`), fetch(`/api/reports/services?${qs}`), fetch(`/api/reports/clients?${qs}`), ]); const failures = [ ["summary", summRes], ["revenue", revRes], ["appointments", apptRes], ["services", svcRes], ["clients", clientRes], ].filter(([, r]) => !(r as Response).ok); if (failures.length > 0) { const details = await Promise.all( failures.map(async ([name, r]) => { const res = r as Response; let body = ""; try { body = await res.text(); } catch { /* ignore */ } return `${name} (HTTP ${res.status}${body ? `: ${body.slice(0, 120)}` : ""})`; }) ); throw new Error(`Failed to load report data — ${details.join(", ")}`); } const [summData, revData, apptData, svcData, clientData] = await Promise.all([ summRes.json() as Promise, revRes.json() as Promise<{ byPeriod: RevenuePeriod[]; byGroomer: RevenueByGroomer[] }>, apptRes.json() as Promise<{ byPeriod: ApptPeriod[] }>, svcRes.json() as Promise<{ rows: ServiceRow[] }>, clientRes.json() as Promise, ]); setSummary(summData); setRevenue(revData); setApptTrends(apptData.byPeriod); setServices(svcData.rows); setClientReport(clientData); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Unknown error"); } finally { setLoading(false); } } useEffect(() => { loadAll(); }, []); // run on mount only function exportCsv(type: "revenue" | "appointments" | "services") { const qs = buildQuery(fromDate, toDate, { type }); window.open(`/api/reports/export.csv?${qs}`, "_blank"); } const completionRate = summary && summary.appointments.total > 0 ? Math.round((summary.appointments.completed / summary.appointments.total) * 100) : 0; const noShowRate = summary && summary.appointments.total > 0 ? Math.round((summary.appointments.noShow / summary.appointments.total) * 100) : 0; return (
{/* ── Controls ── */}

Reports

{error &&

{error}

} {/* ── KPI Cards ── */} {summary && (
)} {/* ── Revenue by Period ── */} Revenue by {groupBy.charAt(0).toUpperCase() + groupBy.slice(1)} [ fmtDate(r.period), r.invoiceCount, fmtMoney(r.totalCents), ])} /> {/* ── Revenue by Groomer ── */} Revenue by Groomer
[ r.staffName, r.invoiceCount, fmtMoney(r.totalCents), ])} /> {/* ── Appointment Trends ── */} Appointment Trends by {groupBy.charAt(0).toUpperCase() + groupBy.slice(1)}
[ fmtDate(r.period), r.total, r.completed, r.cancelled, r.noShow, ])} /> {/* ── Service Popularity ── */} Service Popularity
[ r.serviceName, r.appointmentCount, r.completedCount, fmtMoney(r.revenueCents), ])} /> {/* ── Client Retention ── */} Client Retention {clientReport && (
)} Churn Risk — Clients Without a Visit in 90+ Days
[ r.clientName, fmtDate(r.lastAppointmentAt), ])} /> {clientReport && clientReport.churnRiskTotal > 20 && (

Showing top 20 of {clientReport.churnRiskTotal} at-risk clients.

)} ); } // ─── Shared styles ──────────────────────────────────────────────────────────── const btnStyle: React.CSSProperties = { padding: "0.4rem 0.85rem", border: "1px solid #d1d5db", borderRadius: 6, background: "#fff", cursor: "pointer", fontSize: 13, fontWeight: 500, }; const inputStyle: React.CSSProperties = { padding: "0.35rem 0.5rem", border: "1px solid #d1d5db", borderRadius: 6, fontSize: 13, marginLeft: "0.25rem", };