feat(planning): ship holiday-aware planning and assistant upgrades

This commit is contained in:
2026-03-28 22:49:28 +01:00
parent 2a005794e7
commit 4f48afe7b4
151 changed files with 17738 additions and 1940 deletions
@@ -86,6 +86,10 @@ describe("resource router", () => {
valueScoreBreakdown: null,
valueScoreUpdatedAt: null,
userId: null,
countryId: "country_de",
metroCityId: null,
country: { code: "DE" },
metroCity: null,
};
const db = {
resource: {
@@ -158,6 +162,165 @@ describe("resource router", () => {
});
});
it("calculates utilization with regional holidays removed from available hours", async () => {
const resource = {
id: "resource_1",
eid: "E-001",
displayName: "Alice",
email: "alice@example.com",
chapter: "CGI",
lcrCents: 5000,
ucrCents: 9000,
currency: "EUR",
chargeabilityTarget: 80,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
skills: [],
dynamicFields: {},
blueprintId: null,
isActive: true,
createdAt: new Date("2026-03-01"),
updatedAt: new Date("2026-03-01"),
roleId: null,
portfolioUrl: null,
postalCode: null,
federalState: "BY",
countryId: "country_de",
metroCityId: null,
valueScore: null,
valueScoreBreakdown: null,
valueScoreUpdatedAt: null,
userId: null,
country: { code: "DE" },
metroCity: null,
};
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([resource]),
},
};
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_confirmed",
projectId: "project_1",
resourceId: "resource_1",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-06T00:00:00.000Z"),
hoursPerDay: 8,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_1",
name: "Project 1",
shortCode: "P1",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
]);
const caller = createControllerCaller(db);
const result = await caller.listWithUtilization({
startDate: "2026-01-05T00:00:00.000Z",
endDate: "2026-01-06T00:00:00.000Z",
});
expect(result[0]).toMatchObject({
bookingCount: 1,
bookedHours: 8,
availableHours: 8,
utilizationPercent: 100,
});
});
it("shifts marketplace availability when a local holiday blocks today", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-06T10:00:00.000Z"));
try {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_by",
displayName: "Bavaria Artist",
eid: "E-BY",
chapter: "CGI",
skills: [{ skill: "Houdini", proficiency: 5 }],
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "BY",
metroCityId: null,
country: { code: "DE" },
metroCity: null,
},
{
id: "resource_hh",
displayName: "Hamburg Artist",
eid: "E-HH",
chapter: "CGI",
skills: [{ skill: "Houdini", proficiency: 5 }],
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "HH",
metroCityId: null,
country: { code: "DE" },
metroCity: null,
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const caller = createControllerCaller(db);
const result = await caller.getSkillMarketplace({
searchSkill: "houdini",
availableOnly: true,
});
const bavaria = result.searchResults.find((resource) => resource.id === "resource_by");
const hamburg = result.searchResults.find((resource) => resource.id === "resource_hh");
expect(bavaria?.availableFrom).toBe("2026-01-07T00:00:00.000Z");
expect(hamburg?.availableFrom).toBe("2026-01-06T00:00:00.000Z");
} finally {
vi.useRealTimers();
}
});
it("uses a composite displayName/id cursor for stable pagination", async () => {
const db = {
resource: {
@@ -314,6 +477,84 @@ describe("resource router", () => {
expect(withProposed[0]?.expectedChargeability).toBe(strict[0]?.expectedChargeability);
});
it("excludes regional public holidays from chargeability stats", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_by",
eid: "E-BY",
displayName: "Bavaria",
chapter: "CGI",
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "BY",
metroCityId: "city_munich",
country: { code: "DE" },
metroCity: { name: "Munich" },
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
},
]),
},
};
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_holiday",
projectId: "project_1",
resourceId: "resource_by",
startDate: new Date("2026-01-06T00:00:00.000Z"),
endDate: new Date("2026-01-06T00:00:00.000Z"),
hoursPerDay: 8,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_1",
name: "Project 1",
shortCode: "P1",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_by", displayName: "Bavaria", chapter: "CGI" },
},
]);
const RealDate = Date;
class MockDate extends Date {
constructor(...args: ConstructorParameters<typeof Date>) {
if (args.length === 0) {
super("2026-01-15T00:00:00.000Z");
return;
}
super(...args);
}
static now() {
return new RealDate("2026-01-15T00:00:00.000Z").getTime();
}
}
vi.stubGlobal("Date", MockDate);
try {
const caller = createControllerCaller(db);
const result = await caller.getChargeabilityStats({});
expect(result[0]).toMatchObject({
actualChargeability: 0,
expectedChargeability: 0,
availableHours: 168,
});
} finally {
vi.unstubAllGlobals();
}
});
it("applies country filters including explicit no-country toggle", async () => {
const db = {
resource: {