import { BlueprintTarget, CreateProjectSchema, PermissionKey, UpdateProjectSchema } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { assertBlueprintDynamicFields } from "./blueprint-validation.js"; import { managerProcedure, requirePermission, type TRPCContext } from "../trpc.js"; import type { ProjectBackgroundEffects } from "./project-background-effects.js"; function buildProjectCreateData( input: z.infer, ): Parameters[0]["data"] { return { shortCode: input.shortCode, name: input.name, orderType: input.orderType, allocationType: input.allocationType, winProbability: input.winProbability, budgetCents: input.budgetCents, startDate: input.startDate, endDate: input.endDate, status: input.status, responsiblePerson: input.responsiblePerson, ...(input.color !== undefined ? { color: input.color } : {}), staffingReqs: input.staffingReqs as unknown as import("@capakraken/db").Prisma.InputJsonValue, dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue, blueprintId: input.blueprintId, ...(input.utilizationCategoryId !== undefined ? { utilizationCategoryId: input.utilizationCategoryId || null } : {}), ...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}), } as unknown as Parameters[0]["data"]; } function buildProjectUpdateData( input: z.infer, ): Parameters[0]["data"] { return { ...(input.name !== undefined ? { name: input.name } : {}), ...(input.orderType !== undefined ? { orderType: input.orderType } : {}), ...(input.allocationType !== undefined ? { allocationType: input.allocationType } : {}), ...(input.winProbability !== undefined ? { winProbability: input.winProbability } : {}), ...(input.budgetCents !== undefined ? { budgetCents: input.budgetCents } : {}), ...(input.startDate !== undefined ? { startDate: input.startDate } : {}), ...(input.endDate !== undefined ? { endDate: input.endDate } : {}), ...(input.status !== undefined ? { status: input.status } : {}), ...(input.responsiblePerson !== undefined ? { responsiblePerson: input.responsiblePerson } : {}), ...(input.color !== undefined ? { color: input.color } : {}), ...(input.staffingReqs !== undefined ? { staffingReqs: input.staffingReqs as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}), ...(input.dynamicFields !== undefined ? { dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}), ...(input.blueprintId !== undefined ? { blueprintId: input.blueprintId } : {}), ...(input.utilizationCategoryId !== undefined ? { utilizationCategoryId: input.utilizationCategoryId || null } : {}), ...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}), ...(input.shoringThreshold !== undefined ? { shoringThreshold: input.shoringThreshold } : {}), ...(input.onshoreCountryCode !== undefined ? { onshoreCountryCode: input.onshoreCountryCode } : {}), } as unknown as Parameters[0]["data"]; } export function createProjectMutationProcedures( backgroundEffects: ProjectBackgroundEffects, ) { return { create: managerProcedure .input(CreateProjectSchema) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_PROJECTS); const existing = await ctx.db.project.findUnique({ where: { shortCode: input.shortCode }, }); if (existing) { throw new TRPCError({ code: "CONFLICT", message: `Project with short code "${input.shortCode}" already exists`, }); } await assertBlueprintDynamicFields({ db: ctx.db, blueprintId: input.blueprintId, dynamicFields: input.dynamicFields, target: BlueprintTarget.PROJECT, }); const project = await ctx.db.project.create({ data: buildProjectCreateData(input), }); await ctx.db.auditLog.create({ data: { entityType: "Project", entityId: project.id, action: "CREATE", changes: { after: project }, }, }); backgroundEffects.invalidateDashboardCacheInBackground(); backgroundEffects.dispatchProjectWebhookInBackground(ctx.db, "project.created", { id: project.id, shortCode: project.shortCode, name: project.name, status: project.status, }); return project; }), update: managerProcedure .input(z.object({ id: z.string(), data: UpdateProjectSchema })) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_PROJECTS); const existing = await findUniqueOrThrow( ctx.db.project.findUnique({ where: { id: input.id } }), "Project", ); const nextBlueprintId = input.data.blueprintId ?? existing.blueprintId ?? undefined; const nextDynamicFields = (input.data.dynamicFields ?? existing.dynamicFields ?? {}) as Record; await assertBlueprintDynamicFields({ db: ctx.db, blueprintId: nextBlueprintId, dynamicFields: nextDynamicFields, target: BlueprintTarget.PROJECT, }); const updated = await ctx.db.project.update({ where: { id: input.id }, data: buildProjectUpdateData(input.data), }); await ctx.db.auditLog.create({ data: { entityType: "Project", entityId: input.id, action: "UPDATE", changes: { before: existing, after: updated }, }, }); backgroundEffects.invalidateDashboardCacheInBackground(); return updated; }), }; }