84 lines
3.6 KiB
TypeScript
84 lines
3.6 KiB
TypeScript
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),
|
||
],
|
||
};
|
||
}
|