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