fix(api): reuse cached dashboard detail reads
This commit is contained in:
@@ -28,16 +28,21 @@ vi.mock("../lib/anonymization.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getDashboardBudgetForecast,
|
||||||
getDashboardChargeabilityOverview,
|
getDashboardChargeabilityOverview,
|
||||||
getDashboardDemand,
|
getDashboardDemand,
|
||||||
getDashboardOverview,
|
getDashboardOverview,
|
||||||
getDashboardPeakTimes,
|
getDashboardPeakTimes,
|
||||||
|
getDashboardProjectHealth,
|
||||||
getDashboardTopValueResources,
|
getDashboardTopValueResources,
|
||||||
} from "@capakraken/application";
|
} from "@capakraken/application";
|
||||||
|
import { cacheGet } from "../lib/cache.js";
|
||||||
import { anonymizeResources } from "../lib/anonymization.js";
|
import { anonymizeResources } from "../lib/anonymization.js";
|
||||||
import {
|
import {
|
||||||
|
getDashboardBudgetForecastDetail,
|
||||||
getDashboardChargeabilityOverviewRead,
|
getDashboardChargeabilityOverviewRead,
|
||||||
getDashboardDetail,
|
getDashboardDetail,
|
||||||
|
getDashboardProjectHealthDetail,
|
||||||
getDashboardStatisticsDetail,
|
getDashboardStatisticsDetail,
|
||||||
} from "../router/dashboard-procedure-support.js";
|
} from "../router/dashboard-procedure-support.js";
|
||||||
|
|
||||||
@@ -281,4 +286,217 @@ describe("dashboard procedure support", () => {
|
|||||||
watchlist: [{ id: "res_2", anonymized: true }],
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ export async function getDashboardBudgetForecastRead(
|
|||||||
export async function getDashboardBudgetForecastDetail(
|
export async function getDashboardBudgetForecastDetail(
|
||||||
ctx: DashboardProcedureContext,
|
ctx: DashboardProcedureContext,
|
||||||
): Promise<DashboardBudgetForecastDetail> {
|
): Promise<DashboardBudgetForecastDetail> {
|
||||||
const budgetForecast: BudgetForecastRow[] = await getDashboardBudgetForecast(ctx.db);
|
const budgetForecast: BudgetForecastRow[] = await getDashboardBudgetForecastRead(ctx);
|
||||||
return mapBudgetForecastDetailRows(budgetForecast);
|
return mapBudgetForecastDetailRows(budgetForecast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +375,6 @@ export async function getDashboardProjectHealthRead(ctx: DashboardProcedureConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getDashboardProjectHealthDetail(ctx: DashboardProcedureContext) {
|
export async function getDashboardProjectHealthDetail(ctx: DashboardProcedureContext) {
|
||||||
const projectHealth = await getDashboardProjectHealth(ctx.db);
|
const projectHealth = await getDashboardProjectHealthRead(ctx);
|
||||||
return mapProjectHealthDetailRows(projectHealth);
|
return mapProjectHealthDetailRows(projectHealth);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user