import { auth } from "@/auth"; import { hasPermission } from "@/lib/permissions"; import { NextRequest, NextResponse } from "next/server"; import { getReportDataset, buildAccountIndex, costCentreRows, accountLevelRows, topAccountsForCostCentre, costCentresForAccount, childBreakdown, applyScope, parseScope, parseGranularity, resolveFy, fyLabel, type Tier, } from "@/lib/reports"; const sum = (a: number[]) => a.reduce((x, y) => x + y, 0); const cell = (v: string | number) => { const s = String(v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; function csv(headers: string[], rows: (string | number)[][]): string { return [headers, ...rows].map((r) => r.map(cell).join(",")).join("\n"); } function file(name: string, body: string) { return new NextResponse(body, { headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="${name}-${Date.now()}.csv"`, }, }); } // CSV export for the Reports → Purchasing views. The `dim` query param mirrors // the page the user is on, so the download matches what's on screen. export async function GET(req: NextRequest) { const session = await auth(); if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!hasPermission(session.user.role, "view_analytics")) return NextResponse.json({ error: "Forbidden" }, { status: 403 }); const sp = req.nextUrl.searchParams; const dim = sp.get("dim") ?? "cost-centre"; const ds = await getReportDataset(); const idx = buildAccountIndex(ds.accounts); const gran = parseGranularity(sp.get("gran") ?? undefined); const scope = parseScope(sp.get("scope") ?? undefined); const fy = resolveFy(ds, sp.get("fy") ?? undefined); const yearly = gran === "yearly"; const fyCols = ds.fys.map(fyLabel); if (dim === "cost-centre") { const ranked = costCentreRows(ds, fy).sort((a, b) => (yearly ? sum(b.fyTotals) - sum(a.fyTotals) : b.total - a.total)); const rows = applyScope(ranked, scope).map((r) => [r.code, r.name, ...r.fyTotals, r.total, r.poCount]); return file("pelagia-cost-centre-spend", csv(["Code", "Cost Centre", ...fyCols, `${fyLabel(fy)} Total`, "POs"], rows)); } if (dim === "accounting-code") { const parent = sp.get("parent"); const parentId = parent && idx.byId.has(parent) ? parent : null; const ranked = accountLevelRows(ds, idx, parentId, fy).sort((a, b) => (yearly ? sum(b.fyTotals) - sum(a.fyTotals) : b.total - a.total)); const rows = applyScope(ranked, scope).map((r) => [r.node.code, r.node.name, r.node.tier, ...r.fyTotals, r.total, r.poCount]); return file("pelagia-accounting-code-spend", csv(["Code", "Name", "Tier", ...fyCols, `${fyLabel(fy)} Total`, "POs"], rows)); } if (dim === "cost-centre-detail") { const id = sp.get("id") ?? ""; const tier = (["Heading", "Sub-heading", "Leaf"] as Tier[]).includes(sp.get("tier") as Tier) ? (sp.get("tier") as Tier) : "Leaf"; const rows = topAccountsForCostCentre(ds, idx, id, fy, tier).map((b) => [b.label, b.value]); return file("pelagia-cost-centre-detail", csv([tier, `Spend (${fyLabel(fy)})`], rows)); } if (dim === "accounting-code-detail") { const id = sp.get("id") ?? ""; const leaf = idx.isLeaf(id); const mode = leaf || sp.get("break") === "cc" ? "cc" : "children"; const bd = mode === "cc" ? costCentresForAccount(ds, idx, id, fy) : childBreakdown(ds, idx, id, fy); const rows = bd.map((b) => [b.label, b.value]); return file("pelagia-accounting-code-detail", csv([mode === "cc" ? "Cost centre" : "Sub-account", `Spend (${fyLabel(fy)})`], rows)); } return NextResponse.json({ error: "Unknown report dimension" }, { status: 400 }); }