import { auth } from "@/auth"; import { redirect } from "next/navigation"; import Link from "next/link"; import type { Metadata } from "next"; import { ChevronRight, BarChart3 } from "lucide-react"; import { hasPermission } from "@/lib/permissions"; import { formatCurrency, formatCompactINR } from "@/lib/utils"; import { getReportDataset, buildAccountIndex, accountLevelRows, accountNodeSpend, accountNodeWeekly, applyScope, parseScope, parseGranularity, resolveFy, resolveMonth, parseSel, toggleSel, fyLabel, FY_MONTHS, WEEK_LABELS, SCOPE_LABELS, type NodeSpend, } from "@/lib/reports"; import { ReportsToolbar } from "@/components/reports/reports-toolbar"; import { ComparisonChart, Sparkline, SERIES_COLORS, type Series } from "@/components/reports/charts"; import { Kpi, KpiStrip } from "@/components/reports/kpi"; import { ReportBreadcrumb, ReportTitle, SelectCheckbox, CompareBar } from "@/components/reports/report-header"; export const metadata: Metadata = { title: "Accounting Codes — Reports" }; const sum = (a: number[]) => a.reduce((x, y) => x + y, 0); const tierBadgeCls: Record = { Heading: "bg-primary-50 text-primary-700", "Sub-heading": "bg-violet-50 text-violet-700", Leaf: "bg-neutral-100 text-neutral-600", }; export default async function AccountingCodesReport({ searchParams, }: { searchParams: Promise<{ fy?: string; gran?: string; scope?: string; month?: string; parent?: string; sel?: string; cmp?: string }>; }) { const session = await auth(); if (!session?.user) return null; if (!hasPermission(session.user.role, "view_analytics")) redirect("/dashboard"); const sp = await searchParams; const ds = await getReportDataset(); const idx = buildAccountIndex(ds.accounts); const gran = parseGranularity(sp.gran); const scope = parseScope(sp.scope); const fy = resolveFy(ds, sp.fy); const yearly = gran === "yearly"; const weekly = gran === "weekly"; const month = resolveMonth(ds, fy, sp.month); const sel = parseSel(sp.sel); const cmp = sp.cmp === "1" && sel.length > 0; const parent = sp.parent && idx.byId.has(sp.parent) ? sp.parent : null; const parentNode = parent ? idx.byId.get(parent)! : null; const rankOf = (r: NodeSpend) => (yearly ? sum(r.fyTotals) : weekly ? r.months[month] : r.total); const sparkOf = (r: NodeSpend) => (yearly ? r.fyTotals : weekly ? accountNodeWeekly(ds, idx, r.node.id, fy, month) : r.months); const ranked = cmp ? sel.filter((id) => idx.byId.has(id)).map((id) => ({ node: idx.byId.get(id)!, ...accountNodeSpend(ds, idx, id, fy) })) : accountLevelRows(ds, idx, parent, fy); ranked.sort((a, b) => rankOf(b) - rankOf(a)); const shown = cmp ? ranked : applyScope(ranked, scope); const grand = shown.reduce((s, r) => s + rankOf(r), 0); const childTier = shown[0]?.node.tier ?? "Heading"; const top = shown[0]; const nf = ds.fys.length; const curT = nf >= 1 ? shown.reduce((s, r) => s + r.fyTotals[nf - 1], 0) : 0; const prevT = nf >= 2 ? shown.reduce((s, r) => s + r.fyTotals[nf - 2], 0) : 0; const yoy = prevT ? ((curT - prevT) / prevT) * 100 : 0; // One distinct colour per accounting code (series) in every granularity; the // x-axis is months / weeks / financial years. const colored = (i: number) => SERIES_COLORS[i % SERIES_COLORS.length]; const chartLabels = yearly ? ds.fys.map(fyLabel) : weekly ? [...WEEK_LABELS] : [...FY_MONTHS]; const chartData: Record[] = chartLabels.map((lab, i) => { const row: Record = { x: lab }; shown.forEach((r) => (row[r.node.code] = sparkOf(r)[i])); return row; }); const series: Series[] = shown.map((r, i) => ({ key: r.node.code, color: colored(i) })); const monthLabel = (i: number) => `${FY_MONTHS[i]} '${String((fy + (i >= 9 ? 1 : 0)) % 100).padStart(2, "0")}`; const periodLabel = yearly ? ds.fys.map(fyLabel).join(" · ") : weekly ? `${monthLabel(month)} · ${fyLabel(fy)}` : fyLabel(fy); const base: Record = { fy: String(fy), gran: gran === "monthly" ? undefined : gran, scope: scope === "top5" ? undefined : scope, month: weekly ? String(month) : undefined, }; const qs = (extra: Record) => { const p = new URLSearchParams(); for (const [k, v] of Object.entries({ ...base, ...extra })) if (v) p.set(k, v); const s = p.toString(); return s ? `?${s}` : ""; }; const linkWith = (parentId: string | null) => `/reports/accounting-codes${qs({ parent: parentId ?? undefined, sel: sel.join(",") || undefined })}`; const detailHref = (id: string) => `/reports/accounting-codes/${id}${qs({ scope: undefined, parent: undefined })}`; const selHref = (id: string) => { const next = toggleSel(sel, id); return `/reports/accounting-codes${qs({ parent: cmp ? undefined : parent ?? undefined, sel: next.join(",") || undefined, cmp: cmp && next.length ? "1" : undefined })}`; }; const rowHref = (r: NodeSpend) => (idx.isLeaf(r.node.id) ? detailHref(r.node.id) : linkWith(r.node.id)); const exportHref = `/api/reports/spend?dim=accounting-code&fy=${fy}&gran=${gran}&scope=${scope}${parent && !cmp ? `&parent=${parent}` : ""}${cmp ? `&sel=${sel.join(",")}` : ""}`; const trail = [{ label: "Accounting Codes", href: parent || cmp ? linkWith(null) : undefined }]; if (parentNode && !cmp) { idx.pathTo(parentNode.id).forEach((a, i, arr) => trail.push({ label: `${a.code} · ${a.name}`, href: i < arr.length - 1 ? linkWith(a.id) : undefined })); } return (
({ value: i, label: monthLabel(i) }))} exportHref={exportHref} /> {cmp ? ( ← Back to browse ) : ( <> {parentNode && ( ← Back to {parentNode.parentId ? idx.byId.get(parentNode.parentId)!.name : "Accounting Codes"} )} {sel.length > 0 && } )} {grand === 0 ? (
No approved spend recorded for {periodLabel} yet.
) : ( <> = 0 ? "+" : ""}${yoy.toFixed(1)}%`} sub="vs prior FY" delta={yoy} />

{yearly ? `Spend by ${childTier.toLowerCase()} — year over year` : weekly ? `Weekly spend by ${childTier.toLowerCase()}` : `Monthly spend by ${childTier.toLowerCase()}`}

{periodLabel}
{shown.map((r) => { const value = rankOf(r); const pct = grand ? (value / grand) * 100 : 0; const leaf = idx.isLeaf(r.node.id); const inner = ( <> {r.node.code} {r.node.name} {r.node.tier} {formatCurrency(value)}
{pct.toFixed(0)}%
{!cmp && (leaf ? : )} ); return (
{cmp ? (
{inner}
) : ( {inner} )}
); })}
)}
); }