refactor(api): modularize computation graph resource snapshot

This commit is contained in:
2026-03-31 10:41:24 +02:00
parent f08b47171c
commit 8fdc83aea1
4 changed files with 794 additions and 576 deletions
@@ -0,0 +1,83 @@
import { computeBudgetStatus } from "@capakraken/engine";
import type { TRPCContext } from "../trpc.js";
import { fmtEur } from "../lib/format-utils.js";
import { type GraphLink, type GraphNode, l, n } from "./computation-graph-shared.js";
type ResourceBudgetProject = {
id: string;
name: string;
budgetCents: number | null;
winProbability: number;
};
type ResourceBudgetAssignment = {
project: ResourceBudgetProject;
};
export type ResourceBudgetGraph = {
nodes: GraphNode[];
links: GraphLink[];
};
export async function readResourceBudgetGraph(
db: TRPCContext["db"],
assignments: ResourceBudgetAssignment[],
monthStart: Date,
monthEnd: Date,
): Promise<ResourceBudgetGraph> {
const budgetProject = assignments.find((assignment) => (
assignment.project.budgetCents != null && assignment.project.budgetCents > 0
))?.project;
if (!budgetProject?.budgetCents) {
return { nodes: [], links: [] };
}
const projectAllocs = await db.assignment.findMany({
where: { projectId: budgetProject.id },
select: {
status: true,
dailyCostCents: true,
startDate: true,
endDate: true,
hoursPerDay: true,
},
});
const budgetStatus = computeBudgetStatus(
budgetProject.budgetCents,
budgetProject.winProbability,
projectAllocs.map((projectAllocation) => ({
status: projectAllocation.status as unknown as string,
dailyCostCents: projectAllocation.dailyCostCents,
startDate: projectAllocation.startDate,
endDate: projectAllocation.endDate,
hoursPerDay: projectAllocation.hoursPerDay,
})) as Parameters<typeof computeBudgetStatus>[2],
monthStart,
monthEnd,
);
return {
nodes: [
n("input.budgetCents", "Project Budget", fmtEur(budgetProject.budgetCents), "EUR", "INPUT", `Budget for ${budgetProject.name}`, 0),
n("input.winProbability", "Win Probability", `${budgetProject.winProbability}%`, "%", "INPUT", "Project win probability", 0),
n("budget.confirmedCents", "Confirmed", fmtEur(budgetStatus.confirmedCents), "EUR", "BUDGET", "Sum of CONFIRMED/ACTIVE allocation costs", 2, "Σ(confirmed allocs)"),
n("budget.proposedCents", "Proposed", fmtEur(budgetStatus.proposedCents), "EUR", "BUDGET", "Sum of PROPOSED allocation costs", 2, "Σ(proposed allocs)"),
n("budget.allocatedCents", "Allocated", fmtEur(budgetStatus.allocatedCents), "EUR", "BUDGET", "Total allocated budget", 2, "confirmed + proposed"),
n("budget.remainingCents", "Remaining", fmtEur(budgetStatus.remainingCents), "EUR", "BUDGET", "Remaining budget", 3, "budget - allocated"),
n("budget.utilizationPct", "Utilization", `${budgetStatus.utilizationPercent.toFixed(1)}%`, "%", "BUDGET", "Budget utilization percentage", 3, "allocated / budget × 100"),
n("budget.weightedCents", "Win-Weighted", fmtEur(budgetStatus.winProbabilityWeightedCents), "EUR", "BUDGET", "Win-probability-weighted cost", 3, "allocated × winProb / 100"),
],
links: [
l("alloc.totalCostCents", "budget.confirmedCents", "per assignment", 1),
l("budget.confirmedCents", "budget.allocatedCents", "+", 2),
l("budget.proposedCents", "budget.allocatedCents", "+", 2),
l("input.budgetCents", "budget.remainingCents", "", 2),
l("budget.allocatedCents", "budget.remainingCents", "", 2),
l("budget.allocatedCents", "budget.utilizationPct", "÷ budget × 100", 2),
l("input.budgetCents", "budget.utilizationPct", "÷", 1),
l("budget.allocatedCents", "budget.weightedCents", "× winProb / 100", 1),
l("input.winProbability", "budget.weightedCents", "×", 1),
],
};
}