Files
CapaKraken/packages/api/src/router/computation-graph-resource-budget.ts
T

84 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
],
};
}