import { NextResponse } from 'next/server'; import { z } from 'zod'; import { withAuth, withPermission } from '@/lib/api/helpers'; import { parseBody } from '@/lib/api/route-helpers'; import { errorResponse } from '@/lib/errors'; import { createReportTemplate, listReportTemplates } from '@/lib/services/report-templates.service'; const createBodySchema = z.object({ kind: z.enum(['dashboard', 'clients', 'berths', 'interests']), name: z.string().min(1).max(120), description: z.string().max(400).nullable().optional(), // Config is the raw discriminated-union payload; the // /api/v1/reports/generate route re-validates at use time, so we // accept it as `record` here without imposing the full union shape. config: z.record(z.string(), z.unknown()), }); /** * GET /api/v1/reports/templates?kind=clients * List saved templates for the active port, optionally filtered to * one kind. Used by the Export dialog's saved-templates dropdown. * * POST /api/v1/reports/templates * Persist a template. The dialog calls this when the rep ticks * "Save as template" while configuring an export. * * Both gated on `reports.export` - the same permission that lets * the rep generate reports also lets them save templates. */ export const GET = withAuth( withPermission('reports', 'export', async (req, ctx) => { try { const url = new URL(req.url); const kind = url.searchParams.get('kind') ?? undefined; const rows = await listReportTemplates(ctx.portId, kind ?? undefined); return NextResponse.json({ data: rows }); } catch (error) { return errorResponse(error); } }), ); export const POST = withAuth( withPermission('reports', 'export', async (req, ctx) => { try { const body = await parseBody(req, createBodySchema); // Cross-validate that the config's discriminator matches the // outer `kind`. Without this, a rep could save a clients-kind // template with a dashboard config and confuse the rendering // path at use time. const configKind = (body.config as { kind?: unknown }).kind; if (configKind !== body.kind) { return NextResponse.json( { error: `config.kind must equal "${body.kind}"` }, { status: 400 }, ); } const row = await createReportTemplate({ portId: ctx.portId, kind: body.kind, name: body.name, description: body.description ?? null, config: body.config, meta: { userId: ctx.userId, portId: ctx.portId, ipAddress: ctx.ipAddress, userAgent: ctx.userAgent, }, }); return NextResponse.json({ data: row }); } catch (error) { return errorResponse(error); } }), );