import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { requireSession } from "@/lib/auth/requireSession"; import { prisma } from "@/lib/prisma"; import { computeFinancialImpact } from "@/lib/financial/impact"; const RANGE_MS: Record = { "24h": 24 * 60 * 60 * 1000, "7d": 7 * 24 * 60 * 60 * 1000, "30d": 30 * 24 * 60 * 60 * 1000, }; function canManageFinancials(role?: string | null) { return role === "OWNER"; } function parseDate(input?: string | null) { if (!input) return null; const n = Number(input); if (!Number.isNaN(n)) return new Date(n); const d = new Date(input); return Number.isNaN(d.getTime()) ? null : d; } function pickRange(req: NextRequest) { const url = new URL(req.url); const range = url.searchParams.get("range") ?? "7d"; const now = new Date(); if (range === "custom") { const start = parseDate(url.searchParams.get("start")) ?? new Date(now.getTime() - RANGE_MS["24h"]); const end = parseDate(url.searchParams.get("end")) ?? now; return { start, end }; } const ms = RANGE_MS[range] ?? RANGE_MS["24h"]; return { start: new Date(now.getTime() - ms), end: now }; } function escapeHtml(value: string) { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function formatMoney(value: number, currency: string) { if (!Number.isFinite(value)) return "--"; try { return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 2 }).format(value); } catch { return `${value.toFixed(2)} ${currency}`; } } function formatNumber(value: number | null, digits = 2) { if (value == null || !Number.isFinite(value)) return "--"; return value.toFixed(digits); } function slugify(value: string) { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 60) || "report"; } export async function GET(req: NextRequest) { const session = await requireSession(); if (!session) return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); const membership = await prisma.orgUser.findUnique({ where: { orgId_userId: { orgId: session.orgId, userId: session.userId } }, select: { role: true }, }); if (!canManageFinancials(membership?.role)) { return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 }); } const url = new URL(req.url); const { start, end } = pickRange(req); const machineId = url.searchParams.get("machineId") ?? undefined; const location = url.searchParams.get("location") ?? undefined; const sku = url.searchParams.get("sku") ?? undefined; const currency = url.searchParams.get("currency") ?? undefined; const [org, impact] = await Promise.all([ prisma.org.findUnique({ where: { id: session.orgId }, select: { name: true } }), computeFinancialImpact({ orgId: session.orgId, start, end, machineId, location, sku, currency, includeEvents: true, }), ]); const orgName = org?.name ?? "Organization"; const summaryBlocks = impact.currencySummaries .map( (summary) => `
${escapeHtml(summary.currency)}
${escapeHtml(formatMoney(summary.totals.total, summary.currency))}
Slow: ${escapeHtml(formatMoney(summary.totals.slowCycle, summary.currency))}
Micro: ${escapeHtml(formatMoney(summary.totals.microstop, summary.currency))}
Macro: ${escapeHtml(formatMoney(summary.totals.macrostop, summary.currency))}
Scrap: ${escapeHtml(formatMoney(summary.totals.scrap, summary.currency))}
` ) .join(""); const dailyTables = impact.currencySummaries .map((summary) => { const rows = summary.byDay .map( (row) => ` ${escapeHtml(row.day)} ${escapeHtml(formatMoney(row.total, summary.currency))} ${escapeHtml(formatMoney(row.slowCycle, summary.currency))} ${escapeHtml(formatMoney(row.microstop, summary.currency))} ${escapeHtml(formatMoney(row.macrostop, summary.currency))} ${escapeHtml(formatMoney(row.scrap, summary.currency))} ` ) .join(""); return `

${escapeHtml(summary.currency)} Daily Breakdown

${rows || ""}
Day Total Slow Micro Macro Scrap
No data
`; }) .join(""); const eventRows = impact.events .map( (e) => ` ${escapeHtml(e.ts.toISOString())} ${escapeHtml(e.eventType)} ${escapeHtml(e.category)} ${escapeHtml(e.machineName ?? "-")} ${escapeHtml(e.location ?? "-")} ${escapeHtml(e.sku ?? "-")} ${escapeHtml(e.workOrderId ?? "-")} ${escapeHtml(formatNumber(e.durationSec))} ${escapeHtml(formatMoney(e.costTotal, e.currency))} ${escapeHtml(e.currency)} ` ) .join(""); const html = ` Financial Impact Report

Financial Impact Report

${escapeHtml(orgName)} | ${escapeHtml(start.toISOString())} - ${escapeHtml(end.toISOString())}
${summaryBlocks || "
No totals yet.
"}
${dailyTables}

Event Details

${eventRows || ""}
Timestamp Event Category Machine Location SKU Work Order Duration (sec) Cost Currency
No events
`; const fileName = `financial_report_${slugify(orgName)}.html`; return new NextResponse(html, { headers: { "Content-Type": "text/html; charset=utf-8", "Content-Disposition": `attachment; filename=\"${fileName}\"`, }, }); }