refactor(api): extract estimate phasing procedures
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import {
|
||||
aggregateWeeklyByChapter,
|
||||
aggregateWeeklyToMonthly,
|
||||
distributeHoursToWeeks,
|
||||
generateWeekRange,
|
||||
} from "@capakraken/engine";
|
||||
import { PermissionKey, GenerateWeeklyPhasingSchema } from "@capakraken/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||
import {
|
||||
controllerProcedure,
|
||||
managerProcedure,
|
||||
requirePermission,
|
||||
} from "../trpc.js";
|
||||
import { getEstimateById } from "@capakraken/application";
|
||||
|
||||
type WeeklyPhasingMeta = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
pattern: string;
|
||||
weeklyHours: Record<string, number>;
|
||||
generatedAt: string;
|
||||
};
|
||||
|
||||
export const estimatePhasingProcedures = {
|
||||
generateWeeklyPhasing: managerProcedure
|
||||
.input(GenerateWeeklyPhasingSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||
|
||||
const estimate = await findUniqueOrThrow(
|
||||
getEstimateById(
|
||||
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
|
||||
input.estimateId,
|
||||
),
|
||||
"Estimate",
|
||||
);
|
||||
|
||||
const workingVersion = estimate.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
);
|
||||
|
||||
if (!workingVersion) {
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
message: "Estimate has no working version",
|
||||
});
|
||||
}
|
||||
|
||||
const pattern = input.pattern ?? "even";
|
||||
const updates: Array<{
|
||||
id: string;
|
||||
monthlySpread: Record<string, number>;
|
||||
metadata: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
for (const line of workingVersion.demandLines) {
|
||||
const result = distributeHoursToWeeks({
|
||||
totalHours: line.hours,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
pattern,
|
||||
});
|
||||
|
||||
const monthlySpread = aggregateWeeklyToMonthly(result.weeklyHours);
|
||||
const existingMetadata = (line.metadata ?? {}) as Record<string, unknown>;
|
||||
const metadata = {
|
||||
...existingMetadata,
|
||||
weeklyPhasing: {
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
pattern,
|
||||
weeklyHours: result.weeklyHours,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
updates.push({ id: line.id, monthlySpread, metadata });
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
updates.map((update) =>
|
||||
ctx.db.estimateDemandLine.update({
|
||||
where: { id: update.id },
|
||||
data: {
|
||||
monthlySpread: update.monthlySpread as Prisma.InputJsonValue,
|
||||
metadata: update.metadata as Prisma.InputJsonValue,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
estimateId: input.estimateId,
|
||||
versionId: workingVersion.id,
|
||||
linesUpdated: updates.length,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
pattern,
|
||||
};
|
||||
}),
|
||||
|
||||
getWeeklyPhasing: controllerProcedure
|
||||
.input(z.object({ estimateId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const estimate = await findUniqueOrThrow(
|
||||
getEstimateById(
|
||||
ctx.db as unknown as Parameters<typeof getEstimateById>[0],
|
||||
input.estimateId,
|
||||
),
|
||||
"Estimate",
|
||||
);
|
||||
|
||||
const version = estimate.versions[0];
|
||||
if (!version) {
|
||||
throw new TRPCError({
|
||||
code: "PRECONDITION_FAILED",
|
||||
message: "Estimate has no versions",
|
||||
});
|
||||
}
|
||||
|
||||
const linesWithPhasing: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
chapter: string | null;
|
||||
hours: number;
|
||||
weeklyHours: Record<string, number>;
|
||||
}> = [];
|
||||
let phasingConfig: { startDate: string; endDate: string; pattern: string } | null = null;
|
||||
|
||||
for (const line of version.demandLines) {
|
||||
const meta = (line.metadata ?? {}) as Record<string, unknown>;
|
||||
const phasing = meta["weeklyPhasing"] as WeeklyPhasingMeta | undefined;
|
||||
if (!phasing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!phasingConfig) {
|
||||
phasingConfig = {
|
||||
startDate: phasing.startDate,
|
||||
endDate: phasing.endDate,
|
||||
pattern: phasing.pattern,
|
||||
};
|
||||
}
|
||||
|
||||
linesWithPhasing.push({
|
||||
id: line.id,
|
||||
name: line.name,
|
||||
chapter: line.chapter ?? null,
|
||||
hours: line.hours,
|
||||
weeklyHours: phasing.weeklyHours,
|
||||
});
|
||||
}
|
||||
|
||||
if (!phasingConfig || linesWithPhasing.length === 0) {
|
||||
return {
|
||||
estimateId: input.estimateId,
|
||||
versionId: version.id,
|
||||
hasPhasing: false as const,
|
||||
config: null,
|
||||
weeks: [],
|
||||
lines: [],
|
||||
chapterAggregation: {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
estimateId: input.estimateId,
|
||||
versionId: version.id,
|
||||
hasPhasing: true as const,
|
||||
config: phasingConfig,
|
||||
weeks: generateWeekRange(phasingConfig.startDate, phasingConfig.endDate),
|
||||
lines: linesWithPhasing,
|
||||
chapterAggregation: aggregateWeeklyByChapter(linesWithPhasing),
|
||||
};
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user