chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { analyzeUtilization, findCapacityWindows } from "../capacity-analyzer.js";
|
||||
import type { CapacityAnalysisInput } from "../capacity-analyzer.js";
|
||||
|
||||
const standardAvailability = {
|
||||
sunday: 0,
|
||||
monday: 8,
|
||||
tuesday: 8,
|
||||
wednesday: 8,
|
||||
thursday: 8,
|
||||
friday: 8,
|
||||
saturday: 0,
|
||||
};
|
||||
|
||||
const resource = {
|
||||
id: "res-1",
|
||||
displayName: "Jane Doe",
|
||||
chargeabilityTarget: 80,
|
||||
availability: standardAvailability,
|
||||
};
|
||||
|
||||
describe("analyzeUtilization", () => {
|
||||
it("computes chargeability for a single chargeable allocation", () => {
|
||||
const input: CapacityAnalysisInput = {
|
||||
resource,
|
||||
allocations: [
|
||||
{
|
||||
startDate: new Date("2026-03-09"), // Monday
|
||||
endDate: new Date("2026-03-13"), // Friday
|
||||
hoursPerDay: 8,
|
||||
status: "CONFIRMED",
|
||||
projectName: "Project A",
|
||||
isChargeable: true,
|
||||
},
|
||||
],
|
||||
analysisStart: new Date("2026-03-09"),
|
||||
analysisEnd: new Date("2026-03-13"),
|
||||
};
|
||||
|
||||
const result = analyzeUtilization(input);
|
||||
expect(result.resourceId).toBe("res-1");
|
||||
expect(result.currentChargeability).toBe(100);
|
||||
expect(result.chargeabilityGap).toBe(-20); // target 80, actual 100
|
||||
expect(result.overallocatedDays).toHaveLength(0);
|
||||
expect(result.underutilizedDays).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("detects overallocated days", () => {
|
||||
const input: CapacityAnalysisInput = {
|
||||
resource,
|
||||
allocations: [
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-09"),
|
||||
hoursPerDay: 5,
|
||||
status: "CONFIRMED",
|
||||
projectName: "A",
|
||||
isChargeable: true,
|
||||
},
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-09"),
|
||||
hoursPerDay: 5,
|
||||
status: "CONFIRMED",
|
||||
projectName: "B",
|
||||
isChargeable: true,
|
||||
},
|
||||
],
|
||||
analysisStart: new Date("2026-03-09"),
|
||||
analysisEnd: new Date("2026-03-09"),
|
||||
};
|
||||
|
||||
const result = analyzeUtilization(input);
|
||||
expect(result.overallocatedDays.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("detects underutilized days (below 50%)", () => {
|
||||
const input: CapacityAnalysisInput = {
|
||||
resource,
|
||||
allocations: [
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-09"),
|
||||
hoursPerDay: 2,
|
||||
status: "CONFIRMED",
|
||||
projectName: "A",
|
||||
isChargeable: true,
|
||||
},
|
||||
],
|
||||
analysisStart: new Date("2026-03-09"),
|
||||
analysisEnd: new Date("2026-03-09"),
|
||||
};
|
||||
|
||||
const result = analyzeUtilization(input);
|
||||
expect(result.underutilizedDays).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("ignores allocations with inactive status", () => {
|
||||
const input: CapacityAnalysisInput = {
|
||||
resource,
|
||||
allocations: [
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-13"),
|
||||
hoursPerDay: 8,
|
||||
status: "CANCELLED",
|
||||
projectName: "Cancelled",
|
||||
isChargeable: true,
|
||||
},
|
||||
],
|
||||
analysisStart: new Date("2026-03-09"),
|
||||
analysisEnd: new Date("2026-03-13"),
|
||||
};
|
||||
|
||||
const result = analyzeUtilization(input);
|
||||
expect(result.currentChargeability).toBe(0);
|
||||
expect(result.allocations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("separates chargeable from non-chargeable hours", () => {
|
||||
const input: CapacityAnalysisInput = {
|
||||
resource,
|
||||
allocations: [
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-09"),
|
||||
hoursPerDay: 4,
|
||||
status: "CONFIRMED",
|
||||
projectName: "Chargeable",
|
||||
isChargeable: true,
|
||||
},
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-09"),
|
||||
hoursPerDay: 4,
|
||||
status: "CONFIRMED",
|
||||
projectName: "Internal",
|
||||
isChargeable: false,
|
||||
},
|
||||
],
|
||||
analysisStart: new Date("2026-03-09"),
|
||||
analysisEnd: new Date("2026-03-09"),
|
||||
};
|
||||
|
||||
const result = analyzeUtilization(input);
|
||||
expect(result.currentChargeability).toBe(50);
|
||||
expect(result.overallocatedDays).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("skips weekends", () => {
|
||||
const input: CapacityAnalysisInput = {
|
||||
resource,
|
||||
allocations: [],
|
||||
analysisStart: new Date("2026-03-07"), // Saturday
|
||||
analysisEnd: new Date("2026-03-08"), // Sunday
|
||||
};
|
||||
|
||||
const result = analyzeUtilization(input);
|
||||
expect(result.currentChargeability).toBe(0);
|
||||
expect(result.overallocatedDays).toHaveLength(0);
|
||||
expect(result.underutilizedDays).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCapacityWindows", () => {
|
||||
const simpleResource = {
|
||||
id: "res-1",
|
||||
displayName: "Jane Doe",
|
||||
availability: standardAvailability,
|
||||
};
|
||||
|
||||
it("finds a full-week window with no allocations", () => {
|
||||
const windows = findCapacityWindows(
|
||||
simpleResource,
|
||||
[],
|
||||
new Date("2026-03-09"), // Monday
|
||||
new Date("2026-03-13"), // Friday
|
||||
);
|
||||
expect(windows).toHaveLength(1);
|
||||
expect(windows[0]!.availableDays).toBe(5);
|
||||
expect(windows[0]!.availableHoursPerDay).toBe(8);
|
||||
expect(windows[0]!.totalAvailableHours).toBe(40);
|
||||
});
|
||||
|
||||
it("reduces available hours when partially allocated", () => {
|
||||
const windows = findCapacityWindows(
|
||||
simpleResource,
|
||||
[
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-13"),
|
||||
hoursPerDay: 4,
|
||||
status: "CONFIRMED",
|
||||
},
|
||||
],
|
||||
new Date("2026-03-09"),
|
||||
new Date("2026-03-13"),
|
||||
);
|
||||
expect(windows).toHaveLength(1);
|
||||
expect(windows[0]!.availableHoursPerDay).toBe(4);
|
||||
expect(windows[0]!.totalAvailableHours).toBe(20);
|
||||
});
|
||||
|
||||
it("returns no windows when fully allocated", () => {
|
||||
const windows = findCapacityWindows(
|
||||
simpleResource,
|
||||
[
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-13"),
|
||||
hoursPerDay: 8,
|
||||
status: "CONFIRMED",
|
||||
},
|
||||
],
|
||||
new Date("2026-03-09"),
|
||||
new Date("2026-03-13"),
|
||||
);
|
||||
expect(windows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("ignores cancelled allocations", () => {
|
||||
const windows = findCapacityWindows(
|
||||
simpleResource,
|
||||
[
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-13"),
|
||||
hoursPerDay: 8,
|
||||
status: "CANCELLED",
|
||||
},
|
||||
],
|
||||
new Date("2026-03-09"),
|
||||
new Date("2026-03-13"),
|
||||
);
|
||||
expect(windows).toHaveLength(1);
|
||||
expect(windows[0]!.availableDays).toBe(5);
|
||||
});
|
||||
|
||||
it("splits windows around gaps", () => {
|
||||
const windows = findCapacityWindows(
|
||||
simpleResource,
|
||||
[
|
||||
{
|
||||
startDate: new Date("2026-03-11"), // Wed only fully booked
|
||||
endDate: new Date("2026-03-11"),
|
||||
hoursPerDay: 8,
|
||||
status: "CONFIRMED",
|
||||
},
|
||||
],
|
||||
new Date("2026-03-09"), // Mon
|
||||
new Date("2026-03-13"), // Fri
|
||||
);
|
||||
// Two windows: Mon-Tue, Thu-Fri
|
||||
expect(windows).toHaveLength(2);
|
||||
expect(windows[0]!.availableDays).toBe(2);
|
||||
expect(windows[1]!.availableDays).toBe(2);
|
||||
});
|
||||
|
||||
it("respects minAvailableHoursPerDay threshold", () => {
|
||||
const windows = findCapacityWindows(
|
||||
simpleResource,
|
||||
[
|
||||
{
|
||||
startDate: new Date("2026-03-09"),
|
||||
endDate: new Date("2026-03-09"),
|
||||
hoursPerDay: 6,
|
||||
status: "CONFIRMED",
|
||||
},
|
||||
],
|
||||
new Date("2026-03-09"),
|
||||
new Date("2026-03-09"),
|
||||
4, // need at least 4 hours free
|
||||
);
|
||||
// Only 2 hours free (8-6), threshold is 4 → no window
|
||||
expect(windows).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user