import { describe, expect, it, vi } from "vitest"; import { getDashboardBudgetForecast, getDashboardChargeabilityOverview, getDashboardDemand, getDashboardOverview, getDashboardPeakTimes, getDashboardProjectHealth, getDashboardTopValueResources, } from "../index.js"; describe("dashboard use-cases", () => { it("computes overview budget summary from project budgets and allocation cost", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assignment_1", projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 1_000, status: "ACTIVE", project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED", dynamicFields: null, }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI", }, }, ]), }, resource: { count: vi .fn() .mockResolvedValueOnce(4) .mockResolvedValueOnce(3), findMany: vi.fn().mockResolvedValue([ { chapter: "CGI", chargeabilityTarget: 80 }, { chapter: "CGI", chargeabilityTarget: 60 }, { chapter: null, chargeabilityTarget: null }, ]), }, project: { count: vi.fn().mockResolvedValue(2), findMany: vi.fn().mockResolvedValue([ { status: "ACTIVE", budgetCents: 100_000 }, { status: "DRAFT", budgetCents: 50_000 }, ]), }, demandRequirement: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { findMany: vi.fn().mockResolvedValue([ { id: "audit_1", entityType: "Allocation", action: "CREATE", createdAt: new Date("2026-03-05T10:00:00.000Z"), }, ]), }, vacation: { count: vi.fn().mockResolvedValue(2), }, estimate: { count: vi.fn().mockResolvedValue(5), }, }; const result = await getDashboardOverview(db as never); expect(result.budgetSummary).toEqual({ totalBudgetCents: 150_000, totalCostCents: 3_000, avgUtilizationPercent: 2, }); expect(result.projectsByStatus).toEqual([ { status: "ACTIVE", count: 1 }, { status: "DRAFT", count: 1 }, ]); expect(result.approvedVacations).toBe(2); expect(result.totalEstimates).toBe(5); expect(result.chapterUtilization).toEqual([ { chapter: "CGI", resourceCount: 2, avgChargeabilityTarget: 70 }, { chapter: "Unassigned", resourceCount: 1, avgChargeabilityTarget: 0 }, ]); }); it("avoids double-counting linked legacy allocations in overview budget totals", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assignment_1", projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 2_000, status: "ACTIVE", project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED", }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI", }, }, ]), }, resource: { count: vi .fn() .mockResolvedValueOnce(1) .mockResolvedValueOnce(1), findMany: vi.fn().mockResolvedValue([{ chapter: "CGI", chargeabilityTarget: 80 }]), }, project: { count: vi.fn().mockResolvedValue(1), findMany: vi.fn().mockResolvedValue([{ status: "ACTIVE", budgetCents: 100_000 }]), }, demandRequirement: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { findMany: vi.fn().mockResolvedValue([]), }, vacation: { count: vi.fn().mockResolvedValue(0), }, estimate: { count: vi.fn().mockResolvedValue(0), }, }; const result = await getDashboardOverview(db as never); expect(result.budgetSummary).toEqual({ totalBudgetCents: 100_000, totalCostCents: 4_000, avgUtilizationPercent: 4, }); }); it("counts explicit demand and assignment rows in overview totals even without legacy allocation rows", async () => { const db = { assignment: { findMany: vi .fn() .mockResolvedValueOnce([ { id: "assignment_explicit", demandRequirementId: "demand_explicit", projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", dailyCostCents: 2_000, status: "ACTIVE", metadata: {}, createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), }, ]) .mockResolvedValueOnce([ { id: "assignment_explicit", projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 2_000, status: "ACTIVE", project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED", }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI", }, }, ]), }, resource: { count: vi .fn() .mockResolvedValueOnce(1) .mockResolvedValueOnce(1), findMany: vi.fn().mockResolvedValue([{ chapter: "CGI", chargeabilityTarget: 80 }]), }, project: { count: vi.fn().mockResolvedValue(1), findMany: vi.fn().mockResolvedValue([{ status: "ACTIVE", budgetCents: 100_000 }]), }, demandRequirement: { findMany: vi.fn().mockResolvedValue([ { id: "demand_explicit", projectId: "proj_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: "Compositor", roleId: "role_comp", headcount: 1, status: "ACTIVE", metadata: {}, createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), }, { id: "demand_cancelled", projectId: "proj_2", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 4, percentage: 50, role: "FX", roleId: "role_fx", headcount: 1, status: "CANCELLED", metadata: {}, createdAt: new Date("2026-03-03T00:00:00.000Z"), updatedAt: new Date("2026-03-03T00:00:00.000Z"), }, ]), }, auditLog: { findMany: vi.fn().mockResolvedValue([]), }, vacation: { count: vi.fn().mockResolvedValue(1), }, estimate: { count: vi.fn().mockResolvedValue(2), }, }; const result = await getDashboardOverview(db as never); expect(result.totalAllocations).toBe(3); expect(result.activeAllocations).toBe(2); expect(result.approvedVacations).toBe(1); expect(result.totalEstimates).toBe(2); expect(result.budgetSummary).toEqual({ totalBudgetCents: 100_000, totalCostCents: 4_000, avgUtilizationPercent: 4, }); }); it("aggregates peak times into sorted buckets and capacity totals", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assign_1", projectId: "proj_1", resourceId: "res_1", status: "PROPOSED", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 4, dailyCostCents: 0, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED" }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, { id: "assign_2", projectId: "proj_2", resourceId: "res_2", status: "PROPOSED", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 3, dailyCostCents: 0, project: { id: "proj_2", name: "Bravo", shortCode: "BRAVO", status: "ACTIVE", orderType: "FIXED" }, resource: { id: "res_2", displayName: "Bob", chapter: "Lighting" }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", displayName: "Alice", chapter: "CGI", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: null, metroCityId: "city_1", country: { code: "DE" }, metroCity: { name: "Munich" }, }, { id: "res_2", displayName: "Bob", chapter: "Lighting", availability: { monday: 6, tuesday: 6, wednesday: 6, thursday: 6, friday: 6 }, countryId: "country_de", federalState: null, metroCityId: "city_2", country: { code: "DE" }, metroCity: { name: "Hamburg" }, }, ]), }, }; const result = await getDashboardPeakTimes(db as never, { startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), granularity: "month", groupBy: "project", }); expect(result).toEqual([ expect.objectContaining({ period: "2026-03", groups: [ { name: "ALPHA", hours: 4 }, { name: "BRAVO", hours: 3 }, ], totalHours: 7, capacityHours: 28, derivation: expect.objectContaining({ baseAvailableHours: 28, effectiveAvailableHours: 28, publicHolidayHoursDeduction: 0, absenceDayEquivalent: 0, absenceHoursDeduction: 0, bookedHours: 7, capacityHours: 28, remainingCapacityHours: 21, overbookedHours: 0, utilizationPct: 25, groupCount: 2, resourceCount: 2, }), }), ]); }); it("provides department capacity and utilization details for chapter grouping", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assign_1", projectId: "proj_1", resourceId: "res_1", status: "ACTIVE", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 0, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED" }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", displayName: "Alice", chapter: "CGI", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: null, metroCityId: "city_1", country: { code: "DE" }, metroCity: { name: "Munich" }, }, { id: "res_2", displayName: "Bob", chapter: "Lighting", availability: { monday: 4, tuesday: 4, wednesday: 4, thursday: 4, friday: 4 }, countryId: "country_de", federalState: null, metroCityId: "city_2", country: { code: "DE" }, metroCity: { name: "Hamburg" }, }, ]), }, }; const result = await getDashboardPeakTimes(db as never, { startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), granularity: "month", groupBy: "chapter", }); expect(result).toEqual([ expect.objectContaining({ period: "2026-03", groups: [ expect.objectContaining({ name: "CGI", hours: 8, capacityHours: 8, remainingHours: 0, overbookedHours: 0, utilizationPct: 100, }), expect.objectContaining({ name: "Lighting", hours: 0, capacityHours: 4, remainingHours: 4, overbookedHours: 0, utilizationPct: 0, }), ], totalHours: 8, capacityHours: 12, derivation: expect.objectContaining({ baseAvailableHours: 12, effectiveAvailableHours: 12, publicHolidayHoursDeduction: 0, absenceDayEquivalent: 0, absenceHoursDeduction: 0, bookedHours: 8, capacityHours: 12, remainingCapacityHours: 4, overbookedHours: 0, utilizationPct: 67, groupCount: 2, resourceCount: 2, }), }), ]); }); it("enforces visible-role filtering for top value resources", async () => { const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ scoreVisibleRoles: ["ADMIN"], }), }, resource: { findMany: vi.fn(), }, }; const hidden = await getDashboardTopValueResources(db as never, { limit: 10, userRole: "USER", }); expect(hidden).toEqual([]); expect(db.resource.findMany).not.toHaveBeenCalled(); db.resource.findMany.mockResolvedValue([ { id: "res_1", eid: "alice", displayName: "Alice", chapter: "Delivery", valueScore: 99, valueScoreBreakdown: { skillDepth: 90, skillBreadth: 80, costEfficiency: 85, chargeability: 88, experience: 92, total: 99, }, valueScoreUpdatedAt: new Date("2026-03-01T00:00:00.000Z"), lcrCents: 12_300, country: { code: "DE", name: "Germany" }, federalState: "BY", metroCity: { name: "Munich" }, }, ]); const visible = await getDashboardTopValueResources(db as never, { limit: 1, userRole: "ADMIN", }); expect(visible).toEqual([ { id: "res_1", eid: "alice", displayName: "Alice", chapter: "Delivery", valueScore: 99, valueScoreBreakdown: { skillDepth: 90, skillBreadth: 80, costEfficiency: 85, chargeability: 88, experience: 92, total: 99, }, valueScoreUpdatedAt: new Date("2026-03-01T00:00:00.000Z"), lcrCents: 12_300, countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", }, ]); expect(db.resource.findMany).toHaveBeenCalledWith( expect.objectContaining({ take: 1, select: expect.objectContaining({ valueScoreBreakdown: true, valueScoreUpdatedAt: true, country: { select: { code: true, name: true } }, federalState: true, metroCity: { select: { name: true } }, }), }), ); }); it("keeps proposed allocations out of actual chargeability by default but can include them", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assign_1", projectId: "proj_1", resourceId: "res_1", status: "PROPOSED", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 0, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED", }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI", }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", eid: "alice", displayName: "Alice", chapter: "CGI", chargeabilityTarget: 80, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, }, ]), }, }; const strict = await getDashboardChargeabilityOverview(db as never, { now: new Date("2026-03-15T00:00:00.000Z"), topN: 10, watchlistThreshold: 15, }); const withProposed = await getDashboardChargeabilityOverview(db as never, { includeProposed: true, now: new Date("2026-03-15T00:00:00.000Z"), topN: 10, watchlistThreshold: 15, }); expect(strict.top[0]?.actualChargeability).toBe(0); expect(strict.top[0]?.expectedChargeability).toBe(5); expect(withProposed.top[0]?.actualChargeability).toBe(5); expect(withProposed.top[0]?.expectedChargeability).toBe(5); }); it("filters chargeability overview by departed state and country", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", eid: "alice", displayName: "Alice", chapter: "CGI", countryId: "country_de", departed: false, chargeabilityTarget: 80, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, }, ]), }, }; const result = await getDashboardChargeabilityOverview(db as never, { now: new Date("2026-03-15T00:00:00.000Z"), topN: 10, watchlistThreshold: 15, countryIds: ["country_de"], departed: false, }); expect(db.resource.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ isActive: true, countryId: { in: ["country_de"] }, departed: false, }), }), ); expect(result.rows).toHaveLength(1); expect(result.top).toHaveLength(1); expect(result.top[0]).toEqual( expect.objectContaining({ id: "res_1", countryId: "country_de", departed: false, }), ); }); it("includes imported TBD draft projects in actual chargeability only when proposed work is enabled", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assign_tbd", projectId: "proj_tbd", resourceId: "res_1", status: "PROPOSED", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 0, project: { id: "proj_tbd", name: "TBD: AMG", shortCode: "TBD-AMG", status: "DRAFT", orderType: "CLIENT", dynamicFields: { dispoImport: { isTbd: true } }, }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI", }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", eid: "alice", displayName: "Alice", chapter: "CGI", chargeabilityTarget: 80, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, }, ]), }, }; const strict = await getDashboardChargeabilityOverview(db as never, { now: new Date("2026-03-15T00:00:00.000Z"), topN: 10, watchlistThreshold: 15, }); const withProposed = await getDashboardChargeabilityOverview(db as never, { includeProposed: true, now: new Date("2026-03-15T00:00:00.000Z"), topN: 10, watchlistThreshold: 15, }); expect(strict.top[0]?.actualChargeability).toBe(0); expect(strict.top[0]?.expectedChargeability).toBe(5); expect(withProposed.top[0]?.actualChargeability).toBe(5); expect(withProposed.top[0]?.expectedChargeability).toBe(5); }); it("excludes regional public holidays from dashboard chargeability availability and bookings", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assign_holiday", projectId: "proj_1", resourceId: "res_by", status: "CONFIRMED", startDate: new Date("2026-01-06T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 0, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "res_by", displayName: "Bruce", chapter: "CGI", }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_by", eid: "bruce.banner", displayName: "Bruce", chapter: "CGI", countryId: "country_de", federalState: "BY", metroCityId: "city_augsburg", departed: false, chargeabilityTarget: 80, country: { id: "country_de", code: "DE", }, metroCity: { id: "city_augsburg", name: "Augsburg", }, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, }, ]), }, }; const result = await getDashboardChargeabilityOverview(db as never, { now: new Date("2026-01-15T00:00:00.000Z"), topN: 10, watchlistThreshold: 15, }); expect(result.rows).toHaveLength(1); expect(result.top[0]?.actualChargeability).toBe(0); expect(result.top[0]?.expectedChargeability).toBe(0); expect(result.top[0]).toEqual( expect.objectContaining({ countryCode: "DE", federalState: "BY", metroCityName: "Augsburg", derivation: expect.objectContaining({ weeklyAvailabilityHours: 40, baseAvailableHours: 184, effectiveAvailableHours: 168, publicHolidayCount: 2, publicHolidayWorkdayCount: 2, publicHolidayHoursDeduction: 16, absenceHoursDeduction: 0, actualBookedHours: 0, expectedBookedHours: 0, targetBookedHours: 134.4, unassignedHours: 168, }), }), ); }); it("uses holiday-aware capacity in peak times for regional calendars", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assign_1", projectId: "proj_1", resourceId: "res_by", status: "CONFIRMED", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 0, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", status: "ACTIVE", orderType: "FIXED" }, resource: { id: "res_by", displayName: "Bruce", chapter: "CGI" }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_by", displayName: "Bruce", chapter: "CGI", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE" }, metroCity: { name: "Munich" }, }, ]), }, }; const result = await getDashboardPeakTimes(db as never, { startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), granularity: "month", groupBy: "project", }); expect(result).toEqual([ expect.objectContaining({ period: "2026-01", groups: [{ name: "ALPHA", hours: 8 }], totalHours: 8, capacityHours: 8, derivation: expect.objectContaining({ baseAvailableHours: 16, effectiveAvailableHours: 8, publicHolidayHoursDeduction: 8, absenceDayEquivalent: 0, absenceHoursDeduction: 0, bookedHours: 8, capacityHours: 8, remainingCapacityHours: 0, overbookedHours: 0, utilizationPct: 100, groupCount: 1, resourceCount: 1, }), }), ]); }); it("exposes holiday and approved-absence deductions in peak times derivation", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_by", displayName: "Bruce", chapter: "CGI", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE" }, metroCity: { name: "Munich" }, }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([ { resourceId: "res_by", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-05T00:00:00.000Z"), type: "VACATION", isHalfDay: false, }, ]), }, }; const result = await getDashboardPeakTimes(db as never, { startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), granularity: "month", groupBy: "chapter", }); expect(result).toEqual([ expect.objectContaining({ period: "2026-01", totalHours: 0, capacityHours: 0, derivation: expect.objectContaining({ baseAvailableHours: 16, effectiveAvailableHours: 0, publicHolidayHoursDeduction: 8, absenceDayEquivalent: 1, absenceHoursDeduction: 8, bookedHours: 0, capacityHours: 0, remainingCapacityHours: 0, overbookedHours: 0, utilizationPct: 0, groupCount: 0, resourceCount: 1, }), }), ]); }); it("exposes calendar context summaries in peak times derivation", async () => { const db = { assignment: { findMany: vi.fn().mockResolvedValue([]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_by", displayName: "Bruce", chapter: "CGI", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE", name: "Germany" }, metroCity: { name: "Munich" }, }, { id: "res_hh", displayName: "Harvey", chapter: "CGI", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "HH", metroCityId: "city_hamburg", country: { code: "DE", name: "Germany" }, metroCity: { name: "Hamburg" }, }, ]), }, }; const result = await getDashboardPeakTimes(db as never, { startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), granularity: "month", groupBy: "chapter", }); expect(result).toEqual([ expect.objectContaining({ period: "2026-01", derivation: expect.objectContaining({ calendarContextCount: 2, calendarLocations: [ expect.objectContaining({ countryCode: "DE", countryName: "Germany", federalState: "HH", metroCityName: "Hamburg", resourceCount: 1, effectiveAvailableHours: 16, }), expect.objectContaining({ countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", resourceCount: 1, effectiveAvailableHours: 8, }), ], }), }), ]); }); it("does not burn budget on regional public holidays", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([ { id: "proj_1", name: "Alpha", shortCode: "ALPHA", budgetCents: 10_000, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-12-31T00:00:00.000Z"), clientId: null, client: null, }, ]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { projectId: "proj_1", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), dailyCostCents: 1_000, resource: { id: "res_by", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE" }, metroCity: { name: "Munich" }, }, }, ]), }, }; const result = await getDashboardBudgetForecast(db as never); expect(result).toEqual([ expect.objectContaining({ projectId: "proj_1", shortCode: "ALPHA", spentCents: 1_000, remainingCents: 9_000, activeAssignmentCount: 0, calendarLocations: [], }), ]); }); it("returns burn derivation and calendar basis for active project forecasts", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T00:00:00.000Z")); try { const db = { project: { findMany: vi.fn().mockResolvedValue([ { id: "proj_1", name: "Gelddruckmaschine", shortCode: "GDM", budgetCents: 100_000, startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-12-31T00:00:00.000Z"), clientId: "client_1", client: { name: "ACME" }, }, ]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { projectId: "proj_1", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), dailyCostCents: 1_000, resource: { id: "res_by", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE", name: "Deutschland" }, metroCity: { name: "Munich" }, }, }, { projectId: "proj_1", startDate: new Date("2026-01-15T00:00:00.000Z"), endDate: new Date("2026-01-16T00:00:00.000Z"), dailyCostCents: 2_000, resource: { id: "res_hh", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "HH", metroCityId: "city_hamburg", country: { code: "DE", name: "Deutschland" }, metroCity: { name: "Hamburg" }, }, }, ]), }, }; const result = await getDashboardBudgetForecast(db as never); expect(result).toEqual([ expect.objectContaining({ projectId: "proj_1", shortCode: "GDM", budgetCents: 100_000, spentCents: 5_000, remainingCents: 95_000, burnRate: 4_000, activeAssignmentCount: 1, derivation: expect.objectContaining({ calendarContextCount: 1, holidayAwareAssignmentCount: 1, fallbackAssignmentCount: 0, baseBurnRateCents: 4_000, adjustedBurnRateCents: 4_000, publicHolidayDayEquivalent: 0, publicHolidayCostDeductionCents: 0, absenceDayEquivalent: 0, absenceCostDeductionCents: 0, }), calendarLocations: [ expect.objectContaining({ countryCode: "DE", countryName: "Deutschland", federalState: "HH", metroCityName: "Hamburg", activeAssignmentCount: 1, burnRateCents: 4_000, }), ], }), ]); } finally { vi.useRealTimers(); } }); it("excludes regional public holidays from overview budget totals", async () => { const db = { assignment: { findMany: vi .fn() .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), dailyCostCents: 1_000, resource: { id: "res_by", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE" }, metroCity: { name: "Munich" }, }, }, ]), }, resource: { count: vi .fn() .mockResolvedValueOnce(1) .mockResolvedValueOnce(1), findMany: vi.fn().mockResolvedValue([{ chapter: "CGI", chargeabilityTarget: 80 }]), }, project: { count: vi.fn().mockResolvedValue(1), findMany: vi.fn().mockResolvedValue([{ status: "ACTIVE", budgetCents: 10_000 }]), }, demandRequirement: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { findMany: vi.fn().mockResolvedValue([]), }, vacation: { count: vi.fn().mockResolvedValue(1), }, estimate: { count: vi.fn().mockResolvedValue(4), }, }; const result = await getDashboardOverview(db as never); expect(result.budgetSummary).toEqual({ totalBudgetCents: 10_000, totalCostCents: 1_000, avgUtilizationPercent: 10, }); expect(result.approvedVacations).toBe(1); expect(result.totalEstimates).toBe(4); }); it("excludes regional public holidays from project health budget usage", async () => { const db = { project: { findMany: vi.fn().mockResolvedValue([ { id: "proj_1", name: "Alpha", shortCode: "ALPHA", budgetCents: 10_000, endDate: new Date("2026-12-31T00:00:00.000Z"), clientId: null, client: null, demandRequirements: [], }, ]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { projectId: "proj_1", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), dailyCostCents: 1_000, resource: { id: "res_by", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_munich", country: { code: "DE" }, metroCity: { name: "Munich" }, }, }, ]), }, }; const result = await getDashboardProjectHealth(db as never); expect(result).toEqual([ expect.objectContaining({ shortCode: "ALPHA", budgetHealth: 90, spentCents: 1_000, budgetUtilizationPercent: 10, derivation: expect.objectContaining({ periodStart: "2026-01-05", periodEnd: "2026-01-06", calendarContextCount: 1, holidayAwareAssignmentCount: 1, fallbackAssignmentCount: 0, baseSpentCents: 2_000, adjustedSpentCents: 1_000, publicHolidayDayEquivalent: 1, publicHolidayCostDeductionCents: 1_000, absenceDayEquivalent: 0, absenceCostDeductionCents: 0, }), calendarLocations: [ expect.objectContaining({ countryCode: "DE", federalState: "BY", metroCityName: "Munich", assignmentCount: 1, spentCents: 1_000, }), ], }), ]); }); it("returns distinct resource counts for chapter demand grouping", async () => { const db = { demandRequirement: { findMany: vi.fn().mockResolvedValue([]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "alloc_1", demandRequirementId: null, projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 4, percentage: 50, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [], }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, { id: "alloc_2", demandRequirementId: null, projectId: "proj_2", resourceId: "res_1", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 6, percentage: 75, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-03T00:00:00.000Z"), updatedAt: new Date("2026-03-03T00:00:00.000Z"), project: { id: "proj_2", name: "Bravo", shortCode: "BRAVO", staffingReqs: [], }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, { id: "alloc_3", demandRequirementId: null, projectId: "proj_2", resourceId: "res_2", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 5, percentage: 62.5, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-03T00:00:00.000Z"), updatedAt: new Date("2026-03-03T00:00:00.000Z"), project: { id: "proj_2", name: "Bravo", shortCode: "BRAVO", staffingReqs: [], }, resource: { id: "res_2", displayName: "Bob", chapter: "CGI" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([]), }, }; const result = await getDashboardDemand(db as never, { startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), groupBy: "chapter", }); expect(result).toEqual([ expect.objectContaining({ id: "CGI", name: "CGI", shortCode: "CGI", allocatedHours: 19, requiredFTEs: 0, resourceCount: 2, derivation: expect.objectContaining({ periodWorkingHoursBase: 176, requiredHours: null, fillPct: null, demandSource: "NONE", calendarLocations: [], }), }), ]); }); it("prefers demand requirements and assignments for project demand semantics", async () => { const db = { demandRequirement: { findMany: vi.fn().mockResolvedValue([ { id: "dem_1", projectId: "proj_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-01T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, headcount: 2, status: "PROPOSED", }, { id: "dem_2", projectId: "proj_1", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 4, percentage: 50, headcount: 1, status: "COMPLETED", }, ]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "asn_1", demandRequirementId: "dem_1", projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-01T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [{ fteCount: 9 }], }, }, { id: "asn_2", demandRequirementId: "dem_2", projectId: "proj_1", resourceId: "res_2", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 4, percentage: 50, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-02T00:00:00.000Z"), updatedAt: new Date("2026-03-02T00:00:00.000Z"), resource: { id: "res_2", displayName: "Bob", chapter: "Lighting" }, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [{ fteCount: 9 }], }, }, { id: "asn_3", demandRequirementId: null, projectId: "proj_1", resourceId: "res_3", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-03-03T00:00:00.000Z"), hoursPerDay: 6, percentage: 75, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-03T00:00:00.000Z"), updatedAt: new Date("2026-03-03T00:00:00.000Z"), resource: { id: "res_3", displayName: "Cara", chapter: "CGI" }, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [{ fteCount: 9 }], }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [{ fteCount: 9 }], }, ]), }, }; const result = await getDashboardDemand(db as never, { startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), groupBy: "project", }); expect(result).toEqual([ expect.objectContaining({ id: "proj_1", name: "Alpha", shortCode: "ALPHA", allocatedHours: 18, requiredFTEs: 3.5, resourceCount: 3, derivation: expect.objectContaining({ periodWorkingHoursBase: 176, requiredHours: 616, fillPct: 3, demandSource: "DEMAND_REQUIREMENTS", calendarLocations: [], }), }), ]); }); it("keeps explicit project metadata when demand and assignment rows exist without legacy allocations", async () => { const db = { demandRequirement: { findMany: vi.fn().mockResolvedValue([ { id: "dem_1", projectId: "proj_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-01T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, headcount: 1, status: "PROPOSED", project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [], }, }, ]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "asn_1", demandRequirementId: "dem_1", projectId: "proj_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-01T00:00:00.000Z"), hoursPerDay: 8, project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [], }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([]), }, }; const result = await getDashboardDemand(db as never, { startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), groupBy: "project", }); expect(result).toEqual([ expect.objectContaining({ id: "proj_1", name: "Alpha", shortCode: "ALPHA", allocatedHours: 8, requiredFTEs: 2, resourceCount: 1, derivation: expect.objectContaining({ periodWorkingHoursBase: 176, requiredHours: 352, fillPct: 2, demandSource: "DEMAND_REQUIREMENTS", }), }), ]); }); it("falls back to staffing requirements when no demand rows exist", async () => { const db = { demandRequirement: { findMany: vi.fn().mockResolvedValue([]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "alloc_1", demandRequirementId: null, projectId: "proj_1", resourceId: "res_1", startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-02T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: null, roleId: null, dailyCostCents: 0, status: "PROPOSED", metadata: {}, createdAt: new Date("2026-03-01T00:00:00.000Z"), updatedAt: new Date("2026-03-01T00:00:00.000Z"), project: { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [{ fteCount: 2 }], }, resource: { id: "res_1", displayName: "Alice", chapter: "CGI" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "proj_1", name: "Alpha", shortCode: "ALPHA", staffingReqs: [{ fteCount: 2 }], }, ]), }, }; const result = await getDashboardDemand(db as never, { startDate: new Date("2026-03-01T00:00:00.000Z"), endDate: new Date("2026-03-31T00:00:00.000Z"), groupBy: "project", }); expect(result).toEqual([ expect.objectContaining({ id: "proj_1", name: "Alpha", shortCode: "ALPHA", allocatedHours: 16, requiredFTEs: 2, resourceCount: 1, derivation: expect.objectContaining({ periodWorkingHoursBase: 176, requiredHours: 352, fillPct: 5, demandSource: "PROJECT_STAFFING_REQS", }), }), ]); }); });