chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import type { Allocation, BudgetStatus, BudgetWarning } from "@planarchy/shared";
|
||||
import { BUDGET_WARNING_THRESHOLDS } from "@planarchy/shared";
|
||||
|
||||
/**
|
||||
* Computes budget status for a project given its allocations.
|
||||
* Pure function — all data passed as parameters.
|
||||
*/
|
||||
export function computeBudgetStatus(
|
||||
budgetCents: number,
|
||||
winProbability: number,
|
||||
allocations: Pick<Allocation, "status" | "dailyCostCents" | "startDate" | "endDate" | "hoursPerDay">[],
|
||||
projectStartDate: Date,
|
||||
projectEndDate: Date,
|
||||
): BudgetStatus {
|
||||
const activeStatuses = new Set(["CONFIRMED", "ACTIVE"]);
|
||||
const proposedStatuses = new Set(["PROPOSED"]);
|
||||
|
||||
let confirmedCents = 0;
|
||||
let proposedCents = 0;
|
||||
|
||||
for (const alloc of allocations) {
|
||||
const days = countWorkingDaysInRange(
|
||||
new Date(alloc.startDate),
|
||||
new Date(alloc.endDate),
|
||||
);
|
||||
const totalCents = alloc.dailyCostCents * days;
|
||||
|
||||
if (activeStatuses.has(alloc.status)) {
|
||||
confirmedCents += totalCents;
|
||||
} else if (proposedStatuses.has(alloc.status)) {
|
||||
proposedCents += totalCents;
|
||||
}
|
||||
}
|
||||
|
||||
const allocatedCents = confirmedCents + proposedCents;
|
||||
const remainingCents = budgetCents - allocatedCents;
|
||||
const utilizationPercent = budgetCents > 0 ? (allocatedCents / budgetCents) * 100 : 0;
|
||||
const winProbabilityWeightedCents = Math.round(allocatedCents * (winProbability / 100));
|
||||
|
||||
const warnings: BudgetWarning[] = [];
|
||||
|
||||
if (utilizationPercent >= BUDGET_WARNING_THRESHOLDS.CRITICAL) {
|
||||
warnings.push({
|
||||
level: "critical",
|
||||
code: "BUDGET_CRITICAL",
|
||||
message: `Budget utilization at ${utilizationPercent.toFixed(1)}% — critical threshold exceeded`,
|
||||
thresholdPercent: BUDGET_WARNING_THRESHOLDS.CRITICAL,
|
||||
currentPercent: utilizationPercent,
|
||||
});
|
||||
} else if (utilizationPercent >= BUDGET_WARNING_THRESHOLDS.WARNING) {
|
||||
warnings.push({
|
||||
level: "warning",
|
||||
code: "BUDGET_WARNING",
|
||||
message: `Budget utilization at ${utilizationPercent.toFixed(1)}% — approaching limit`,
|
||||
thresholdPercent: BUDGET_WARNING_THRESHOLDS.WARNING,
|
||||
currentPercent: utilizationPercent,
|
||||
});
|
||||
} else if (utilizationPercent >= BUDGET_WARNING_THRESHOLDS.INFO) {
|
||||
warnings.push({
|
||||
level: "info",
|
||||
code: "BUDGET_INFO",
|
||||
message: `Budget utilization at ${utilizationPercent.toFixed(1)}%`,
|
||||
thresholdPercent: BUDGET_WARNING_THRESHOLDS.INFO,
|
||||
currentPercent: utilizationPercent,
|
||||
});
|
||||
}
|
||||
|
||||
if (allocatedCents > budgetCents) {
|
||||
warnings.push({
|
||||
level: "critical",
|
||||
code: "BUDGET_EXCEEDED",
|
||||
message: `Budget exceeded by ${((allocatedCents - budgetCents) / 100).toFixed(2)} EUR`,
|
||||
thresholdPercent: 100,
|
||||
currentPercent: utilizationPercent,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
budgetCents,
|
||||
allocatedCents,
|
||||
confirmedCents,
|
||||
proposedCents,
|
||||
remainingCents,
|
||||
utilizationPercent,
|
||||
winProbabilityWeightedCents,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/** Simple working day counter (Mon-Fri) for budget calculations */
|
||||
function countWorkingDaysInRange(startDate: Date, endDate: Date): number {
|
||||
let count = 0;
|
||||
const current = new Date(startDate);
|
||||
current.setHours(0, 0, 0, 0);
|
||||
const end = new Date(endDate);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
|
||||
while (current <= end) {
|
||||
const dow = current.getDay();
|
||||
if (dow !== 0 && dow !== 6) {
|
||||
count++;
|
||||
}
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
Reference in New Issue
Block a user