feat(allocation): implement resource allocation feature

- Add AllocationController with CRUD + bulk endpoints
- Add AllocationValidationService for capacity/estimate validation
- Add AllocationMatrixService for optimized matrix queries
- Add AllocationPolicy for authorization
- Add AllocationResource for API responses
- Add frontend allocationService and matrix UI
- Add E2E tests for allocation matrix (20 tests)
- Add unit tests for validation service and policies
- Fix month format conversion (YYYY-MM to YYYY-MM-01)
This commit is contained in:
2026-02-25 16:28:47 -05:00
parent fedfc21425
commit 3324c4f156
35 changed files with 3337 additions and 67 deletions

View File

@@ -0,0 +1,105 @@
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
function getStoreValue<T>(store: { subscribe: (run: (value: T) => void) => () => void }): T {
let value!: T;
const unsubscribe = store.subscribe((current) => {
value = current;
});
unsubscribe();
return value;
}
describe('4.1 expertMode store', () => {
beforeEach(() => {
vi.resetModules();
(localStorage.getItem as Mock).mockReturnValue(null);
});
it('4.1.1 expertMode defaults to false when not in localStorage', async () => {
const store = await import('../../src/lib/stores/expertMode');
expect(getStoreValue(store.expertMode)).toBe(false);
});
it('4.1.2 expertMode reads "true" from localStorage', async () => {
(localStorage.getItem as Mock).mockReturnValue('true');
const store = await import('../../src/lib/stores/expertMode');
expect(getStoreValue(store.expertMode)).toBe(true);
expect(localStorage.getItem).toHaveBeenCalledWith('headroom.capacity.expertMode');
});
it('4.1.3 expertMode ignores invalid localStorage values', async () => {
(localStorage.getItem as Mock).mockReturnValue('invalid');
const store = await import('../../src/lib/stores/expertMode');
expect(getStoreValue(store.expertMode)).toBe(false);
});
it('4.1.4 toggleExpertMode writes to localStorage', async () => {
const store = await import('../../src/lib/stores/expertMode');
store.toggleExpertMode();
expect(getStoreValue(store.expertMode)).toBe(true);
expect(localStorage.setItem).toHaveBeenCalledWith('headroom.capacity.expertMode', 'true');
});
it('4.1.5 setExpertMode updates value and localStorage', async () => {
const store = await import('../../src/lib/stores/expertMode');
store.setExpertMode(true);
expect(getStoreValue(store.expertMode)).toBe(true);
expect(localStorage.setItem).toHaveBeenCalledWith('headroom.capacity.expertMode', 'true');
store.setExpertMode(false);
expect(getStoreValue(store.expertMode)).toBe(false);
expect(localStorage.setItem).toHaveBeenCalledWith('headroom.capacity.expertMode', 'false');
});
});
describe('4.2 ExpertModeToggle component', () => {
beforeEach(() => {
vi.resetModules();
(localStorage.getItem as Mock).mockReturnValue(null);
});
it.todo('4.2.1 renders with default unchecked state');
it.todo('4.2.2 toggles and updates store on click');
it.todo('4.2.3 appears right-aligned in container');
});
describe('6.1-6.2 CapacityExpertGrid component layout', () => {
it.todo('6.1 renders a row per active team member');
it.todo('6.2 renders a column per day of the month');
});
describe('6.3-6.11 Token normalization', () => {
it.todo('6.3 normalizes H to { rawToken: "H", numericValue: 0, valid: true }');
it.todo('6.4 normalizes O to { rawToken: "O", numericValue: 0, valid: true }');
it.todo('6.5 normalizes .5 to { rawToken: "0.5", numericValue: 0.5, valid: true }');
it.todo('6.6 normalizes 0.5 to { rawToken: "0.5", numericValue: 0.5, valid: true }');
it.todo('6.7 normalizes 1 to { rawToken: "1", numericValue: 1, valid: true }');
it.todo('6.8 normalizes 0 to { rawToken: "0", numericValue: 0, valid: true }');
it.todo('6.9 marks invalid token 2 as { rawToken: "2", numericValue: null, valid: false }');
it.todo('6.10 auto-render: 0 on weekend column becomes O');
it.todo('6.11 auto-render: 0 on holiday column becomes H');
});
describe('6.12-6.14 Grid validation and submit', () => {
it.todo('6.12 invalid cell shows red border on blur');
it.todo('6.13 Submit button disabled when any invalid cell exists');
it.todo('6.14 Submit button disabled when no dirty cells exist');
});
describe('8.1-8.4 KPI bar calculations', () => {
it.todo('8.1 capacity = sum of all members numeric cell values (person-days)');
it.todo('8.2 revenue = sum(member person-days × hourly_rate × 8)');
it.todo('8.3 invalid cells contribute 0 to KPI totals');
it.todo('8.4 KPI bar updates when a cell value changes');
});