Files
CapaKraken/packages/api/src/router/project.ts
T

121 lines
4.3 KiB
TypeScript

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)),
});