fix(api): reuse cached dashboard detail reads
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user