feat(platform): harden access scoping and delivery baseline
This commit is contained in:
@@ -11,6 +11,7 @@ vi.mock("@capakraken/application", async (importOriginal) => {
|
||||
getDashboardTopValueResources: vi.fn(),
|
||||
getDashboardChargeabilityOverview: vi.fn(),
|
||||
getDashboardBudgetForecast: vi.fn(),
|
||||
getDashboardProjectHealth: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
getDashboardTopValueResources,
|
||||
getDashboardChargeabilityOverview,
|
||||
getDashboardBudgetForecast,
|
||||
getDashboardProjectHealth,
|
||||
} from "@capakraken/application";
|
||||
import { dashboardRouter } from "../router/dashboard.js";
|
||||
import { createCallerFactory } from "../trpc.js";
|
||||
@@ -97,7 +99,7 @@ describe("dashboard router", () => {
|
||||
|
||||
vi.mocked(getDashboardOverview).mockResolvedValue(overview);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getOverview();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
@@ -115,6 +117,72 @@ describe("dashboard router", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatisticsDetail", () => {
|
||||
it("returns assistant-friendly statistics derived from the canonical dashboard overview", async () => {
|
||||
vi.mocked(getDashboardOverview).mockResolvedValue({
|
||||
totalResources: 12,
|
||||
activeResources: 10,
|
||||
inactiveResources: 2,
|
||||
totalProjects: 7,
|
||||
activeProjects: 4,
|
||||
inactiveProjects: 3,
|
||||
totalAllocations: 21,
|
||||
activeAllocations: 18,
|
||||
cancelledAllocations: 3,
|
||||
approvedVacations: 6,
|
||||
totalEstimates: 9,
|
||||
budgetSummary: {
|
||||
totalBudgetCents: 1_234_56,
|
||||
totalCostCents: 654_32,
|
||||
avgUtilizationPercent: 53,
|
||||
},
|
||||
budgetBasis: {
|
||||
remainingBudgetCents: 58_024,
|
||||
budgetedProjects: 5,
|
||||
unbudgetedProjects: 2,
|
||||
trackedAssignmentCount: 18,
|
||||
windowStart: null,
|
||||
windowEnd: null,
|
||||
},
|
||||
projectsByStatus: [
|
||||
{ status: "ACTIVE", count: 4 },
|
||||
{ status: "DRAFT", count: 2 },
|
||||
{ status: "DONE", count: 1 },
|
||||
],
|
||||
chapterUtilization: [
|
||||
{ chapter: "CGI", resourceCount: 5, avgChargeabilityTarget: 78 },
|
||||
{ chapter: "Compositing", resourceCount: 3, avgChargeabilityTarget: 74 },
|
||||
{ chapter: "Unassigned", resourceCount: 2, avgChargeabilityTarget: 0 },
|
||||
],
|
||||
recentActivity: [],
|
||||
});
|
||||
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getStatisticsDetail();
|
||||
|
||||
expect(getDashboardOverview).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
activeResources: 10,
|
||||
totalProjects: 7,
|
||||
activeProjects: 4,
|
||||
totalAllocations: 21,
|
||||
approvedVacations: 6,
|
||||
totalEstimates: 9,
|
||||
totalBudget: "1.234,56 EUR",
|
||||
projectsByStatus: {
|
||||
ACTIVE: 4,
|
||||
DRAFT: 2,
|
||||
DONE: 1,
|
||||
},
|
||||
topChapters: [
|
||||
{ chapter: "CGI", count: 5 },
|
||||
{ chapter: "Compositing", count: 3 },
|
||||
{ chapter: "Unassigned", count: 2 },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getPeakTimes ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("getPeakTimes", () => {
|
||||
@@ -126,7 +194,7 @@ describe("dashboard router", () => {
|
||||
|
||||
vi.mocked(getDashboardPeakTimes).mockResolvedValue(peakData);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getPeakTimes({
|
||||
startDate: "2026-03-01T00:00:00.000Z",
|
||||
endDate: "2026-06-30T00:00:00.000Z",
|
||||
@@ -148,7 +216,7 @@ describe("dashboard router", () => {
|
||||
it("passes week granularity to application layer", async () => {
|
||||
vi.mocked(getDashboardPeakTimes).mockResolvedValue([]);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
await caller.getPeakTimes({
|
||||
startDate: "2026-03-01T00:00:00.000Z",
|
||||
endDate: "2026-03-31T00:00:00.000Z",
|
||||
@@ -177,7 +245,7 @@ describe("dashboard router", () => {
|
||||
|
||||
vi.mocked(getDashboardDemand).mockResolvedValue(demandData);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getDemand({
|
||||
startDate: "2026-01-01T00:00:00.000Z",
|
||||
endDate: "2026-12-31T00:00:00.000Z",
|
||||
@@ -194,7 +262,7 @@ describe("dashboard router", () => {
|
||||
it("supports grouping by chapter", async () => {
|
||||
vi.mocked(getDashboardDemand).mockResolvedValue([]);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
await caller.getDemand({
|
||||
startDate: "2026-06-01T00:00:00.000Z",
|
||||
endDate: "2026-06-30T00:00:00.000Z",
|
||||
@@ -208,6 +276,73 @@ describe("dashboard router", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProjectHealthDetail", () => {
|
||||
it("returns assistant-friendly health detail derived from the canonical dashboard read model", async () => {
|
||||
vi.mocked(getDashboardProjectHealth).mockResolvedValue([
|
||||
{
|
||||
id: "project_critical",
|
||||
projectName: "Critical Project",
|
||||
shortCode: "CRIT",
|
||||
status: "ACTIVE",
|
||||
clientId: "client_1",
|
||||
clientName: "Acme",
|
||||
budgetHealth: 25,
|
||||
staffingHealth: 40,
|
||||
timelineHealth: 30,
|
||||
compositeScore: 35,
|
||||
},
|
||||
{
|
||||
id: "project_healthy",
|
||||
projectName: "Healthy Project",
|
||||
shortCode: "HLTH",
|
||||
status: "ACTIVE",
|
||||
clientId: "client_1",
|
||||
clientName: "Acme",
|
||||
budgetHealth: 90,
|
||||
staffingHealth: 92,
|
||||
timelineHealth: 86,
|
||||
compositeScore: 89,
|
||||
},
|
||||
]);
|
||||
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getProjectHealthDetail();
|
||||
|
||||
expect(getDashboardProjectHealth).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
projects: [
|
||||
{
|
||||
projectId: "project_critical",
|
||||
projectName: "Critical Project",
|
||||
shortCode: "CRIT",
|
||||
status: "ACTIVE",
|
||||
overall: 35,
|
||||
budget: 25,
|
||||
staffing: 40,
|
||||
timeline: 30,
|
||||
rating: "critical",
|
||||
},
|
||||
{
|
||||
projectId: "project_healthy",
|
||||
projectName: "Healthy Project",
|
||||
shortCode: "HLTH",
|
||||
status: "ACTIVE",
|
||||
overall: 89,
|
||||
budget: 90,
|
||||
staffing: 92,
|
||||
timeline: 86,
|
||||
rating: "healthy",
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
healthy: 1,
|
||||
atRisk: 0,
|
||||
critical: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getTopValueResources ─────────────────────────────────────────────────
|
||||
|
||||
describe("getTopValueResources", () => {
|
||||
@@ -219,7 +354,7 @@ describe("dashboard router", () => {
|
||||
|
||||
vi.mocked(getDashboardTopValueResources).mockResolvedValue(resources);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getTopValueResources({ limit: 10 });
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
@@ -232,7 +367,7 @@ describe("dashboard router", () => {
|
||||
it("respects custom limit", async () => {
|
||||
vi.mocked(getDashboardTopValueResources).mockResolvedValue([]);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
await caller.getTopValueResources({ limit: 5 });
|
||||
|
||||
expect(getDashboardTopValueResources).toHaveBeenCalledWith(
|
||||
@@ -334,7 +469,7 @@ describe("dashboard router", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const caller = createProtectedCaller({});
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getBudgetForecast();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
@@ -351,5 +486,177 @@ describe("dashboard router", () => {
|
||||
});
|
||||
expect(getDashboardBudgetForecast).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns assistant-friendly budget forecast detail derived from the canonical dashboard read model", async () => {
|
||||
vi.mocked(getDashboardBudgetForecast).mockResolvedValue([
|
||||
{
|
||||
projectId: "project_1",
|
||||
projectName: "Alpha",
|
||||
shortCode: "ALPHA",
|
||||
clientId: "client_1",
|
||||
clientName: "Client One",
|
||||
budgetCents: 100_000,
|
||||
spentCents: 40_000,
|
||||
remainingCents: 60_000,
|
||||
burnRate: 10_000,
|
||||
estimatedExhaustionDate: "2026-06-30",
|
||||
pctUsed: 40,
|
||||
activeAssignmentCount: 2,
|
||||
calendarLocations: [
|
||||
{
|
||||
countryCode: "DE",
|
||||
countryName: "Germany",
|
||||
federalState: "BY",
|
||||
metroCityName: "Munich",
|
||||
activeAssignmentCount: 2,
|
||||
burnRateCents: 10_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getBudgetForecastDetail();
|
||||
|
||||
expect(getDashboardBudgetForecast).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
forecasts: [
|
||||
expect.objectContaining({
|
||||
projectId: "project_1",
|
||||
projectName: "Alpha",
|
||||
shortCode: "ALPHA",
|
||||
budgetCents: 100_000,
|
||||
spentCents: 40_000,
|
||||
remainingCents: 60_000,
|
||||
projectedCents: 100_000,
|
||||
burnRateCents: 10_000,
|
||||
utilization: "40%",
|
||||
burnStatus: "on_track",
|
||||
calendarLocations: [
|
||||
expect.objectContaining({
|
||||
countryCode: "DE",
|
||||
federalState: "BY",
|
||||
metroCityName: "Munich",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDetail", () => {
|
||||
it("returns the canonical assistant dashboard detail payload", async () => {
|
||||
vi.mocked(getDashboardOverview).mockResolvedValue({
|
||||
budgetBasis: {
|
||||
windowStart: "2026-01-01T00:00:00.000Z",
|
||||
windowEnd: "2026-06-30T00:00:00.000Z",
|
||||
},
|
||||
chapterUtilization: [
|
||||
{
|
||||
chapter: "Delivery",
|
||||
resourceCount: 4,
|
||||
avgChargeabilityTarget: 78,
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(getDashboardPeakTimes).mockResolvedValue([
|
||||
{
|
||||
period: "2026-03",
|
||||
totalHours: 320.4,
|
||||
capacityHours: 400.2,
|
||||
utilizationPct: 80,
|
||||
},
|
||||
]);
|
||||
vi.mocked(getDashboardTopValueResources).mockResolvedValue([
|
||||
{
|
||||
id: "res_1",
|
||||
eid: "pparker",
|
||||
displayName: "Peter Parker",
|
||||
chapter: "Delivery",
|
||||
valueScore: 91,
|
||||
lcrCents: 9_500,
|
||||
},
|
||||
]);
|
||||
vi.mocked(getDashboardDemand).mockResolvedValue([
|
||||
{
|
||||
id: "project_1",
|
||||
name: "Gelddruckmaschine",
|
||||
shortCode: "GDM",
|
||||
allocatedHours: 120,
|
||||
requiredFTEs: 4,
|
||||
resourceCount: 2,
|
||||
derivation: {
|
||||
calendarLocations: [
|
||||
{
|
||||
countryCode: "DE",
|
||||
federalState: "BY",
|
||||
metroCityName: "Augsburg",
|
||||
resourceCount: 2,
|
||||
allocatedHours: 120,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getDetail({ section: "all" });
|
||||
|
||||
expect(result).toEqual({
|
||||
peakTimes: [
|
||||
{
|
||||
month: "2026-03",
|
||||
totalHours: 320.4,
|
||||
totalHoursPerDay: 320.4,
|
||||
capacityHours: 400.2,
|
||||
utilizationPct: 80,
|
||||
},
|
||||
],
|
||||
topResources: [
|
||||
{
|
||||
name: "Peter Parker",
|
||||
eid: "pparker",
|
||||
chapter: "Delivery",
|
||||
lcr: "95,00 EUR",
|
||||
valueScore: 91,
|
||||
},
|
||||
],
|
||||
demandPipeline: [
|
||||
{
|
||||
project: "Gelddruckmaschine (GDM)",
|
||||
needed: 2,
|
||||
requiredFTEs: 4,
|
||||
allocatedResources: 2,
|
||||
allocatedHours: 120,
|
||||
calendarLocations: [
|
||||
{
|
||||
countryCode: "DE",
|
||||
federalState: "BY",
|
||||
metroCityName: "Augsburg",
|
||||
resourceCount: 2,
|
||||
allocatedHours: 120,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
chargeabilityByChapter: [
|
||||
{
|
||||
chapter: "Delivery",
|
||||
headcount: 4,
|
||||
avgTarget: "78%",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(getDashboardPeakTimes).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
startDate: new Date("2026-01-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-06-30T00:00:00.000Z"),
|
||||
granularity: "month",
|
||||
groupBy: "project",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user