diff --git a/packages/api/src/__tests__/dashboard-procedure-support.test.ts b/packages/api/src/__tests__/dashboard-procedure-support.test.ts index 94dd89f..d237565 100644 --- a/packages/api/src/__tests__/dashboard-procedure-support.test.ts +++ b/packages/api/src/__tests__/dashboard-procedure-support.test.ts @@ -28,16 +28,21 @@ vi.mock("../lib/anonymization.js", () => ({ })); import { + getDashboardBudgetForecast, getDashboardChargeabilityOverview, getDashboardDemand, getDashboardOverview, getDashboardPeakTimes, + getDashboardProjectHealth, getDashboardTopValueResources, } from "@capakraken/application"; +import { cacheGet } from "../lib/cache.js"; import { anonymizeResources } from "../lib/anonymization.js"; import { + getDashboardBudgetForecastDetail, getDashboardChargeabilityOverviewRead, getDashboardDetail, + getDashboardProjectHealthDetail, getDashboardStatisticsDetail, } from "../router/dashboard-procedure-support.js"; @@ -281,4 +286,217 @@ describe("dashboard procedure support", () => { watchlist: [{ id: "res_2", anonymized: true }], }); }); + + it("reuses cached budget forecast rows for detail payloads", async () => { + vi.mocked(cacheGet).mockResolvedValueOnce([ + { + projectId: "project_1", + projectName: "Apollo", + shortCode: "APO", + clientId: "client_1", + clientName: "Acme", + budgetCents: 500_000, + spentCents: 320_000, + remainingCents: 180_000, + burnRate: 120_000, + estimatedExhaustionDate: "2026-05-31", + pctUsed: 64, + activeAssignmentCount: 3, + calendarLocations: [ + { + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: "Munich", + activeAssignmentCount: 2, + burnRateCents: 80_000, + }, + ], + derivation: { + periodStart: "2026-03-01", + periodEnd: "2026-03-31", + calendarContextCount: 1, + holidayAwareAssignmentCount: 2, + fallbackAssignmentCount: 1, + baseBurnRateCents: 130_000, + adjustedBurnRateCents: 120_000, + publicHolidayDayEquivalent: 1, + publicHolidayCostDeductionCents: 5_000, + absenceDayEquivalent: 0.5, + absenceCostDeductionCents: 5_000, + }, + }, + ]); + + const result = await getDashboardBudgetForecastDetail(createContext()); + + expect(getDashboardBudgetForecast).not.toHaveBeenCalled(); + expect(result).toEqual({ + forecasts: [ + { + projectId: "project_1", + projectName: "Apollo", + shortCode: "APO", + clientId: "client_1", + clientName: "Acme", + budget: "5.000,00 EUR", + budgetCents: 500_000, + spent: "3.200,00 EUR", + spentCents: 320_000, + remaining: "1.800,00 EUR", + remainingCents: 180_000, + projected: "5.000,00 EUR", + projectedCents: 500_000, + burnRate: "1.200,00 EUR", + burnRateCents: 120_000, + utilization: "64%", + estimatedExhaustionDate: "2026-05-31", + activeAssignmentCount: 3, + calendarLocations: [ + { + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: "Munich", + activeAssignmentCount: 2, + burnRateCents: 80_000, + }, + ], + derivation: { + periodStart: "2026-03-01", + periodEnd: "2026-03-31", + calendarContextCount: 1, + holidayAwareAssignmentCount: 2, + fallbackAssignmentCount: 1, + baseBurnRateCents: 130_000, + adjustedBurnRateCents: 120_000, + publicHolidayDayEquivalent: 1, + publicHolidayCostDeductionCents: 5_000, + absenceDayEquivalent: 0.5, + absenceCostDeductionCents: 5_000, + }, + burnStatus: "on_track", + }, + ], + }); + }); + + it("reuses cached project health rows for detail payloads", async () => { + vi.mocked(cacheGet).mockResolvedValueOnce([ + { + id: "project_1", + projectName: "Apollo", + shortCode: "APO", + status: "ACTIVE", + clientId: "client_1", + clientName: "Acme", + budgetHealth: 78, + staffingHealth: 66, + timelineHealth: 55, + compositeScore: 66, + budgetCents: 500_000, + spentCents: 320_000, + remainingBudgetCents: 180_000, + budgetUtilizationPercent: 64, + demandHeadcountTotal: 5, + demandHeadcountFilled: 3, + demandHeadcountOpen: 2, + demandRequirementCount: 2, + plannedEndDate: new Date("2026-06-30T00:00:00.000Z"), + daysUntilEndDate: 107, + timelineStatus: "ON_TRACK" as const, + calendarLocations: [ + { + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: "Munich", + assignmentCount: 3, + spentCents: 320_000, + }, + ], + derivation: { + periodStart: "2026-03-01", + periodEnd: "2026-03-31", + calendarContextCount: 1, + holidayAwareAssignmentCount: 2, + fallbackAssignmentCount: 1, + baseSpentCents: 330_000, + adjustedSpentCents: 320_000, + publicHolidayDayEquivalent: 1, + publicHolidayCostDeductionCents: 5_000, + absenceDayEquivalent: 0.5, + absenceCostDeductionCents: 5_000, + }, + }, + ]); + + const result = await getDashboardProjectHealthDetail(createContext()); + + expect(getDashboardProjectHealth).not.toHaveBeenCalled(); + expect(result).toEqual({ + projects: [ + { + projectId: "project_1", + projectName: "Apollo", + shortCode: "APO", + status: "ACTIVE", + overall: 66, + budget: 78, + staffing: 66, + timeline: 55, + rating: "at_risk", + budgetBasis: { + budgetCents: 500_000, + spentCents: 320_000, + remainingBudgetCents: 180_000, + budgetUtilizationPercent: 64, + calendarLocations: [ + { + countryCode: "DE", + countryName: "Germany", + federalState: "BY", + metroCityName: "Munich", + assignmentCount: 3, + spentCents: 320_000, + }, + ], + derivation: { + periodStart: "2026-03-01", + periodEnd: "2026-03-31", + calendarContextCount: 1, + holidayAwareAssignmentCount: 2, + fallbackAssignmentCount: 1, + baseSpentCents: 330_000, + adjustedSpentCents: 320_000, + publicHolidayDayEquivalent: 1, + publicHolidayCostDeductionCents: 5_000, + absenceDayEquivalent: 0.5, + absenceCostDeductionCents: 5_000, + }, + }, + staffingBasis: { + demandHeadcountTotal: 5, + demandHeadcountFilled: 3, + demandHeadcountOpen: 2, + demandRequirementCount: 2, + }, + timelineBasis: { + plannedEndDate: "2026-06-30T00:00:00.000Z", + daysUntilEndDate: 107, + timelineStatus: "ON_TRACK", + }, + context: { + clientId: "client_1", + clientName: "Acme", + }, + }, + ], + summary: { + healthy: 0, + atRisk: 1, + critical: 0, + }, + }); + }); }); diff --git a/packages/api/src/router/dashboard-procedure-support.ts b/packages/api/src/router/dashboard-procedure-support.ts index 80749d7..4eb9498 100644 --- a/packages/api/src/router/dashboard-procedure-support.ts +++ b/packages/api/src/router/dashboard-procedure-support.ts @@ -340,7 +340,7 @@ export async function getDashboardBudgetForecastRead( export async function getDashboardBudgetForecastDetail( ctx: DashboardProcedureContext, ): Promise { - const budgetForecast: BudgetForecastRow[] = await getDashboardBudgetForecast(ctx.db); + const budgetForecast: BudgetForecastRow[] = await getDashboardBudgetForecastRead(ctx); return mapBudgetForecastDetailRows(budgetForecast); } @@ -375,6 +375,6 @@ export async function getDashboardProjectHealthRead(ctx: DashboardProcedureConte } export async function getDashboardProjectHealthDetail(ctx: DashboardProcedureContext) { - const projectHealth = await getDashboardProjectHealth(ctx.db); + const projectHealth = await getDashboardProjectHealthRead(ctx); return mapProjectHealthDetailRows(projectHealth); }