import { countPlanningEntries } from "@capakraken/application"; import { FieldType, ProjectStatus } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { paginate, PaginationInputSchema } from "../db/pagination.js"; import { buildDynamicFieldWhereClauses } from "./custom-field-filters.js"; import { projectCostReadProcedures } from "./project-cost-read.js"; import { projectCoverProcedures } from "./project-cover.js"; import { projectIdentifierReadProcedures } from "./project-identifier-read.js"; import { createProjectLifecycleProcedures } from "./project-lifecycle.js"; import { createProjectMutationProcedures } from "./project-mutations.js"; import { loadProjectPlanningReadModel } from "./project-planning-read-model.js"; import { createProjectBackgroundEffects } from "./project-background-effects.js"; import { getProjectShoringRatio } from "./project-shoring-ratio.js"; import { controllerProcedure, createTRPCRouter } from "../trpc.js"; const projectBackgroundEffects = createProjectBackgroundEffects(); export const projectRouter = createTRPCRouter({ ...projectCostReadProcedures, ...projectCoverProcedures, ...projectIdentifierReadProcedures, ...createProjectLifecycleProcedures({ invalidateDashboardCacheInBackground: projectBackgroundEffects.invalidateDashboardCacheInBackground, dispatchProjectWebhookInBackground: projectBackgroundEffects.dispatchProjectWebhookInBackground, }), ...createProjectMutationProcedures(projectBackgroundEffects), list: controllerProcedure .input( PaginationInputSchema.extend({ status: z.nativeEnum(ProjectStatus).optional(), search: z.string().optional(), // Custom field JSONB filters customFieldFilters: z.array(z.object({ key: z.string(), value: z.string(), type: z.nativeEnum(FieldType), })).optional(), }), ) .query(async ({ ctx, input }) => { const { status, search, cursor, customFieldFilters } = input; const cfConditions = buildDynamicFieldWhereClauses(customFieldFilters).map((dynamicFields) => ({ dynamicFields })); // eslint-disable-next-line @typescript-eslint/no-explicit-any const where: any = { ...(status ? { status } : {}), ...(search ? { OR: [ { name: { contains: search, mode: "insensitive" as const } }, { shortCode: { contains: search, mode: "insensitive" as const } }, ], } : {}), ...(cfConditions.length > 0 ? { AND: cfConditions } : {}), }; const whereWithCursor = cursor ? { ...where, id: { gt: cursor } } : where; const result = await paginate( ({ skip, take }) => ctx.db.project.findMany({ where: whereWithCursor, skip, take, orderBy: [{ startDate: "asc" }, { id: "asc" }], }), () => ctx.db.project.count({ where }), input, ); const { countsByProjectId } = await countPlanningEntries(ctx.db, { projectIds: result.items.map((project) => project.id), }); return { projects: result.items.map((project) => ({ ...project, _count: { allocations: countsByProjectId.get(project.id) ?? 0, }, })), total: result.total, page: result.page, limit: result.limit, nextCursor: result.nextCursor, }; }), getById: controllerProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const [project, planningRead] = await Promise.all([ ctx.db.project.findUnique({ where: { id: input.id }, include: { blueprint: true }, }), loadProjectPlanningReadModel(ctx.db, { projectId: input.id }), ]); if (!project) { throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" }); } return { ...project, allocations: planningRead.readModel.assignments, demands: planningRead.readModel.demands, assignments: planningRead.readModel.assignments, }; }), getShoringRatio: controllerProcedure .input(z.object({ projectId: z.string() })) .query(async ({ ctx, input }) => getProjectShoringRatio(ctx.db, input.projectId)), });