Files
CapaKraken/packages/api/src/router/project-cost-read.ts
T
Hartmut 9bd3781c03 fix(types): flatten tRPC Zod schema types to resolve TS2589 inference depth errors
Cast Zod schemas with .refine()/.superRefine() to z.ZodType<InferredType> at the
procedure level. This short-circuits TypeScript's deep type recursion through
tRPC's middleware chain, eliminating 4 of 5 @ts-expect-error TS2589 suppressions
in web components (VacationModal, ProjectModal, UsersClient, CountriesClient).

Applied same pattern to allocation, timeline, staffing, dashboard, project, and
resource query/mutation procedures to reduce client-side type depth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:28:12 +02:00

80 lines
2.7 KiB
TypeScript

import { listAssignmentBookings } from "@capakraken/application";
import { ProjectStatus } from "@capakraken/shared";
import { z } from "zod";
import { CursorInputSchema, paginateCursor } from "../db/pagination.js";
import { controllerProcedure } from "../trpc.js";
const ListWithCostsInputSchema = CursorInputSchema.extend({
status: z.nativeEnum(ProjectStatus).optional(),
search: z.string().optional(),
});
export const projectCostReadProcedures = {
listWithCosts: controllerProcedure
.input(
ListWithCostsInputSchema as z.ZodType<
z.infer<typeof ListWithCostsInputSchema>,
z.ZodTypeDef,
z.input<typeof ListWithCostsInputSchema>
>,
)
.query(async ({ ctx, input }) => {
const { status, search, cursor } = input;
const where = {
...(status ? { status } : {}),
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ shortCode: { contains: search, mode: "insensitive" as const } },
],
}
: {}),
};
const whereWithCursor = cursor ? { ...where, id: { gt: cursor } } : where;
const result = await paginateCursor(
({ take }) =>
ctx.db.project.findMany({
where: whereWithCursor,
take,
orderBy: [{ startDate: "asc" }, { id: "asc" }],
}),
input,
);
const projectIds = result.items.map((project) => project.id);
const bookings = projectIds.length
? await listAssignmentBookings(ctx.db, {
startDate: new Date("1900-01-01T00:00:00.000Z"),
endDate: new Date("2100-12-31T23:59:59.999Z"),
projectIds,
})
: [];
const projects = result.items.map((project) => {
const projectBookings = bookings.filter((booking) => booking.projectId === project.id);
let totalCostCents = 0;
let totalPersonDays = 0;
for (const booking of projectBookings) {
const days =
(new Date(booking.endDate).getTime() - new Date(booking.startDate).getTime()) /
(1000 * 60 * 60 * 24) +
1;
totalCostCents += booking.dailyCostCents * days;
totalPersonDays += (booking.hoursPerDay * days) / 8;
}
const utilizationPercent =
project.budgetCents > 0 ? Math.round((totalCostCents / project.budgetCents) * 100) : 0;
return {
...project,
totalCostCents: Math.round(totalCostCents),
totalPersonDays: Math.round(totalPersonDays * 10) / 10,
utilizationPercent,
};
});
return { projects, nextCursor: result.nextCursor };
}),
};