import { Prisma } from "@capakraken/db"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { controllerProcedure, createTRPCRouter } from "../trpc.js"; import { type EntityKey, ReportTemplateConfigSchema, validateReportInput, } from "./report-query-config.js"; import { reportQueryProcedures } from "./report-query-engine.js"; const ReportTemplateEntity = { RESOURCE: "RESOURCE", PROJECT: "PROJECT", ASSIGNMENT: "ASSIGNMENT", RESOURCE_MONTH: "RESOURCE_MONTH", } as const; type ReportTemplateEntity = (typeof ReportTemplateEntity)[keyof typeof ReportTemplateEntity]; type ReportTemplateRecord = { id: string; name: string; description: string | null; entity: ReportTemplateEntity; config: unknown; isShared: boolean; ownerId: string; updatedAt: Date; }; function getReportTemplateDelegate(db: unknown) { return (db as { reportTemplate: { findMany: (args: unknown) => Promise; findUnique: (args: unknown) => Promise<{ ownerId: string } | null>; update: (args: unknown) => Promise<{ id: string; updatedAt: Date }>; upsert: (args: unknown) => Promise<{ id: string; updatedAt: Date }>; delete: (args: unknown) => Promise; }; }).reportTemplate; } export const reportRouter = createTRPCRouter({ ...reportQueryProcedures, listTemplates: controllerProcedure.query(async ({ ctx }) => { const reportTemplate = getReportTemplateDelegate(ctx.db); const templates = await reportTemplate.findMany({ where: { OR: [ { ownerId: ctx.dbUser!.id }, { isShared: true }, ], }, orderBy: [{ name: "asc" }], select: { id: true, name: true, description: true, entity: true, config: true, isShared: true, ownerId: true, updatedAt: true, }, }); return templates.map((template: ReportTemplateRecord) => ({ id: template.id, name: template.name, description: template.description, entity: fromTemplateEntity(template.entity), config: ReportTemplateConfigSchema.parse(template.config), isShared: template.isShared, isOwner: template.ownerId === ctx.dbUser!.id, updatedAt: template.updatedAt, })); }), saveTemplate: controllerProcedure .input(z.object({ id: z.string().optional(), name: z.string().trim().min(1).max(120), description: z.string().trim().max(500).optional(), isShared: z.boolean().default(false), config: ReportTemplateConfigSchema, })) .mutation(async ({ ctx, input }) => { validateReportInput(input.config); const reportTemplate = getReportTemplateDelegate(ctx.db); const payload = input.config as unknown as Prisma.InputJsonValue; const entity = toTemplateEntity(input.config.entity); if (input.id) { const existing = await reportTemplate.findUnique({ where: { id: input.id }, select: { ownerId: true }, }); if (!existing || existing.ownerId !== ctx.dbUser!.id) { throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be updated" }); } return reportTemplate.update({ where: { id: input.id }, data: { name: input.name, description: input.description, entity, config: payload, isShared: input.isShared, }, select: { id: true, updatedAt: true }, }); } return reportTemplate.upsert({ where: { ownerId_name: { ownerId: ctx.dbUser!.id, name: input.name, }, }, update: { description: input.description, entity, config: payload, isShared: input.isShared, }, create: { ownerId: ctx.dbUser!.id, name: input.name, description: input.description, entity, config: payload, isShared: input.isShared, }, select: { id: true, updatedAt: true }, }); }), deleteTemplate: controllerProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const reportTemplate = getReportTemplateDelegate(ctx.db); const existing = await reportTemplate.findUnique({ where: { id: input.id }, select: { ownerId: true }, }); if (!existing || existing.ownerId !== ctx.dbUser!.id) { throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be deleted" }); } await reportTemplate.delete({ where: { id: input.id } }); return { ok: true }; }), }); function toTemplateEntity(entity: EntityKey): ReportTemplateEntity { switch (entity) { case "resource": return ReportTemplateEntity.RESOURCE; case "project": return ReportTemplateEntity.PROJECT; case "assignment": return ReportTemplateEntity.ASSIGNMENT; case "resource_month": return ReportTemplateEntity.RESOURCE_MONTH; default: throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` }); } } function fromTemplateEntity(entity: ReportTemplateEntity): EntityKey { switch (entity) { case ReportTemplateEntity.RESOURCE: return "resource"; case ReportTemplateEntity.PROJECT: return "project"; case ReportTemplateEntity.ASSIGNMENT: return "assignment"; case ReportTemplateEntity.RESOURCE_MONTH: return "resource_month"; default: throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` }); } }