fix(dashboard): stabilize budget forecast derivation typing

This commit is contained in:
2026-03-31 22:11:39 +02:00
parent 459ab6911b
commit 1a90f4b930
4 changed files with 220 additions and 8 deletions
@@ -1,4 +1,5 @@
import {
type BudgetForecastRow,
getDashboardBudgetForecast,
getDashboardChargeabilityOverview,
getDashboardDemand,
@@ -66,6 +67,55 @@ type DashboardDemandInput = z.infer<typeof dashboardDemandInputSchema>;
type DashboardDetailInput = z.infer<typeof dashboardDetailInputSchema>;
type DashboardChargeabilityOverviewInput = z.infer<typeof dashboardChargeabilityOverviewInputSchema>;
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;
};
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";
}>;
};
function round1(value: number): number {
return Math.round(value * 10) / 10;
}
@@ -98,7 +148,7 @@ function mapProjectHealthDetailRows(rows: Awaited<ReturnType<typeof getDashboard
};
}
function mapBudgetForecastDetailRows(rows: Awaited<ReturnType<typeof getDashboardBudgetForecast>>) {
function mapBudgetForecastDetailRows(rows: BudgetForecastRow[]): DashboardBudgetForecastDetail {
return {
forecasts: rows.map((forecast) => ({
projectId: forecast.projectId ?? null,
@@ -124,6 +174,7 @@ function mapBudgetForecastDetailRows(rows: Awaited<ReturnType<typeof getDashboar
estimatedExhaustionDate: forecast.estimatedExhaustionDate,
activeAssignmentCount: forecast.activeAssignmentCount ?? null,
calendarLocations: forecast.calendarLocations ?? [],
derivation: forecast.derivation ?? null,
burnStatus: forecast.pctUsed >= 100
? "ahead"
: forecast.burnRate > 0
@@ -374,9 +425,11 @@ export async function getDashboardChargeabilityOverviewRead(
return result;
}
export async function getDashboardBudgetForecastRead(ctx: DashboardProcedureContext) {
export async function getDashboardBudgetForecastRead(
ctx: DashboardProcedureContext,
): Promise<BudgetForecastRow[]> {
const cacheKey = "budgetForecast";
const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardBudgetForecast>>>(cacheKey);
const cached = await cacheGet<BudgetForecastRow[]>(cacheKey);
if (cached) return cached;
const result = await getDashboardBudgetForecast(ctx.db);
@@ -384,8 +437,10 @@ export async function getDashboardBudgetForecastRead(ctx: DashboardProcedureCont
return result;
}
export async function getDashboardBudgetForecastDetail(ctx: DashboardProcedureContext) {
const budgetForecast = await getDashboardBudgetForecast(ctx.db);
export async function getDashboardBudgetForecastDetail(
ctx: DashboardProcedureContext,
): Promise<DashboardBudgetForecastDetail> {
const budgetForecast: BudgetForecastRow[] = await getDashboardBudgetForecast(ctx.db);
return mapBudgetForecastDetailRows(budgetForecast);
}