refactor(api): split dashboard detail shaping

This commit is contained in:
2026-03-31 22:26:52 +02:00
parent a9028290f2
commit 46d00c2635
3 changed files with 373 additions and 113 deletions
@@ -0,0 +1,213 @@
import { type BudgetForecastRow, getDashboardProjectHealth } from "@capakraken/application";
import { fmtEur } from "../lib/format-utils.js";
type DashboardBudgetForecastCalendarLocation = {
countryCode: string | null;
countryName: string | null;
federalState: string | null;
metroCityName: string | null;
activeAssignmentCount: number;
burnRateCents: number;
};
type DashboardBudgetForecastDerivation = {
periodStart: string;
periodEnd: string;
calendarContextCount: number;
holidayAwareAssignmentCount: number;
fallbackAssignmentCount: number;
baseBurnRateCents: number;
adjustedBurnRateCents: number;
publicHolidayDayEquivalent: number;
publicHolidayCostDeductionCents: number;
absenceDayEquivalent: number;
absenceCostDeductionCents: number;
};
export type DashboardBudgetForecastDetail = {
forecasts: Array<{
projectId: string | null;
projectName: string;
shortCode: string;
clientId: string | null;
clientName: string | null;
budget: string;
budgetCents: number;
spent: string;
spentCents: number;
remaining: string;
remainingCents: number;
projected: string;
projectedCents: number;
burnRate: string;
burnRateCents: number;
utilization: string;
estimatedExhaustionDate: string | null;
activeAssignmentCount: number | null;
calendarLocations: DashboardBudgetForecastCalendarLocation[];
derivation: DashboardBudgetForecastDerivation | null;
burnStatus: "ahead" | "on_track" | "not_started";
}>;
};
type DashboardProjectHealthCalendarLocation = {
countryCode: string | null;
countryName: string | null;
federalState: string | null;
metroCityName: string | null;
assignmentCount: number;
spentCents: number;
};
type DashboardProjectHealthDerivation = {
periodStart: string;
periodEnd: string;
calendarContextCount: number;
holidayAwareAssignmentCount: number;
fallbackAssignmentCount: number;
baseSpentCents: number;
adjustedSpentCents: number;
publicHolidayDayEquivalent: number;
publicHolidayCostDeductionCents: number;
absenceDayEquivalent: number;
absenceCostDeductionCents: number;
};
export type DashboardProjectHealthDetail = {
projects: Array<{
projectId: string;
projectName: string;
shortCode: string;
status: string;
overall: number;
budget: number;
staffing: number;
timeline: number;
rating: "healthy" | "at_risk" | "critical";
budgetBasis: {
budgetCents: number | null;
spentCents: number;
remainingBudgetCents: number | null;
budgetUtilizationPercent: number | null;
calendarLocations: DashboardProjectHealthCalendarLocation[];
derivation: DashboardProjectHealthDerivation | null;
};
staffingBasis: {
demandHeadcountTotal: number;
demandHeadcountFilled: number;
demandHeadcountOpen: number;
demandRequirementCount: number;
};
timelineBasis: {
plannedEndDate: string | null;
daysUntilEndDate: number | null;
timelineStatus: "ON_TRACK" | "DUE_SOON" | "OVERDUE" | "UNSCHEDULED";
};
context: {
clientId: string | null;
clientName: string | null;
};
}>;
summary: {
healthy: number;
atRisk: number;
critical: number;
};
};
export function mapProjectHealthDetailRows(
rows: Awaited<ReturnType<typeof getDashboardProjectHealth>>,
): DashboardProjectHealthDetail {
const projects: DashboardProjectHealthDetail["projects"] = rows
.map((project): DashboardProjectHealthDetail["projects"][number] => {
const overall = project.compositeScore;
const rating: DashboardProjectHealthDetail["projects"][number]["rating"] = overall >= 80
? "healthy"
: overall >= 50
? "at_risk"
: "critical";
return {
projectId: project.id,
projectName: project.projectName,
shortCode: project.shortCode,
status: project.status,
overall,
budget: project.budgetHealth,
staffing: project.staffingHealth,
timeline: project.timelineHealth,
rating,
budgetBasis: {
budgetCents: project.budgetCents ?? null,
spentCents: project.spentCents ?? 0,
remainingBudgetCents: project.remainingBudgetCents ?? null,
budgetUtilizationPercent: project.budgetUtilizationPercent ?? null,
calendarLocations: project.calendarLocations ?? [],
derivation: project.derivation ?? null,
},
staffingBasis: {
demandHeadcountTotal: project.demandHeadcountTotal ?? 0,
demandHeadcountFilled: project.demandHeadcountFilled ?? 0,
demandHeadcountOpen: project.demandHeadcountOpen ?? 0,
demandRequirementCount: project.demandRequirementCount ?? 0,
},
timelineBasis: {
plannedEndDate: project.plannedEndDate?.toISOString() ?? null,
daysUntilEndDate: project.daysUntilEndDate ?? null,
timelineStatus: project.timelineStatus ?? "UNSCHEDULED",
},
context: {
clientId: project.clientId ?? null,
clientName: project.clientName ?? null,
},
};
})
.sort((left, right) => left.overall - right.overall);
return {
projects,
summary: {
healthy: projects.filter((project) => project.rating === "healthy").length,
atRisk: projects.filter((project) => project.rating === "at_risk").length,
critical: projects.filter((project) => project.rating === "critical").length,
},
};
}
export function mapBudgetForecastDetailRows(
rows: BudgetForecastRow[],
): DashboardBudgetForecastDetail {
return {
forecasts: rows.map((forecast) => ({
projectId: forecast.projectId ?? null,
projectName: forecast.projectName,
shortCode: forecast.shortCode,
clientId: forecast.clientId,
clientName: forecast.clientName,
budget: fmtEur(forecast.budgetCents),
budgetCents: forecast.budgetCents,
spent: fmtEur(forecast.spentCents),
spentCents: forecast.spentCents,
remaining: fmtEur(forecast.remainingCents ?? (forecast.budgetCents - forecast.spentCents)),
remainingCents: forecast.remainingCents ?? (forecast.budgetCents - forecast.spentCents),
projected: forecast.burnRate > 0
? fmtEur(forecast.spentCents + Math.max(0, forecast.budgetCents - forecast.spentCents))
: fmtEur(forecast.spentCents),
projectedCents: forecast.burnRate > 0
? Math.max(forecast.spentCents, forecast.budgetCents)
: forecast.spentCents,
burnRate: fmtEur(forecast.burnRate),
burnRateCents: forecast.burnRate,
utilization: `${forecast.pctUsed}%`,
estimatedExhaustionDate: forecast.estimatedExhaustionDate,
activeAssignmentCount: forecast.activeAssignmentCount ?? null,
calendarLocations: forecast.calendarLocations ?? [],
derivation: forecast.derivation ?? null,
burnStatus: forecast.pctUsed >= 100
? "ahead"
: forecast.burnRate > 0
? "on_track"
: "not_started",
})),
};
}