import { computeBudgetStatus } from "@nexus/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), ], }; }