9bd3781c03
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>
80 lines
2.7 KiB
TypeScript
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 };
|
|
}),
|
|
};
|