"use client";

/**
 * Reports (PRD 8), on the Medora design system and the Reports API plan (DECISIONS.md, 26 Oct 2026). The design's
 * report filter bar, report table, charts, expandable sales, insights and general report, in the PRD's seven
 * sections. Every table here is described once (`View`), and the same description is what Excel, PDF and CSV export,
 * so an export holds exactly the rows and totals on screen (RP-01). Cost and margin arrive only for roles that see cost
 * prices (Q1); columns without data aren't drawn.
 *
 * Not built: branch inventory totals and branch comparison (RP-03/04, stock isn't kept per branch yet, Q3), invoice
 * sales (no invoicing yet, Q4), background exports for very large reports.
 */

import * as React from "react";
import { useRouter } from "next/navigation";
import { ChevronDownIcon, RotateCcwIcon } from "lucide-react";

import { MedoraPageHeader } from "@/components/medora/page-header";
import { ReportFilterBar, ReportTable, ChartCard, type ReportColumn, type ReportFilters } from "@/components/medora/records/reports";
import { ExpandableSales, MonthlyTrend, PeakHeatmap, RxOtcSplit, PAY_METHODS, type PayMethod, type SaleDetail } from "@/components/medora/sales-reports";
import { CategoryMarginChart, MethodSplit, PeriodTrend, PLWaterfall } from "@/components/medora/report-charts";
import { InsightCards, InsightNarrative, MonthlyComparison, type InsightBucket, type Narrative } from "@/components/medora/insights";
import { GeneralReport } from "@/components/medora/reports-more";
import { TopCustomers } from "@/components/medora/customer-reports";
import { BranchTotals } from "@/components/medora/catalog-ops";
import { BranchComparison, GoodsReturnsReport, PurchaseAgeing } from "@/components/medora/inventory-reports";
import { ConfirmDialog } from "@/components/medora/confirm-dialog";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "@/components/ui/toast";
import { BranchesApi, CustomersApi, DashboardApi, ExpensesApi, ExportsApi, LedgerApi, ProductsApi, ReportsApi, SalesApi, SuppliersApi, type ReportSaleRow } from "@/lib/api";
import { useTabParam } from "@/lib/use-tab-param";
import { formatDate, formatKES, localToday, addDays } from "@/lib/format";
import { useMenuPermissions } from "@/lib/permissions";
import { Surface } from "@/components/medora/surface";
import { Loading } from "@/components/medora/loading";

const apiError = (e: unknown, fallback: string): string => {
  const m = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data?.message;
  return Array.isArray(m) ? m.join(" ") : m || fallback;
};
const n0 = (n: number) => Math.round(n).toLocaleString("en-US");
const pct = (n: number) => `${n.toFixed(1)}%`;
const time = (iso: string) => new Date(iso).toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });

/* ------------------------------------ One description per report (RP-01) ------------------------------------ */

type Kind = "text" | "money" | "number" | "percent" | "date" | "datetime";
type Col = { key: string; header: string; kind?: Kind; total?: number | string; render?: (r: Row) => React.ReactNode };
type Row = Record<string, unknown> & { _key: string | number };
type View = { title: string; summary?: { label: string; value: string }[]; note?: React.ReactNode; top?: React.ReactNode; table?: { columns: Col[]; rows: Row[] }; bottom?: React.ReactNode; exportable?: boolean };

const fmt = (kind: Kind | undefined, v: unknown): string => {
  if (v == null || v === "") return "";
  if (kind === "money") return formatKES(Number(v) || 0, { decimals: true });
  if (kind === "number") return n0(Number(v));
  if (kind === "percent") return pct(Number(v));
  if (kind === "date") return formatDate(String(v).slice(0, 10));
  if (kind === "datetime") return time(String(v));
  return String(v);
};
const numeric = (k?: Kind) => k === "money" || k === "number" || k === "percent";
const toReportColumns = (cols: Col[]): ReportColumn<Row>[] =>
  cols.map((c) => ({ key: c.key, header: c.header, numeric: numeric(c.kind), cell: (r) => (c.render ? c.render(r) : fmt(c.kind, r[c.key])), total: c.total === undefined ? undefined : typeof c.total === "number" ? fmt(c.kind, c.total) : c.total }));
/** Columns whose data the API sent (cost and margin are left out for roles that don't see cost). */
const present = (cols: (Col | false | null | undefined)[], rows: Row[]) => (cols.filter(Boolean) as Col[]).filter((c) => c.render || !rows.length || rows.some((r) => r[c.key] !== undefined));
const sum = (rows: Row[], key: string) => rows.reduce((t, r) => t + (Number(r[key]) || 0), 0);

function exportPayload(v: View, period: string, branch: string) {
  const t = v.table;
  return {
    title: v.title,
    subtitle: period,
    meta: [branch],
    sheets: [
      {
        name: v.title.slice(0, 31),
        columns: (t?.columns ?? []).map((c) => ({ key: c.key, label: c.header, type: (c.kind === "money" ? "money" : c.kind === "number" ? "number" : "text") as "money" | "number" | "text" })),
        rows: (t?.rows ?? []).map((r) => Object.fromEntries((t?.columns ?? []).map((c) => [c.key, c.kind === "money" || c.kind === "number" ? Number(r[c.key]) || 0 : fmt(c.kind, r[c.key])]))),
        summary: v.summary,
      },
    ],
  };
}

/* ------------------------------------ Sections (PRD 8.1–8.7) ------------------------------------ */

type ReportId =
  | "sales" | "activity" | "daily" | "daily-items" | "sold-items" | "tenders" | "card" | "mpesa" | "stk" | "users" | "vat" | "etims" | "archived"
  | "stock" | "closing" | "dead" | "branch-totals" | "branch-compare"
  | "customers" | "suppliers" | "purchases" | "goods-returns" | "ageing" | "summary" | "monthly" | "insights" | "general" | "pl";

/** `need`: the report also needs Read on one of these pages (its API route's), or it isn't offered. */
const SECTIONS: { id: string; label: string; page: string; reports: { id: ReportId; label: string; dates?: boolean; need?: string[] }[] }[] = [
  {
    id: "sales", label: "Sales", page: "Reports/Sales reports",
    reports: [
      { id: "sales", label: "Sales" }, { id: "activity", label: "Returns and voids" }, { id: "daily", label: "Daily sales" }, { id: "daily-items", label: "Daily item sales" },
      { id: "sold-items", label: "Sold items" }, { id: "tenders", label: "Payment breakdown" }, { id: "card", label: "Card payments" }, { id: "mpesa", label: "M-Pesa payments" },
      { id: "stk", label: "M-Pesa prompts" }, { id: "users", label: "Users' sales" }, { id: "vat", label: "VAT" }, { id: "etims", label: "eTIMS" }, { id: "archived", label: "Archived sales" },
    ],
  },
  { id: "inventory", label: "Inventory", page: "Reports/Stock reports", reports: [{ id: "stock", label: "Stock", dates: false }, { id: "closing", label: "Stock as at a date", dates: false }, { id: "dead", label: "Dead stock" }, { id: "branch-totals", label: "Stock by branch", dates: false }, { id: "branch-compare", label: "Branch comparison" }] },
  { id: "customers", label: "Customers", page: "Reports/Sales reports", reports: [{ id: "customers", label: "Customers" }] },
  { id: "suppliers", label: "Suppliers", page: "Reports/Sales reports", reports: [{ id: "suppliers", label: "Suppliers" }, { id: "purchases", label: "Product purchases" }, { id: "goods-returns", label: "Goods returns", need: ["Suppliers/Supplier returns", "Inventory/Purchase orders"] }, { id: "ageing", label: "Purchase ageing", dates: false, need: ["Accounts/Payables", "Inventory/Purchase orders"] }] },
  { id: "summary", label: "Sales summary", page: "Reports/Sales reports", reports: [{ id: "summary", label: "Overview" }, { id: "monthly", label: "Monthly comparison", dates: false }] },
  { id: "insights", label: "Stock insights", page: "Reports/Stock reports", reports: [{ id: "insights", label: "Stock insights", dates: false }] },
  { id: "system", label: "System", page: "Reports/Sales reports", reports: [{ id: "general", label: "General report" }, { id: "pl", label: "Profit and loss" }] },
];

export default function ReportsPage() {
  const { can, loaded } = useMenuPermissions();
  const today = localToday();
  const [filters, setFilters] = React.useState<ReportFilters>({ from: addDays(today, -29), to: today, branch: "all" });
  const [branches, setBranches] = React.useState<{ id: number; name: string }[]>([]);
  const allowed = SECTIONS.filter((s) => loaded && (can(s.page, "Read") || (s.id === "suppliers" && can("Suppliers/Supplier list", "Read"))));
  const [sectionParam, setSection] = useTabParam(); // page tabs change the URL (tabs spec 7.1)
  // A ?tab= that is not a section (an old link, another page's tab) falls back to Sales instead of failing
  const section = SECTIONS.some((s) => s.id === sectionParam) ? (sectionParam as string) : "sales";
  const [report, setReport] = React.useState<ReportId>("sales");
  const [view, setView] = React.useState<View | null>(null);
  const [exporting, setExporting] = React.useState<"excel" | "pdf" | "csv" | null>(null);
  const allBranches = loaded && can("Reports/Sales reports", "All branches");
  const offered = (s: (typeof SECTIONS)[number]) => s.reports.filter((x) => !x.need || x.need.some((pg) => can(pg, "Read")));
  const canExport = loaded && (can("Reports/Sales reports", "Export") || can("Reports/Stock reports", "Export"));

  React.useEffect(() => {
    if (allBranches) BranchesApi.getAll().then((b: { id: number; name: string }[]) => setBranches(b.map((x) => ({ id: x.id, name: x.name })))).catch(() => undefined);
  }, [allBranches]);
  React.useEffect(() => {
    if (loaded && allowed.length && !allowed.some((s) => s.id === section)) {
      setSection(allowed[0].id);
      setReport(allowed[0].reports[0].id);
    }
  }, [loaded, allowed, section, setSection]);

  const sec = SECTIONS.find((s) => s.id === section)!;
  // The report on screen is always one this section offers (a deep link to a section opens its first report)
  const shown = offered(sec).some((r) => r.id === report) ? report : (offered(sec)[0]?.id ?? sec.reports[0].id);
  const branchId = filters.branch !== "all" ? Number(filters.branch) : null;
  const branchName = branchId ? (branches.find((b) => b.id === branchId)?.name ?? "One branch") : allBranches ? "All branches" : "Your branch";
  const periodLabel = `${formatDate(filters.from)} to ${formatDate(filters.to)}`;
  const doExport = async (kind: "excel" | "pdf" | "csv") => {
    if (!view?.table) return toast.add({ type: "warning", title: "Nothing to export here", description: "This report has no table to export." });
    setExporting(kind);
    try {
      const payload = exportPayload(view, periodLabel, branchName);
      if (kind === "excel") await ExportsApi.excel(payload);
      else if (kind === "pdf") await ExportsApi.pdf(payload);
      else await ExportsApi.csvReport(payload);
    } catch (e) {
      toast.add({ type: "error", title: "Export failed", description: apiError(e, "Try again.") });
    } finally {
      setExporting(null);
    }
  };

  if (loaded && !allowed.length) return <div className="medora mx-auto w-full max-w-6xl py-10 text-center text-sm text-muted-foreground">Your role can&apos;t see any reports.</div>;

  const pickSection = (id: string) => {
    const s = SECTIONS.find((x) => x.id === id)!;
    setSection(s.id);
    setReport(offered(s)[0].id);
    setView(null);
  };
  return (
    <div className="medora mx-auto w-full max-w-7xl space-y-4">
      <MedoraPageHeader title="Reports" description="Sales, stock, customers and suppliers, by period and branch." />
      {/* R4 (card 037): at most five section tabs, the rest under More; R3: the report is picked on the filter row */}
      <Tabs value={section} onValueChange={(v) => pickSection(String(v))}>
        <TabsList variant="line" className="w-full justify-start border-b border-border">
          {allowed.slice(0, 5).map((s) => (
            <TabsTrigger key={s.id} value={s.id}>
              {s.label}
            </TabsTrigger>
          ))}
          {allowed.length > 5 ? (
            <DropdownMenu>
              <DropdownMenuTrigger
                render={<Button variant="ghost" size="sm" className={allowed.slice(5).some((s) => s.id === section) ? "font-medium text-foreground underline decoration-2 underline-offset-[14px]" : "text-foreground/60"} />}
              >
                {allowed.slice(5).find((s) => s.id === section)?.label ?? "More"}
                <ChevronDownIcon data-icon="inline-end" />
              </DropdownMenuTrigger>
              <DropdownMenuContent align="start">
                {allowed.slice(5).map((s) => (
                  <DropdownMenuItem key={s.id} onClick={() => pickSection(s.id)}>
                    {s.label}
                  </DropdownMenuItem>
                ))}
              </DropdownMenuContent>
            </DropdownMenu>
          ) : null}
        </TabsList>
      </Tabs>
      {offered(sec).length > 1 ? (
        <div role="group" aria-label={`${sec.label} reports`} className="flex flex-wrap gap-1.5">
          {offered(sec).map((r) => (
            <Button key={r.id} size="sm" variant={shown === r.id ? "default" : "outline"} onClick={() => { setReport(r.id); setView(null); }}>
              {r.label}
            </Button>
          ))}
        </div>
      ) : null}
      <ReportFilterBar today={today} branches={allBranches ? branches : []} exporting={exporting} onChange={setFilters} onExport={canExport ? (k) => void doExport(k) : undefined} />
      {sec.reports.find((r) => r.id === shown)?.dates === false ? <p className="-mt-2 text-xs text-muted-foreground">This report doesn&apos;t use the period: it shows the position now, or as at the date it asks for.</p> : null}
      <Report id={shown} from={filters.from} to={filters.to} branchId={branchId} onView={setView} />
    </div>
  );
}

/* ------------------------------------ The reports ------------------------------------ */

function Report({ id, from, to, branchId, onView }: { id: ReportId; from: string; to: string; branchId: number | null; onView: (v: View | null) => void }) {
  const router = useRouter();
  const [v, setV] = React.useState<View | null>(null);
  const [state, setState] = React.useState<"loading" | "ready" | "error">("loading");
  const [error, setError] = React.useState<string>();
  const [version, setVersion] = React.useState(0);
  const [asOf, setAsOf] = React.useState(localToday());
  const [activityPage, setActivityPage] = React.useState(1);
  const [restore, setRestore] = React.useState<{ id: number; receipt: string } | null>(null);
  const p = React.useMemo(() => ({ from, to, branchId }), [from, to, branchId]);
  const show = React.useCallback(
    (x: View) => {
      setV(x);
      onView(x);
      setState("ready");
    },
    [onView],
  );

  React.useEffect(() => {
    let live = true;
    setState("loading");
    onView(null);
    const run = async (): Promise<View> => {
      switch (id) {
        case "sales":
        case "activity": {
          const d = await ReportsApi.sales({ ...p, activity: id === "activity", page: activityPage, pageSize: 50 });
          const rows: Row[] = d.rows.map((s) => ({ _key: s.saleId, receipt: s.receipt, at: s.at, customer: s.customer, cashier: s.cashier, total: s.total, returned: s.returns.reduce((t, r) => t + r.amount, 0), status: s.voided ? "Voided" : s.returns.length ? "Returned" : "Completed" }));
          return {
            title: id === "activity" ? "Sales with returns or voids" : "Sales",
            summary: [{ label: "Sales", value: n0(d.total) }],
            top: <ExpandableSales sales={d.rows.map(toSaleDetail)} />,
            bottom: <Pager page={d.page} pageSize={d.pageSize} total={d.total} onPage={setActivityPage} />,
            table: { columns: [{ key: "receipt", header: "Receipt" }, { key: "at", header: "Time", kind: "datetime" }, { key: "customer", header: "Customer" }, { key: "cashier", header: "Cashier" }, { key: "status", header: "Status" }, { key: "returned", header: "Returned", kind: "money" }, { key: "total", header: "Total", kind: "money", total: sum(rows, "total") }], rows },
            exportable: true,
          };
        }
        case "daily": {
          const d = await ReportsApi.dailySales(p);
          const methods = [...new Set(d.rows.flatMap((r) => Object.keys(r.byMethod)))];
          const rows: Row[] = d.rows.map((r) => ({ _key: r.date, ...r, ...Object.fromEntries(methods.map((m) => [`m_${m}`, r.byMethod[m] ?? 0])) }));
          return {
            title: "Daily sales",
            summary: [{ label: "Gross", value: formatKES(d.totals.gross) }, { label: "Net of VAT", value: formatKES(d.totals.net) }, { label: "VAT", value: formatKES(d.totals.vat) }, { label: "Receipts", value: n0(d.totals.receipts) }],
            table: {
              columns: [
                { key: "date", header: "Day", kind: "date" }, { key: "receipts", header: "Receipts", kind: "number", total: sum(rows, "receipts") },
                ...methods.map((m) => ({ key: `m_${m}`, header: m, kind: "money" as Kind, total: sum(rows, `m_${m}`) })),
                { key: "vat", header: "VAT", kind: "money", total: sum(rows, "vat") }, { key: "returns", header: "Returns", kind: "money", total: sum(rows, "returns") }, { key: "gross", header: "Gross", kind: "money", total: sum(rows, "gross") },
              ],
              rows,
            },
          };
        }
        case "daily-items": {
          const d = await ReportsApi.dailyItems(p);
          const rows: Row[] = d.rows.map((r) => ({ _key: `${r.date}-${r.productId}`, ...r }));
          return { title: "Daily item sales", table: { columns: [{ key: "date", header: "Day", kind: "date" }, { key: "product", header: "Item" }, { key: "qty", header: "Units", kind: "number", total: sum(rows, "qty") }, { key: "revenue", header: "Revenue before VAT", kind: "money", total: sum(rows, "revenue") }], rows } };
        }
        case "sold-items": {
          const d = await ReportsApi.soldItems(p);
          const rows: Row[] = d.rows.map((r) => ({ _key: r.productId, ...r, rxLabel: r.rx ? "Rx" : "OTC" }));
          const cols = present([{ key: "product", header: "Item" }, { key: "category", header: "Category" }, { key: "rxLabel", header: "Rx or OTC" }, { key: "qty", header: "Units", kind: "number", total: sum(rows, "qty") }, { key: "receipts", header: "Receipts", kind: "number" }, { key: "revenue", header: "Revenue before VAT", kind: "money", total: sum(rows, "revenue") }, { key: "cost", header: "Cost", kind: "money", total: sum(rows, "cost") }, { key: "margin", header: "Margin", kind: "percent" }], rows);
          return { title: "Sold items", summary: [{ label: "Products", value: n0(d.summary.products) }, { label: "Units", value: n0(d.summary.qty) }, { label: "Revenue before VAT", value: formatKES(d.summary.revenue) }, ...(d.summary.cost !== undefined ? [{ label: "Gross margin", value: d.summary.revenue ? pct(((d.summary.revenue - d.summary.cost) / d.summary.revenue) * 100) : "–" }] : [])], table: { columns: cols, rows } };
        }
        case "tenders": {
          const d = await ReportsApi.summary(p);
          const rows: Row[] = d.byTender.map((t) => ({ _key: t.tender, ...t, share: d.totals.gross ? (t.amount / d.totals.gross) * 100 : 0 }));
          return { title: "Payment breakdown", top: <ChartCard title="By payment method" description="Money taken by tender; credit left unpaid is shown as On account, so the methods add up to gross sales."><MethodSplit rows={d.byTender.map((t) => ({ method: t.tender, amount: t.amount }))} /></ChartCard>, table: { columns: [{ key: "tender", header: "Method" }, { key: "receipts", header: "Receipts", kind: "number" }, { key: "share", header: "Share", kind: "percent" }, { key: "amount", header: "Amount", kind: "money", total: sum(rows, "amount") }], rows } };
        }
        case "card":
        case "mpesa": {
          const method = id === "card" ? "Card" : "M-Pesa";
          const d = await ReportsApi.payments(method, p);
          const rows: Row[] = d.rows.map((r) => ({ _key: r.id, ...r }));
          return { title: `${method} payments`, summary: [{ label: "Payments", value: n0(d.summary.payments) }, { label: "Amount", value: formatKES(d.summary.amount) }], table: { columns: [{ key: "at", header: "Time", kind: "datetime" }, { key: "receipt", header: "Receipt" }, { key: "reference", header: method === "Card" ? "Card reference" : "M-Pesa code" }, { key: "cashier", header: "Cashier" }, { key: "branch", header: "Branch" }, { key: "amount", header: "Amount", kind: "money", total: sum(rows, "amount") }], rows } };
        }
        case "stk": {
          const d = await ReportsApi.mpesaLog(p);
          const rows: Row[] = d.rows.map((r) => ({ _key: r.id, ...r, outcome: r.status === "SUCCESS" ? "Paid" : r.status === "FAILED" ? "Not paid" : "Waiting", saleRef: r.sale ?? (r.status === "SUCCESS" ? "Not used on a sale" : "") }));
          return { title: "M-Pesa prompts", summary: [{ label: "Prompts", value: n0(d.summary.prompts) }, { label: "Paid", value: `${d.summary.paid} (${formatKES(d.summary.paidAmount)})` }, { label: "Not paid", value: n0(d.summary.failed) }, { label: "Paid but not on a sale", value: n0(d.summary.paidNoSale) }], table: { columns: [{ key: "at", header: "Time", kind: "datetime" }, { key: "phone", header: "Phone" }, { key: "outcome", header: "Outcome" }, { key: "result", header: "M-Pesa said" }, { key: "receipt", header: "M-Pesa code" }, { key: "saleRef", header: "Sale" }, { key: "amount", header: "Amount", kind: "money" }], rows } };
        }
        case "users": {
          const d = await ReportsApi.users(p);
          const rows: Row[] = d.rows.map((r) => ({ _key: r.userId, ...r }));
          return { title: "Users' sales", note: "Rung up: sales they processed at the till. Sold: sales credited to them as seller (the cashier when no seller was chosen).", table: { columns: [{ key: "name", header: "User" }, { key: "role", header: "Role" }, { key: "rungUp", header: "Rung up", kind: "number" }, { key: "rungUpTotal", header: "Rung up value", kind: "money", total: sum(rows, "rungUpTotal") }, { key: "sold", header: "Sold", kind: "number" }, { key: "soldTotal", header: "Sold value", kind: "money", total: sum(rows, "soldTotal") }, { key: "returns", header: "Returns", kind: "number" }, { key: "refunded", header: "Refunded", kind: "money", total: sum(rows, "refunded") }], rows } };
        }
        case "vat": {
          const d = await SalesApi.vatReport(from, to, branchId);
          const rows: Row[] = (d.byTaxCode?.length ? d.byTaxCode : d.byRate ?? []).map((r: { code?: string; rate?: number; transactions: number; taxable: number; vat: number; gross: number }) => ({ _key: r.code ?? r.rate ?? "", band: r.code ? `Tax code ${r.code}` : `${r.rate}%`, ...r }));
          return { title: "VAT", summary: [{ label: "Taxable sales", value: formatKES(d.summary.taxableSales) }, { label: "Zero-rated or exempt", value: formatKES(d.summary.zeroRatedSales) }, { label: "VAT collected", value: formatKES(d.summary.vatCollected) }, { label: "Returns (not netted off)", value: formatKES(d.returns?.gross ?? 0) }], note: "VAT on returns isn't netted off yet: returned sales are shown apart.", table: { columns: [{ key: "band", header: "Band" }, { key: "transactions", header: "Sales", kind: "number", total: sum(rows, "transactions") }, { key: "taxable", header: "Taxable", kind: "money", total: sum(rows, "taxable") }, { key: "vat", header: "VAT", kind: "money", total: sum(rows, "vat") }, { key: "gross", header: "Gross", kind: "money", total: sum(rows, "gross") }], rows } };
        }
        case "etims": {
          const d = await ReportsApi.nonEtims(p);
          const rows: Row[] = d.rows.map((r) => ({ _key: r.saleId, ...r }));
          return { title: "Sales not sent to eTIMS", summary: [{ label: "Not sent to eTIMS", value: `${n0(d.summary.sales)} sales, ${formatKES(d.summary.total)}` }, { label: "Share of gross sales", value: pct(d.summary.shareOfGross) }, { label: "VAT on them", value: formatKES(d.summary.vat) }], note: "Sending sales to KRA eTIMS is part of Integrations (IG-09), not built yet.", table: { columns: [{ key: "at", header: "Time", kind: "datetime" }, { key: "receipt", header: "Receipt" }, { key: "cashier", header: "Cashier" }, { key: "method", header: "Paid by" }, { key: "vat", header: "VAT", kind: "money", total: sum(rows, "vat") }, { key: "total", header: "Total", kind: "money", total: sum(rows, "total") }], rows } };
        }
        case "archived": {
          const d: { id: number; receiptId: string; createdAt: string; total: number; cashier?: { name?: string } }[] = await SalesApi.getAll(from, to, branchId, true, true);
          const rows: Row[] = d.map((s) => ({ _key: s.id, id: s.id, receipt: s.receiptId, at: s.createdAt, cashier: s.cashier?.name ?? "", total: Number(s.total) }));
          return { title: "Archived sales", note: "Voided sales, kept for the record. Restoring one counts it as a sale again (and posts it to the books if they have started).", table: { columns: [{ key: "at", header: "Time", kind: "datetime" }, { key: "receipt", header: "Receipt" }, { key: "cashier", header: "Cashier" }, { key: "total", header: "Total", kind: "money", total: sum(rows, "total") }, { key: "act", header: "", render: (r) => <Button size="xs" variant="outline" onClick={() => setRestore({ id: Number(r.id), receipt: String(r.receipt) })}><RotateCcwIcon data-icon="inline-start" />Restore</Button> }], rows } };
        }
        case "stock": {
          // Plan O: the report's branch filter (a user kept to a branch gets their own whatever is sent)
          const d = await ProductsApi.stockReport(undefined, branchId);
          const rows: Row[] = d.products.map((x: Record<string, unknown>) => ({ _key: x.id as number, ...x, expiry: x.earliestExpiry }));
          const cols = present([{ key: "name", header: "Item" }, { key: "category", header: "Category" }, { key: "stock", header: "In stock", kind: "number", total: sum(rows, "stock") }, { key: "status", header: "Status" }, { key: "expiry", header: "Earliest expiry", kind: "date" }, { key: "costValue", header: "Value at cost", kind: "money", total: sum(rows, "costValue") }, { key: "retailValue", header: "Value at selling price", kind: "money", total: sum(rows, "retailValue") }, { key: "marginPct", header: "Margin", kind: "percent" }], rows);
          const s = d.summary;
          return { title: "Stock", summary: [{ label: "Products", value: n0(s.products) }, { label: "Out of stock", value: n0(s.outOfStock) }, { label: "Low stock", value: n0(s.lowStock) }, { label: "Units", value: n0(s.totalUnits) }, ...(s.costValue !== undefined ? [{ label: "Value at cost", value: formatKES(s.costValue) }] : []), { label: "Value at selling price", value: formatKES(s.retailValue) }, { label: "Expiring within 90 days", value: `${n0(s.expiringUnits)} units` }], note: d.branchId != null ? "Valued from each batch, as it stands now. This branch only, not counting stock in transit to it." : "Valued from each batch, as it stands now, for every branch, including stock in transit.", table: { columns: cols, rows } };
        }
        case "closing": {
          const d = await ProductsApi.stockClosing(asOf);
          const rows: Row[] = d.products.map((x: Record<string, unknown>) => ({ _key: x.id as number, ...x, ok: x.reconciles ? "" : "Doesn't add up" }));
          return {
            title: `Stock as at ${formatDate(asOf)}`,
            top: (
              <Surface className="w-fit">
                <Field className="w-48">
                  <FieldLabel htmlFor="rp-asof">Stock as at</FieldLabel>
                  <Input id="rp-asof" type="date" value={asOf} max={localToday()} onChange={(e) => e.target.value && setAsOf(e.target.value)} />
                </Field>
              </Surface>
            ),
            note: `Worked back from today's stock over sales, receipts, returns and adjustments since then.${d.caveats?.adjustmentsCoverCutoff === false ? " Manual adjustments were only recorded from " + formatDate(d.caveats.adjustmentsSince) + ", so earlier dates may be off." : ""}`,
            table: { columns: present([{ key: "name", header: "Item" }, { key: "closingStock", header: "Closing stock", kind: "number", total: sum(rows, "closingStock") }, { key: "currentStock", header: "Now", kind: "number" }, { key: "soldAfter", header: "Sold since", kind: "number" }, { key: "receivedAfter", header: "Received since", kind: "number" }, { key: "closingValue", header: "Value at today's cost", kind: "money", total: sum(rows, "closingValue") }, { key: "ok", header: "" }], rows), rows },
          };
        }
        case "dead": {
          const d = await ReportsApi.deadStock(p);
          const rows: Row[] = d.rows.map((r) => ({ _key: r.productId, ...r, lastSoldLabel: r.lastSold ? formatDate(r.lastSold) : "Never" }));
          return { title: "Dead stock", summary: [{ label: "Items not selling", value: n0(d.summary.items) }, { label: "Units", value: n0(d.summary.qty) }, ...(d.summary.costValue !== undefined ? [{ label: "Value at cost", value: formatKES(d.summary.costValue) }] : []), { label: "Value at selling price", value: formatKES(d.summary.retailValue) }], note: d.note, table: { columns: present([{ key: "name", header: "Item" }, { key: "category", header: "Category" }, { key: "qty", header: "In stock", kind: "number", total: sum(rows, "qty") }, { key: "lastSoldLabel", header: "Last sold" }, { key: "costValue", header: "Value at cost", kind: "money", total: sum(rows, "costValue") }, { key: "retailValue", header: "Value at selling price", kind: "money", total: sum(rows, "retailValue") }], rows), rows } };
        }
        // Plan B: RP-03, RP-04 (stock per branch, Phase 1.2) and IN-19
        case "branch-totals": {
          const d = await ReportsApi.branchTotals(branchId ?? undefined);
          const seesCost = d.total.cost !== undefined;
          const rows: Row[] = [...d.rows.map((x) => ({ _key: x.branchId, ...x })), ...(d.inTransit ? [{ _key: "transit", branch: "In transit", ...d.inTransit }] : [])].map((x) => ({ ...x, margin: x.cost !== undefined && x.selling ? Math.round(((x.selling - x.cost) / x.selling) * 1000) / 10 : undefined }));
          return {
            title: "Stock by branch",
            summary: [{ label: "Units", value: n0(d.total.units) }, ...(seesCost ? [{ label: "Value at cost", value: formatKES(d.total.cost!) }] : []), { label: "Value at selling price", value: formatKES(d.total.selling) }],
            note: d.inTransit ? "Stock in transit between branches is at neither branch yet; it is its own row, so the total matches the pharmacy's stock." : undefined,
            top: seesCost ? <BranchTotals rows={rows.map((x) => ({ branch: String(x.branch), items: Number(x.items), units: Number(x.units), cost: Number(x.cost), selling: Number(x.selling) }))} /> : undefined,
            exportable: seesCost,
            table: { columns: present([{ key: "branch", header: "Branch" }, { key: "items", header: "Items", kind: "number" }, { key: "units", header: "Units", kind: "number", total: d.total.units }, { key: "cost", header: "At cost", kind: "money", total: d.total.cost }, { key: "selling", header: "At selling price", kind: "money", total: d.total.selling }, { key: "margin", header: "Margin %", kind: "percent" }], rows), rows },
          };
        }
        case "branch-compare": {
          const d = await ReportsApi.branchComparison(p);
          const rows: Row[] = d.rows.map((x) => ({ _key: x.productId, item: x.item, unit: x.unit, reorder: x.reorder, soldInPeriod: x.soldInPeriod, ...Object.fromEntries(d.branches.map((b) => [`b:${b}`, x.qty[b] ?? 0])), total: Object.values(x.qty).reduce((a, q) => a + q, 0) }));
          return {
            title: "Branch comparison",
            note: "Units at each branch now, not counting stock in transit. Sales are those rung up at these branches in the period.",
            top: <BranchComparison branches={d.branches} rows={d.rows} period={`${formatDate(from)} to ${formatDate(to)}`} />,
            exportable: true,
            table: { columns: [{ key: "item", header: "Item" }, { key: "unit", header: "Unit" }, ...d.branches.map((b) => ({ key: `b:${b}`, header: b, kind: "number" as const })), { key: "total", header: "Total", kind: "number" }, { key: "reorder", header: "Reorder level", kind: "number" }, { key: "soldInPeriod", header: "Sold in period", kind: "number" }], rows },
          };
        }
        case "goods-returns": {
          const d = await ReportsApi.goodsReturns(p);
          const rows: Row[] = d.rows.map((x) => ({ _key: x.id, ...x }));
          return {
            title: "Goods returns",
            summary: [{ label: "Returns", value: n0(d.summary.returns) }, { label: "Credit expected", value: formatKES(d.summary.value) }],
            note: "Credit is recorded against a return when supplier credit notes come with Accounts; until then each return shows the credit expected at batch cost.",
            top: d.rows.length ? <GoodsReturnsReport returns={d.rows.map((x) => ({ ref: x.ref, date: x.date, supplier: x.supplier, items: x.items, value: x.value, credited: x.credited }))} /> : undefined,
            exportable: d.rows.length > 0,
            table: { columns: [{ key: "ref", header: "Return" }, { key: "date", header: "Date", kind: "date" }, { key: "supplier", header: "Supplier" }, { key: "items", header: "Items" }, { key: "value", header: "Credit expected", kind: "money", total: d.summary.value }, { key: "credited", header: "Credited", kind: "money" }], rows },
          };
        }
        case "ageing": {
          const d = await ReportsApi.purchaseAgeing();
          const rows: Row[] = d.rows.map((x) => ({ _key: x.supplierId, supplier: x.supplier, b0: x.buckets[0], b1: x.buckets[1], b2: x.buckets[2], b3: x.buckets[3], total: x.buckets.reduce((a, v) => a + v, 0) }));
          return {
            title: "Purchase ageing",
            note: "What is owed to each supplier, aged from each bill's due date (received date plus the supplier's terms): the same figures as supplier statements.",
            top: d.rows.length ? <PurchaseAgeing rows={d.rows.map((x) => ({ supplier: x.supplier, buckets: x.buckets }))} asOf={formatDate(d.asOf)} /> : undefined,
            exportable: d.rows.length > 0,
            table: { columns: [{ key: "supplier", header: "Supplier" }, ...d.labels.map((l, i) => ({ key: `b${i}`, header: l, kind: "money" as const, total: sum(rows, `b${i}`) })), { key: "total", header: "Owed", kind: "money", total: sum(rows, "total") }], rows },
          };
        }
        case "customers": {
          const d = await CustomersApi.report(from, to, branchId);
          const rows: Row[] = d.customers.map((c: Record<string, unknown>) => ({ _key: c.id as number, ...c }));
          return {
            title: "Customers",
            summary: [{ label: "Customers who bought", value: n0(d.summary.activeCustomers) }, { label: "Repeat customers", value: n0(d.summary.repeatCustomers) }, { label: "Registered customers' spend", value: formatKES(d.summary.identifiedRevenue) }, { label: "Walk-in spend", value: formatKES(d.summary.walkInRevenue) }],
            top: <TopCustomers customers={d.customers.filter((c: { transactions: number }) => c.transactions > 0).map((c: { id: number; name: string; totalSpend: number; transactions: number; lastPurchase: string }) => ({ id: c.id, name: c.name, spend: c.totalSpend, visits: c.transactions, balance: 0, lastVisit: c.lastPurchase ? formatDate(String(c.lastPurchase).slice(0, 10)) : "–", purchases: [] }))} walkIn={{ spend: d.summary.walkInRevenue, visits: d.summary.walkInTransactions }} period={`${formatDate(from)} to ${formatDate(to)}`} onStatement={(cid) => router.push(`/customers?id=${cid}`)} />,
            table: { columns: [{ key: "name", header: "Customer", render: (r) => <button type="button" className="cursor-pointer text-left font-medium text-primary hover:underline" onClick={() => router.push(`/customers?id=${r.id}`)}>{String(r.name)}</button> }, { key: "phone", header: "Phone" }, { key: "transactions", header: "Purchases", kind: "number", total: sum(rows, "transactions") }, { key: "avgBasket", header: "Average basket", kind: "money" }, { key: "lastPurchase", header: "Last purchase", kind: "date" }, { key: "totalSpend", header: "Spent", kind: "money", total: sum(rows, "totalSpend") }], rows },
          };
        }
        case "suppliers": {
          const d = await SuppliersApi.report(from, to);
          const rows: Row[] = d.suppliers.map((s: Record<string, unknown>) => ({ _key: s.id as number, ...s }));
          return { title: "Suppliers", note: "Purchase orders have no branch, so supplier reports cover the whole pharmacy.", table: { columns: present([{ key: "name", header: "Supplier", render: (r) => <button type="button" className="cursor-pointer text-left font-medium text-primary hover:underline" onClick={() => router.push(`/suppliers?id=${r.id}&tab=received`)}>{String(r.name)}</button> }, { key: "orders", header: "Orders", kind: "number", total: sum(rows, "orders") }, { key: "pendingOrders", header: "Open", kind: "number" }, { key: "orderedValue", header: "Ordered", kind: "money", total: sum(rows, "orderedValue") }, { key: "receivedValue", header: "Received", kind: "money", total: sum(rows, "receivedValue") }, { key: "outstandingValue", header: "Still to come", kind: "money", total: sum(rows, "outstandingValue") }, { key: "lastOrderDate", header: "Last order", kind: "date" }], rows), rows } };
        }
        case "purchases": {
          const d = await SuppliersApi.productPurchaseReport(from, to);
          const rows: Row[] = d.products.map((x: Record<string, unknown>) => ({ _key: `${x.productId ?? x.name}`, ...x, supplierList: (x.suppliers as string[]).join(", ") }));
          return { title: "Product purchases", table: { columns: [{ key: "name", header: "Item" }, { key: "supplierList", header: "Suppliers" }, { key: "unitsOrdered", header: "Ordered", kind: "number", total: sum(rows, "unitsOrdered") }, { key: "unitsReceived", header: "Received", kind: "number", total: sum(rows, "unitsReceived") }, { key: "avgUnitCost", header: "Average unit cost", kind: "money" }, { key: "lastUnitCost", header: "Last unit cost", kind: "money" }, { key: "receivedValue", header: "Received value", kind: "money", total: sum(rows, "receivedValue") }], rows } };
        }
        case "summary": {
          const d = await ReportsApi.summary(p);
          const cats = d.categories;
          const change = (a: number, b: number) => (b ? `${a >= b ? "+" : ""}${(((a - b) / b) * 100).toFixed(1)}% on the period before` : "No sales the period before");
          return {
            title: "Sales summary",
            summary: [{ label: "Gross sales", value: `${formatKES(d.totals.gross)} (${change(d.totals.gross, d.previousTotals.gross)})` }, { label: "Net of VAT", value: formatKES(d.totals.net) }, { label: "VAT", value: formatKES(d.totals.vat) }, { label: "Receipts", value: n0(d.totals.receipts) }, { label: "Average sale", value: d.totals.receipts ? formatKES(d.totals.gross / d.totals.receipts) : "–" }],
            top: (
              <div className="grid gap-4 lg:grid-cols-2">
                <ChartCard title="Sales trend" description={`Gross sales per day, against the same number of days before (${formatDate(d.previous.from)} to ${formatDate(d.previous.to)})`} height="h-auto">
                  <PeriodTrend points={d.trend.map((t) => ({ label: formatDate(t.date, { short: true }), current: t.current, previous: t.previous }))} currentLabel="This period" previousLabel="Period before" />
                </ChartCard>
                <ChartCard title="When customers buy" description="Receipts by weekday and hour" height="h-auto">
                  <PeakHeatmap days={d.heatmap.days} hours={d.heatmap.hours} counts={d.heatmap.counts} />
                </ChartCard>
                <ChartCard title="Top categories" description={cats[0]?.cost !== undefined ? "Revenue before VAT, split into cost and gross profit" : "Revenue before VAT"} height="h-auto" state={cats.length ? "ready" : "empty"}>
                  {cats[0]?.cost !== undefined ? <CategoryMarginChart rows={cats.map((c) => ({ category: c.category, revenue: c.revenue, cost: c.cost ?? 0 }))} /> : <MethodSplit rows={cats.map((c) => ({ method: c.category, amount: c.revenue }))} />}
                </ChartCard>
                <ChartCard title="Prescription and over-the-counter" description="Revenue before VAT" height="h-auto">
                  <RxOtcSplit rx={d.rxOtc.rx} otc={d.rxOtc.otc} rxProducts={d.rxOtc.rxProducts} products={d.rxOtc.products} />
                </ChartCard>
              </div>
            ),
            table: { columns: present([{ key: "category", header: "Category" }, { key: "revenue", header: "Revenue before VAT", kind: "money", total: sum(cats.map((c) => ({ _key: c.category, ...c })), "revenue") }, { key: "cost", header: "Cost", kind: "money" }], cats.map((c) => ({ _key: c.category, ...c }))), rows: cats.map((c) => ({ _key: c.category, ...c })) },
          };
        }
        case "monthly": {
          const d = await ReportsApi.monthly(24, branchId);
          const recent = d.months.slice(-12);
          const label = (m: string) => new Date(`${m}-01T00:00:00`).toLocaleDateString("en-GB", { month: "short", year: "numeric" });
          const lastYear = Object.fromEntries(recent.map((m) => { const ly = d.months.find((x) => x.month === `${Number(m.month.slice(0, 4)) - 1}${m.month.slice(4)}`); return [label(m.month), ly ? { month: label(ly.month), gross: ly.gross, vat: ly.vat, transactions: ly.receipts } : undefined]; }).filter(([, x]) => x)) as Record<string, { month: string; gross: number; vat: number; transactions: number }>;
          const rows: Row[] = recent.map((m) => ({ ...m, _key: m.month, month: label(m.month) + (m.partial ? " (so far)" : "") }));
          return {
            title: "Monthly comparison",
            top: (
              <div className="space-y-4">
                <MonthlyComparison months={recent.map((m) => ({ month: label(m.month), gross: m.gross, vat: m.vat, transactions: m.receipts, partial: m.partial }))} lastYear={lastYear} />
                <ChartCard title="Net sales by month" height="">
                  <MonthlyTrend months={recent.map((m) => ({ label: label(m.month), net: m.net, partial: m.partial, byMethod: Object.fromEntries(PAY_METHODS.map((k) => [k, k === "Credit" ? (m.byMethod["On account"] ?? 0) : (m.byMethod[k] ?? 0)])) as Record<PayMethod, number> }))} />
                </ChartCard>
              </div>
            ),
            table: { columns: [{ key: "month", header: "Month" }, { key: "receipts", header: "Receipts", kind: "number", total: sum(rows, "receipts") }, { key: "net", header: "Net of VAT", kind: "money", total: sum(rows, "net") }, { key: "vat", header: "VAT", kind: "money", total: sum(rows, "vat") }, { key: "gross", header: "Gross", kind: "money", total: sum(rows, "gross") }], rows },
          };
        }
        case "insights": {
          // Plan O: the report's branch filter (a user kept to a branch gets their own whatever is sent)
          const [d, nar] = await Promise.all([DashboardApi.getInsights(90, branchId), DashboardApi.getInsightsNarrative(90, branchId).catch(() => ({ available: false, reason: "" }))]);
          const ins = (d.insights ?? {}) as Record<InsightBucket, { name: string; stock?: number; daysOfStock?: number | null; suggestedOrder?: number; stockValue?: number; unitsSold?: number; revenue?: number }[]>;
          const detail = (b: InsightBucket, e: (typeof ins)[InsightBucket][number]) => (b === "deadStock" ? `${n0(e.stock ?? 0)} in stock, no sales in 90 days` : b === "stockouts" ? `Out of stock; sold ${n0(e.unitsSold ?? 0)} in 90 days` : `${n0(e.stock ?? 0)} in stock, about ${e.daysOfStock ?? "–"} days`);
          const buckets = Object.fromEntries((Object.keys(ins) as InsightBucket[]).map((b) => [b, { count: ins[b].length, cash: ins[b].reduce((t, e) => t + (e.stockValue ?? 0), 0) || undefined, items: ins[b].slice(0, 5).map((e) => ({ name: e.name, detail: detail(b, e), figure: e.suggestedOrder ? `Order ${n0(e.suggestedOrder)}` : e.stockValue !== undefined ? formatKES(e.stockValue) : undefined })) }]));
          const narrative: Narrative = nar?.available ? { state: "ready", text: nar.narrative, at: nar.generatedAt ?? new Date().toISOString() } : { state: "unavailable", reason: nar?.reason ?? "" };
          return { title: "Stock insights", note: d.window?.branchId != null ? "Worked out from this branch's last 90 days' sales and its stock today; not predicted." : "Worked out from the last 90 days' sales and today's stock in every branch; not predicted.", top: <div className="space-y-4"><InsightNarrative narrative={narrative} /><InsightCards buckets={buckets} /></div> };
        }
        case "general": {
          const d = await ReportsApi.general(p);
          return {
            title: "General report",
            note: <>{d.invoicesNote} {d.branchNote ?? ""} {d.note}</>,
            top: <GeneralReport invoices={d.invoices} byTender={d.byTender.map((t) => ({ tender: t.tender, amount: t.amount }))} creditNotes={d.creditNotes.amount} purchases={d.purchases} onOpenPL={() => router.push("/accounts")} />,
            table: { columns: [{ key: "line", header: "Line" }, { key: "amount", header: "Amount", kind: "money" }], rows: [...d.byTender.map((t) => ({ _key: t.tender, line: `POS sales: ${t.tender}`, amount: t.amount })), { _key: "inv", line: "Invoice sales", amount: d.invoices }, { _key: "cn", line: "Less credit notes", amount: d.creditNotes.amount ? -d.creditNotes.amount : 0 }, { _key: "gr", line: "Gross after returns", amount: d.grossAfterReturns }, { _key: "pu", line: "Less purchases (goods received)", amount: d.purchases ? -d.purchases : 0 }, { _key: "lp", line: "Sales less purchases (not profit)", amount: d.salesLessPurchases }] },
          };
        }
        case "pl": {
          const o = await LedgerApi.openings().catch(() => null);
          if (o?.started) return { title: "Profit and loss", note: <>Profit and loss now comes from the books. <Button size="sm" variant="outline" onClick={() => router.push("/accounts")}>Open profit and loss in Accounts</Button></> };
          const [r, ex] = await Promise.all([SalesApi.report(from, to, branchId), ExpensesApi.summary(from, to, branchId).catch(() => null)]);
          if (r.cogs === undefined) return { title: "Profit and loss", note: "Profit and loss uses cost figures your role doesn't see." };
          const expenses = Number(ex?.summary?.total ?? 0);
          return {
            title: "Profit and loss",
            note: "Your books haven't started yet, so these are the figures from sales and expenses. Once the opening balances are posted in Accounts → General ledger, profit and loss comes from the books instead.",
            top: <PLWaterfall steps={[{ label: "Gross sales", amount: r.totalRevenue, kind: "total" }, { label: "Less VAT", amount: r.totalVat, kind: "less" }, { label: "Net revenue", amount: r.netRevenue, kind: "total" }, { label: "Less cost of goods sold", amount: r.cogs, kind: "less" }, { label: "Gross profit", amount: r.grossProfit, kind: "total" }, { label: "Less expenses", amount: expenses, kind: "less" }, { label: "Net profit", amount: r.grossProfit - expenses, kind: "total" }]} />,
          };
        }
      }
    };
    run()
      .then((x) => live && show(x))
      .catch((e) => {
        if (!live) return;
        setError(apiError(e, "The report could not be loaded."));
        setState("error");
      });
    return () => {
      live = false;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id, p, version, asOf, activityPage]);

  React.useEffect(() => setActivityPage(1), [id, p]);

  if (state === "error") return <ReportTable columns={[{ key: "x", header: "Report", cell: () => "" }]} rows={[]} rowKey={() => 0} state="error" error={error} />;
  if (state === "loading" || !v) return <Loading what="the report" columns={6} rows={8} numeric={[3, 4, 5]} />;
  return (
    <div className="space-y-4">
      {v.summary?.length ? (
        <dl className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
          {v.summary.map((s) => (
            <div key={s.label} className="rounded-lg border border-[var(--color-border-panel)] bg-card p-3">
              <dt className="text-xs text-muted-foreground">{s.label}</dt>
              <dd className="mt-1 text-lg font-semibold tabular-nums">{s.value}</dd>
            </div>
          ))}
        </dl>
      ) : null}
      {v.note ? <div className="text-sm text-muted-foreground">{v.note}</div> : null}
      {v.top}
      {v.table && !v.exportable ? <ReportTable columns={toReportColumns(v.table.columns)} rows={v.table.rows} rowKey={(r) => r._key} /> : null}
      {v.bottom}
      <ConfirmDialog
        open={!!restore}
        onOpenChange={(o) => !o && setRestore(null)}
        level="B"
        tone="primary"
        title={`Restore sale ${restore?.receipt}?`}
        consequences={["It counts as a sale again in every report.", "If the books have started, it posts to the ledger again."]}
        actionLabel="Restore sale"
        busyLabel="Restoring…"
        onConfirm={async () => {
          if (!restore) return;
          try {
            await SalesApi.restore(restore.id);
          } catch (e) {
            throw new Error(apiError(e, "Not restored."));
          }
          toast.add({ title: `${restore.receipt} restored` });
          setRestore(null);
          setVersion((x) => x + 1);
        }}
      />
    </div>
  );
}

function toSaleDetail(s: ReportSaleRow): SaleDetail {
  return {
    receipt: s.receipt + (s.voided ? " (voided)" : ""),
    time: time(s.at),
    cashier: s.cashier,
    customer: s.customer,
    items: s.items.map((i) => ({ name: i.name, qty: i.qty, price: i.unitPrice, discount: i.discount || undefined })),
    vat: s.vat,
    payments: [...s.payments.map((x) => ({ method: x.method, ref: x.reference ?? undefined, amount: x.amount })), ...(s.onAccount > 0 ? [{ method: "On account", amount: s.onAccount }] : [])],
    returns: s.returns.map((r) => ({ ref: r.ref, item: r.type, qty: 1, amount: r.amount, reason: r.method ? `Refunded by ${r.method}` : r.type })),
  };
}

function Pager({ page, pageSize, total, onPage }: { page: number; pageSize: number; total: number; onPage: (p: number) => void }) {
  const pages = Math.max(1, Math.ceil(total / pageSize));
  if (pages <= 1) return null;
  return (
    <span className="flex items-center justify-end gap-2 text-xs text-muted-foreground">
      <Button size="xs" variant="outline" disabled={page <= 1} onClick={() => onPage(page - 1)}>
        Previous
      </Button>
      Page {page} of {pages} · exports hold this page
      <Button size="xs" variant="outline" disabled={page >= pages} onClick={() => onPage(page + 1)}>
        Next
      </Button>
    </span>
  );
}
