refactor(api): modularize computation graph resource snapshot
This commit is contained in:
@@ -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),
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user