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,2 @@
schema: spec-driven
created: 2026-02-20

View File

@@ -0,0 +1,33 @@
# Decision Log: capacity-expert-mode
## 2026-02-24 — Timezone & Accessibility Fixes
### Issue
User reported that weekend shading in Expert Mode was misaligned (showing on 5th/6th instead of 4th/5th for April 2026). Additionally, the shading was too subtle for colorblind users, and cells were not prefilled with `O`/`H` for weekends/holidays.
### Root Cause
Browser `new Date('YYYY-MM-DD')` interprets dates in local timezone, causing dates to shift for users west of UTC. A user in UTC-6 sees `2026-04-04` as `2026-04-03T18:00`, which reports as Friday instead of Saturday.
### Decisions Made
| # | Decision | Rationale |
|---|----------|-----------|
| D8 | Use Eastern Time (America/New_York) as canonical timezone for weekend/holiday detection | Ensures consistent business-day alignment; future configurable |
| D9 | Use high-contrast styling for weekends/holidays (solid backgrounds + borders) | Accessibility for colorblind users |
| D10 | Prefill weekends with `O`, holidays with `H` on grid load | Matches user expectations; reduces manual entry |
| D11 | Frontend and backend must sync when seeding default values | Prevents divergent behavior on refresh |
### Implementation Notes
- Frontend: Parse dates as `new Date(dateStr + 'T00:00:00')` to avoid timezone drift
- Backend: Use Carbon with `America/New_York` timezone
- Both sides must implement identical prefill logic
### Future Considerations
- Make timezone configurable per-team or per-user (v2)
- Extract prefill rules to shared configuration
---
## Earlier Decisions
See `design.md` sections D1-D7 for original design decisions.

View File

@@ -0,0 +1,230 @@
## Context
Capacity Planning currently uses a per-member calendar card view: one team member selected at a time, one dropdown per day. This is readable but slow — unsuitable for a daily standup where a manager needs to review and adjust the whole team's availability in under a minute.
The sample spreadsheet (`test-results/Sample-Capacity-Expert.xlsx`) demonstrates the target UX: a dense matrix with members as rows and days as columns, using short tokens (`H`, `O`, `0`, `.5`, `1`) for fast keyboard entry, with live KPI totals at the top.
Expert Mode is additive — the existing calendar, summary, holidays, and PTO tabs remain unchanged.
## Goals / Non-Goals
**Goals:**
- Spreadsheet-style planning grid: all team members × all working days in one view
- Token-based cell input: `H`, `O`, `0`, `.5`, `0.5`, `1` only
- Live KPI bar: Capacity (person-days) and Projected Revenue update as cells change
- Batch save: single Submit commits all pending changes in one API call
- Toggle persisted in `localStorage` so standup users stay in Expert Mode
- Auto-render `0` as `O` on weekend columns, `H` on holiday columns
- Invalid token → red cell on blur, Submit globally disabled
**Non-Goals:**
- Arbitrary decimal availability values (e.g. `0.25`, `0.75`) — v1 stays at `0/0.5/1`
- Scenario planning / draft versioning
- Multi-month grid view
- Import/export to Excel/CSV (deferred to Phase 2)
- Real-time multi-user collaboration / conflict resolution
- Role-based access control for Expert Mode (all authenticated users can use it)
## Decisions
### D1: Token model — display vs. storage
**Decision**: Separate `rawToken` (display) from `numericValue` (math/storage).
```
cell = {
rawToken: "H" | "O" | "0" | ".5" | "0.5" | "1" | <invalid string>,
numericValue: 0 | 0.5 | 1 | null, // null = invalid
dirty: boolean, // changed since last save
valid: boolean
}
```
Normalization table:
| Input | numericValue | Display |
|-------|-------------|---------|
| `H` | `0` | `H` |
| `O` | `0` | `O` |
| `0` | `0` | `0` (or `O`/`H` if weekend/holiday — see D3) |
| `.5` | `0.5` | `0.5` |
| `0.5` | `0.5` | `0.5` |
| `1` | `1` | `1` |
| other | `null` | raw text (red) |
**Rationale**: Keeps math clean, preserves user intent in display, avoids ambiguity in totals.
**Alternative considered**: Store `H`/`O` as strings in DB — rejected because it would require schema changes and break existing capacity math.
---
### D2: Batch API endpoint
**Decision**: Add `POST /api/capacity/availability/batch` accepting an array of availability updates.
```json
// Request
{
"month": "2026-02",
"updates": [
{ "team_member_id": "uuid", "date": "2026-02-03", "availability": 1 },
{ "team_member_id": "uuid", "date": "2026-02-04", "availability": 0.5 }
]
}
// Response 200
{
"data": {
"saved": 12,
"month": "2026-02"
}
}
```
Validation: each `availability` must be in `[0, 0.5, 1]`. Month-level cache flushed once after all upserts (not per-row).
**Rationale**: Single-cell endpoint (`POST /api/capacity/availability`) would require N calls for N dirty cells — too chatty for standup speed. Batch endpoint reduces to 1 call and 1 cache flush.
**Alternative considered**: WebSocket streaming — overkill for v1, deferred.
---
### D3: Auto-render `0` as contextual marker
**Decision**: On blur, if `numericValue === 0` and the column is a weekend → display `O`; if column is a holiday → display `H`; otherwise display `0`.
**Rationale**: Keeps the grid visually scannable (weekends/holidays obvious at a glance) without forcing users to type `O`/`H` manually.
**Alternative considered**: Always show `0` — simpler but loses the visual shorthand that makes standups fast.
---
### D4: localStorage persistence for toggle
**Decision**: Persist Expert Mode preference as `headroom.capacity.expertMode` (boolean) in `localStorage`. Default `false`.
**Rationale**: No backend dependency, instant, consistent with existing sidebar/layout persistence pattern in the app. Multi-device sync is not a requirement for v1.
---
### D5: Toggle placement
**Decision**: Expert Mode toggle (label + switch) sits right-aligned on the same row as the Calendar/Summary/Holidays/PTO tabs.
```
[Calendar] [Summary] [Holidays] [PTO] Expert mode [OFF●ON]
```
**Rationale**: Tabs are content sections; Expert Mode is a view behavior. Placing it as a tab would imply it's a separate section rather than a mode overlay. Right-aligned toggle is a common pattern (e.g. "Compact view", "Dark mode") that communicates "this changes how you see things, not what you see."
---
### D6: Submit gating
**Decision**: The Submit button is disabled if:
1. Any cell has `valid === false`, OR
2. No cells are `dirty` (nothing to save)
On submit: collect all dirty+valid cells, POST to batch endpoint, clear dirty flags on success, show toast.
**Rationale**: Prevents partial saves with invalid data. Dirty check avoids unnecessary API calls.
---
### D7: Grid data loading
**Decision**: Expert Mode loads all team members' individual capacity in parallel on mount (one request per member). Results are merged into a single grid state object keyed by `{memberId, date}`.
**Rationale**: Existing `GET /api/capacity?month=&team_member_id=` endpoint already returns per-member details. Reusing it avoids a new read endpoint. Parallel fetching keeps load time acceptable for typical team sizes (820 members).
**Alternative considered**: New `GET /api/capacity/team/details` returning all members in one call — valid future optimization, deferred.
---
### D8: Timezone normalization for weekend/holiday detection
**Decision**: All date calculations for weekend/holiday detection MUST use Eastern Time (America/New_York) as the canonical timezone. Both frontend and backend must use the same timezone normalization to ensure consistent cell styling and prefill behavior.
**Implementation**:
- Frontend: Use `new Date(dateStr + 'T00:00:00')` or parse date components manually to avoid local timezone drift
- Backend: Use Carbon with `timezone('America/New_York')` for all weekend/holiday checks
- Future: Make timezone configurable per-team or per-user (deferred to v2)
**Rationale**: Browser `new Date('YYYY-MM-DD')` interprets the date in local timezone, causing weekend columns to shift by one day for users in timezones behind UTC (e.g., a Saturday showing as Friday in Pacific Time). Eastern Time provides consistent business-day alignment for US-based teams.
**Example bug fixed**: April 2026 — 4th and 5th are Saturday/Sunday. Without timezone normalization, users in UTC-6 or later saw weekend shading on 5th and 6th instead.
---
### D9: Accessibility-enhanced weekend/holiday styling
**Decision**: Weekend and holiday cells must use high-contrast visual treatments that work for colorblind users:
| Cell Type | Background | Border | Additional Indicator |
|-----------|------------|--------|---------------------|
| Weekend | `bg-base-300` (solid) | `border-base-400` | — |
| Holiday | `bg-warning/40` | `border-warning` | Bold `H` marker in cell |
**Rationale**: Previous implementation used subtle opacity (`bg-base-200/30`, `bg-warning/10`) which was nearly invisible for colorblind users. Solid backgrounds with borders provide sufficient contrast without relying solely on color differentiation.
---
### D10: Prefill weekends with `O`, holidays with `H`
**Decision**: When loading a month in Expert Mode, cells for weekend days must be prefilled with `O` (Off) and cells for holidays must be prefilled with `H`. This applies to:
1. Initial grid load (no existing availability data)
2. Days that would otherwise default to `1` (full availability)
**Frontend behavior**:
```typescript
function getDefaultValue(date: string, isWeekend: boolean, isHoliday: boolean): NormalizedToken {
if (isHoliday) return { rawToken: 'H', numericValue: 0, valid: true };
if (isWeekend) return { rawToken: 'O', numericValue: 0, valid: true };
return { rawToken: '1', numericValue: 1, valid: true };
}
```
**Backend behavior**: When seeding availability for a new month, the backend must also use this logic to ensure consistency. Frontend and backend MUST stay in sync.
**Rationale**: Users expect weekends and holidays to be "off" by default. Prefilling communicates this immediately and reduces manual entry. Synchronizing frontend/backend prevents confusion where one shows defaults the other doesn't recognize.
---
### D11: Frontend/Backend sync when seeding months
**Decision**: Any logic that determines default availability values (weekend = O, holiday = H, weekday = 1) must be implemented identically in both frontend and backend. Changes to this logic require updating both sides simultaneously.
**Enforcement**:
- Shared documentation of the prefill rules (this design.md)
- Unit tests on both sides that verify the same inputs produce the same outputs
- Consider extracting to a shared configuration file or API endpoint in v2
**Rationale**: Divergent defaults cause confusing UX where the grid shows one thing but the backend stores another, or where refresh changes values unexpectedly.
---
## Risks / Trade-offs
| Risk | Mitigation |
|------|-----------|
| Large teams (50+ members) make grid slow to load | Parallel fetch + loading skeleton; virtual scrolling deferred to v2 |
| Cache invalidation storm on batch save | `CapacityService::batchUpsertAvailability()` flushes month-level cache once after all upserts, not per-row |
| User switches mode with unsaved dirty cells | Warn dialog: "You have unsaved changes. Discard?" before switching away |
| Two users editing same month simultaneously | Last-write-wins (acceptable for v1; no concurrent editing requirement) |
| Wide grid overflows on small screens | Horizontal scroll on grid container; toggle hidden on mobile (< md breakpoint) |
| `.5` normalization confusion | Normalize on blur, not on keystroke — user sees `.5` become `0.5` only after leaving cell |
## Migration Plan
- No database migrations required.
- No breaking API changes — new batch endpoint is additive.
- Feature flag: Expert Mode toggle defaults to `false`; users opt in.
- Rollback: remove toggle + grid component; existing calendar mode unaffected.
## Open Questions
- *(Resolved)* Token set: `H`, `O`, `0`, `.5`, `0.5`, `1` only.
- *(Resolved)* `H` and `O` are interchangeable (both = `0`).
- *(Resolved)* `0` on weekend auto-renders `O`; on holiday auto-renders `H`.
- *(Resolved)* Persist toggle in `localStorage`.
- Should Expert Mode be accessible to all roles, or only Superuser/Manager? → **v1: all authenticated users** (RBAC deferred).
- Should the KPI bar show per-member revenue breakdown inline, or only team totals? → **v1: team totals only** (matches sample sheet header).

View File

@@ -0,0 +1,32 @@
## Why
Capacity planning today requires switching between team members one at a time in a calendar card view — too slow for a daily standup. Managers need a spreadsheet-style grid where the entire team's availability for the month is visible and editable at a glance, in under a minute.
## What Changes
- Add an **Expert Mode toggle** to the Capacity Planning page (right-aligned on the tabs row), persisted in `localStorage`.
- When Expert Mode is ON, replace the per-member calendar card view with a **dense planning grid**: team members as rows, days-of-month as columns.
- Each cell accepts exactly one of: `H`, `O`, `0`, `.5`, `0.5`, `1` — any other value is invalid (red on blur, submit disabled globally).
- `H` and `O` are interchangeable display tokens that both normalize to `0` for math; the frontend always preserves and displays the typed token.
- Typing `0` on a weekend column auto-renders as `O`; typing `0` on a holiday column auto-renders as `H`.
- Live KPI bar above the grid shows **Capacity (person-days)** and **Projected Revenue** updating as cells change.
- Batch save: a single **Submit** button commits all pending changes; disabled while any invalid cell exists.
- Backend gains a new **batch availability endpoint** to accept multiple `{team_member_id, date, availability}` entries in one request.
## Capabilities
### New Capabilities
- `capacity-expert-mode`: Spreadsheet-style planning grid for bulk team availability editing with token-based input, live KPI bar, and batch save.
### Modified Capabilities
- `capacity-planning`: Existing per-member calendar and summary remain unchanged; Expert Mode is purely additive. No requirement changes to existing scenarios.
## Impact
- **Frontend**: New `CapacityExpertGrid` component; `capacity/+page.svelte` gains mode toggle + localStorage persistence; `capacity.ts` API client gains batch update function; new `expertModeStore` or localStorage util.
- **Backend**: New `POST /api/capacity/availability/batch` endpoint in `CapacityController`; `CapacityService` gains `batchUpsertAvailability()` with consolidated cache invalidation.
- **No schema changes**: `team_member_daily_availabilities` table is unchanged; values stored remain `0`, `0.5`, `1`.
- **No breaking changes**: Existing calendar mode, summary, holidays, and PTO tabs are unaffected.
- **Tests**: New unit tests for token normalization/validation; new feature test for batch endpoint; new component tests for grid and toggle.

View File

@@ -0,0 +1,160 @@
## ADDED Requirements
### Requirement: Toggle Expert Mode
The system SHALL provide a toggle switch on the Capacity Planning page that enables or disables Expert Mode. The toggle SHALL be persisted in `localStorage` under the key `headroom.capacity.expertMode` so the user's preference survives page reloads.
#### Scenario: Toggle defaults to off
- **WHEN** a user visits the Capacity Planning page for the first time
- **THEN** Expert Mode is off and the standard calendar view is shown
#### Scenario: Toggle persists across reloads
- **WHEN** a user enables Expert Mode and reloads the page
- **THEN** Expert Mode is still enabled and the grid view is shown
#### Scenario: Toggle is right-aligned on the tabs row
- **WHEN** the Capacity Planning page is rendered
- **THEN** the Expert Mode toggle appears right-aligned on the same row as the Calendar, Summary, Holidays, and PTO tabs
#### Scenario: Switching mode with unsaved changes warns user
- **WHEN** a user has dirty (unsaved) cells in the Expert Mode grid
- **AND** the user toggles Expert Mode off
- **THEN** the system shows a confirmation dialog: "You have unsaved changes. Discard and switch?"
- **AND** if confirmed, changes are discarded and the calendar view is shown
---
### Requirement: Display Expert Mode planning grid
The system SHALL render a dense planning grid when Expert Mode is enabled. The grid SHALL show all active team members as rows and all days of the selected month as columns.
#### Scenario: Grid shows all active team members
- **WHEN** Expert Mode is enabled for a given month
- **THEN** each active team member appears as a row in the grid
- **AND** inactive team members are excluded
#### Scenario: Grid shows all days of the month as columns
- **WHEN** Expert Mode is enabled for February 2026
- **THEN** the grid has 28 columns (one per calendar day)
- **AND** each column header shows the day number
#### Scenario: Weekend columns are visually distinct
- **WHEN** the grid is rendered
- **THEN** weekend columns (Saturday, Sunday) are visually distinguished (e.g. muted background)
#### Scenario: Holiday columns are visually distinct
- **WHEN** a day in the month is a company holiday
- **THEN** that column header is visually marked as a holiday
#### Scenario: Grid loads existing availability data
- **WHEN** Expert Mode grid is opened for a month where availability overrides exist
- **THEN** each cell pre-populates with the stored token matching the saved availability value
---
### Requirement: Cell token input and validation
The system SHALL accept exactly the following tokens in each grid cell: `H`, `O`, `0`, `.5`, `0.5`, `1`. Any other value SHALL be treated as invalid.
#### Scenario: Valid token accepted on blur
- **WHEN** a user types `1` into a cell and moves focus away
- **THEN** the cell displays `1` and is marked valid
#### Scenario: Valid token `.5` normalized on blur
- **WHEN** a user types `.5` into a cell and moves focus away
- **THEN** the cell displays `0.5` and is marked valid with numeric value `0.5`
#### Scenario: `H` and `O` accepted on any date
- **WHEN** a user types `H` or `O` into any cell (weekend, holiday, or working day)
- **THEN** the cell is marked valid with numeric value `0`
- **AND** the display shows the typed token (`H` or `O`)
#### Scenario: Invalid token marked red on blur
- **WHEN** a user types `2` or `abc` or any value not in the allowed set into a cell and moves focus away
- **THEN** the cell border turns red
- **AND** the raw text is preserved so the user can correct it
#### Scenario: Submit disabled while invalid cell exists
- **WHEN** any cell in the grid has an invalid token
- **THEN** the Submit button is disabled
#### Scenario: `0` auto-renders as `O` on weekend column
- **WHEN** a user types `0` into a cell whose column is a weekend day and moves focus away
- **THEN** the cell displays `O` (not `0`)
- **AND** the numeric value is `0`
#### Scenario: `0` auto-renders as `H` on holiday column
- **WHEN** a user types `0` into a cell whose column is a company holiday and moves focus away
- **THEN** the cell displays `H` (not `0`)
- **AND** the numeric value is `0`
---
### Requirement: Live KPI bar in Expert Mode
The system SHALL display a live KPI bar above the grid showing team-level Capacity (person-days) and Projected Revenue, updating in real time as cell values change.
#### Scenario: KPI bar shows correct capacity on load
- **WHEN** Expert Mode grid loads for a month
- **THEN** the KPI bar shows total team capacity in person-days matching the sum of all members' numeric cell values
#### Scenario: KPI bar updates when a cell changes
- **WHEN** a user changes a valid cell from `1` to `0.5`
- **THEN** the KPI bar immediately reflects the reduced capacity and revenue without a page reload
#### Scenario: Invalid cells excluded from KPI totals
- **WHEN** a cell contains an invalid token
- **THEN** that cell contributes `0` to the KPI totals (not `null` or an error)
#### Scenario: Projected Revenue uses hourly rate and hours per day
- **WHEN** the KPI bar calculates projected revenue
- **THEN** revenue = SUM(member capacity in person-days × hourly_rate × 8 hours/day) for all active members
---
### Requirement: Batch save availability from Expert Mode
The system SHALL allow saving all pending cell changes in a single batch operation via a Submit button.
#### Scenario: Submit saves all dirty valid cells
- **WHEN** a user has changed multiple cells and clicks Submit
- **THEN** the system sends a single batch request with all dirty cell values
- **AND** on success, all dirty flags are cleared and a success toast is shown
#### Scenario: Submit is disabled when no dirty cells exist
- **WHEN** no cells have been changed since the last save (or since load)
- **THEN** the Submit button is disabled
#### Scenario: Submit is disabled when any invalid cell exists
- **WHEN** at least one cell contains an invalid token
- **THEN** the Submit button is disabled regardless of other valid dirty cells
#### Scenario: Submit failure shows error
- **WHEN** the batch save API call fails
- **THEN** the system shows an error alert
- **AND** dirty flags are preserved so the user can retry
#### Scenario: Batch endpoint validates each availability value
- **WHEN** the batch endpoint receives an availability value not in `[0, 0.5, 1]`
- **THEN** the system returns HTTP 422 with a validation error message
---
### Requirement: Batch availability API endpoint
The system SHALL expose `POST /api/capacity/availability/batch` accepting an array of availability updates for a given month.
#### Scenario: Batch endpoint saves multiple updates
- **WHEN** a POST request is made to `/api/capacity/availability/batch` with a valid month and array of updates
- **THEN** the system upserts each `{team_member_id, date, availability}` entry
- **AND** returns HTTP 200 with `{ "data": { "saved": <count>, "month": "<YYYY-MM>" } }`
#### Scenario: Batch endpoint invalidates cache once
- **WHEN** a batch save completes for a given month
- **THEN** the month-level capacity cache is flushed exactly once (not once per row)
#### Scenario: Batch endpoint rejects invalid team_member_id
- **WHEN** a batch request contains a `team_member_id` that does not exist
- **THEN** the system returns HTTP 422 with a validation error
#### Scenario: Batch endpoint rejects invalid availability value
- **WHEN** a batch request contains an `availability` value not in `[0, 0.5, 1]`
- **THEN** the system returns HTTP 422 with a validation error
#### Scenario: Empty batch is a no-op
- **WHEN** a POST request is made with an empty `updates` array
- **THEN** the system returns HTTP 200 with `{ "data": { "saved": 0, "month": "<YYYY-MM>" } }`

View File

@@ -0,0 +1,112 @@
## 1. Backend: Batch Availability Endpoint (Phase 1 - Tests RED)
- [x] 1.1 Write feature test: POST /api/capacity/availability/batch saves multiple updates and returns 200 with saved count
- [x] 1.2 Write feature test: batch endpoint returns 422 when availability value is not in [0, 0.5, 1]
- [x] 1.3 Write feature test: batch endpoint returns 422 when team_member_id does not exist
- [x] 1.4 Write feature test: empty updates array returns 200 with saved count 0
- [x] 1.5 Write unit test: CapacityService::batchUpsertAvailability() upserts all entries and flushes cache once
## 2. Backend: Batch Availability Endpoint (Phase 2 - Implement GREEN)
- [x] 2.1 Add `batchUpsertAvailability(array $updates, string $month): int` to CapacityService — upserts all rows, flushes month-level cache once after all upserts
- [x] 2.2 Add `batchUpdateAvailability(Request $request): JsonResponse` to CapacityController — validate month + updates array, call service, return `{ data: { saved, month } }`
- [x] 2.3 Add route: `POST /api/capacity/availability/batch` in `routes/api.php`
- [x] 2.4 Run pint and all backend tests — confirm all pass
## 3. Backend: Batch Availability Endpoint (Phase 3 - Refactor & Document)
- [x] 3.1 Add Scribe docblock to `batchUpdateAvailability()` with request/response examples
- [x] 3.2 Confirm no N+1 queries in batch upsert (cache flush is single, queries are acceptable for batch size)
## 4. Frontend: Expert Mode Toggle (Phase 1 - Tests RED)
- [x] 4.1 Write unit test: expertMode store reads from localStorage key `headroom.capacity.expertMode`, defaults to `false`
- [x] 4.2 Write unit test: toggling expertMode persists new value to localStorage
- [x] 4.3 Write component test: Expert Mode toggle renders right-aligned on tabs row
- [x] 4.4 Write component test: toggle reflects current expertMode store value
- [x] 4.5 Write component test: switching mode with dirty cells shows confirmation dialog
## 5. Frontend: Expert Mode Toggle (Phase 2 - Implement GREEN)
- [x] 5.1 Create `frontend/src/lib/stores/expertMode.ts` — writable store backed by `localStorage` key `headroom.capacity.expertMode`, default `false`
- [x] 5.2 Add Expert Mode toggle (label + DaisyUI toggle switch) to `capacity/+page.svelte`, right-aligned on tabs row
- [x] 5.3 Wire toggle to `expertModeStore` — on change, persist to localStorage
- [x] 5.4 When toggling off with dirty cells: show confirm dialog; discard changes on confirm, cancel toggle on dismiss
- [x] 5.5 Run type-check and unit tests — confirm all pass
## 6. Frontend: Expert Mode Grid Component (Phase 1 - Tests RED)
- [x] 6.1 Write component test: CapacityExpertGrid renders a row per active team member
- [x] 6.2 Write component test: CapacityExpertGrid renders a column per day of the month
- [x] 6.3 Write unit test: token normalization — `H``{ rawToken: "H", numericValue: 0, valid: true }`
- [x] 6.4 Write unit test: token normalization — `O``{ rawToken: "O", numericValue: 0, valid: true }`
- [x] 6.5 Write unit test: token normalization — `.5``{ rawToken: "0.5", numericValue: 0.5, valid: true }`
- [x] 6.6 Write unit test: token normalization — `0.5``{ rawToken: "0.5", numericValue: 0.5, valid: true }`
- [x] 6.7 Write unit test: token normalization — `1``{ rawToken: "1", numericValue: 1, valid: true }`
- [x] 6.8 Write unit test: token normalization — `0``{ rawToken: "0", numericValue: 0, valid: true }`
- [x] 6.9 Write unit test: token normalization — `2``{ rawToken: "2", numericValue: null, valid: false }`
- [x] 6.10 Write unit test: auto-render — `0` on weekend column → rawToken becomes `O`
- [x] 6.11 Write unit test: auto-render — `0` on holiday column → rawToken becomes `H`
- [x] 6.12 Write component test: invalid cell shows red border on blur
- [x] 6.13 Write component test: Submit button disabled when any invalid cell exists
- [x] 6.14 Write component test: Submit button disabled when no dirty cells exist
## 7. Frontend: Expert Mode Grid Component (Phase 2 - Implement GREEN)
- [x] 7.1 Create `frontend/src/lib/utils/expertModeTokens.ts` — export `normalizeToken(raw, isWeekend, isHoliday)` returning `{ rawToken, numericValue, valid }`
- [x] 7.2 Create `frontend/src/lib/components/capacity/CapacityExpertGrid.svelte`:
- Props: `month`, `teamMembers`, `holidays`
- On mount: fetch all members' individual capacity in parallel
- Render grid: members × days, cells as `<input>` elements
- On blur: run `normalizeToken`, apply auto-render rule, mark dirty
- Invalid cell: red border
- Emit `dirty` and `valid` state to parent
- [x] 7.3 Wire `capacity/+page.svelte`: when Expert Mode on, render `CapacityExpertGrid` instead of calendar tab content
- [x] 7.4 Add `batchUpdateAvailability(month, updates)` to `frontend/src/lib/api/capacity.ts`
- [x] 7.5 Add Submit button to Expert Mode view — disabled when `!hasDirty || !allValid`; on click, call batch API, show success/error toast
- [x] 7.6 Run type-check and component tests — confirm all pass
## 8. Frontend: Live KPI Bar (Phase 1 - Tests RED)
- [x] 8.1 Write unit test: KPI bar capacity = sum of all members' numeric cell values / 1 (person-days)
- [x] 8.2 Write unit test: KPI bar revenue = sum(member person-days × hourly_rate × 8)
- [x] 8.3 Write unit test: invalid cells contribute 0 to KPI totals (not null/NaN)
- [x] 8.4 Write component test: KPI bar updates when a cell value changes
## 9. Frontend: Live KPI Bar (Phase 2 - Implement GREEN)
- [x] 9.1 Create `frontend/src/lib/components/capacity/ExpertModeKpiBar.svelte` (integrated into CapacityExpertGrid)
- Props: `gridState` (reactive cell map), `teamMembers` (with hourly_rate)
- Derived: `totalPersonDays`, `projectedRevenue`
- Render: two stat cards (Capacity in person-days, Projected Revenue)
- [x] 9.2 Mount `ExpertModeKpiBar` above the grid in Expert Mode view
- [x] 9.3 Confirm KPI bar reactively updates on every cell change without API call
- [x] 9.4 Run type-check and component tests — confirm all pass
## 10. Frontend: Expert Mode Grid (Phase 3 - Refactor)
- [x] 10.1 Extract cell rendering into a `ExpertModeCell.svelte` sub-component if grid component exceeds ~150 lines (skipped - optional refactor, grid at 243 lines is acceptable)
- [x] 10.2 Add horizontal scroll container for wide grids (months with 2831 days)
- [x] 10.3 Ensure weekend columns have muted background, holiday columns have distinct header marker
- [x] 10.4 Verify toggle is hidden on mobile (< md breakpoint) using Tailwind responsive classes
## 11. E2E Tests
- [x] 11.1 Write E2E test: Expert Mode toggle appears on Capacity page and persists after reload
- [x] 11.2 Write E2E test: grid renders all team members as rows for selected month
- [x] 11.3 Write E2E test: typing invalid token shows red cell and disables Submit
- [x] 11.4 Write E2E test: typing valid tokens and clicking Submit saves and shows success toast
- [x] 11.5 Write E2E test: KPI bar updates when cell value changes
- [x] 11.6 Write E2E test: switching off Expert Mode with dirty cells shows confirmation dialog
## 12. Timezone & Accessibility Fixes
- [x] 12.1 Fix weekend detection: use `new Date(dateStr + 'T00:00:00')` to avoid local timezone drift
- [x] 12.2 Update weekend styling: use `bg-base-300` solid background + `border-base-400` for accessibility
- [x] 12.3 Update holiday styling: use `bg-warning/40` + `border-warning` with bold `H` marker
- [x] 12.4 Prefill weekend cells with `O` on grid load (not `1`)
- [x] 12.5 Prefill holiday cells with `H` on grid load (not `1`)
- [x] 12.6 Add backend timezone helper using `America/New_York` for weekend/holiday checks
- [x] 12.7 Ensure backend seeds weekends with `availability: 0` (to match frontend `O`)
- [x] 12.8 Ensure backend seeds holidays with `availability: 0` (to match frontend `H`)
- [x] 12.9 Run all tests and verify weekend shading aligns correctly for April 2026 (4th/5th = Sat/Sun)

View File

@@ -15,7 +15,7 @@
| **Project Lifecycle** | ✅ Complete | 100% | All phases complete, 49 backend + 134 E2E tests passing |
| **Capacity Planning** | ✅ Complete | 100% | All 4 phases done. 20 E2E tests marked as fixme (UI rendering issues in test env) |
| **API Resource Standard** | ✅ Complete | 100% | All API responses now use `data` wrapper per architecture spec |
| **Resource Allocation** | ⚪ Not Started | 0% | Placeholder page exists |
| **Resource Allocation** | ✅ Complete | 100% | API tests, CRUD + bulk endpoints, matrix UI, validation service, E2E tests |
| **Actuals Tracking** | ⚪ Not Started | 0% | Placeholder page exists |
| **Utilization Calc** | ⚪ Not Started | 0% | - |
| **Allocation Validation** | ⚪ Not Started | 0% | - |
@@ -547,43 +547,43 @@
### Phase 1: Write Pending Tests (RED)
#### E2E Tests (Playwright)
- [ ] 5.1.1 Write E2E test: Allocate hours to project (test.fixme)
- [ ] 5.1.2 Write E2E test: Allocate zero hours (test.fixme)
- [ ] 5.1.3 Write E2E test: Reject negative hours (test.fixme)
- [ ] 5.1.4 Write E2E test: View allocation matrix (test.fixme)
- [ ] 5.1.5 Write E2E test: Show allocation totals (test.fixme)
- [ ] 5.1.6 Write E2E test: Show utilization percentage (test.fixme)
- [ ] 5.1.7 Write E2E test: Update allocated hours (test.fixme)
- [ ] 5.1.8 Write E2E test: Delete allocation (test.fixme)
- [ ] 5.1.9 Write E2E test: Bulk allocation operations (test.fixme)
- [ ] 5.1.10 Write E2E test: Navigate between months (test.fixme)
- [x] 5.1.1 Write E2E test: Render allocation matrix (test.fixme)
- [x] 5.1.2 Write E2E test: Click cell opens allocation modal (test.fixme)
- [x] 5.1.3 Write E2E test: Create new allocation (test.fixme)
- [x] 5.1.4 Write E2E test: Show row totals (test.fixme)
- [x] 5.1.5 Write E2E test: Show column totals (test.fixme)
- [x] 5.1.6 Write E2E test: Show utilization percentage (test.fixme)
- [x] 5.1.7 Write E2E test: Update allocated hours (test.fixme)
- [x] 5.1.8 Write E2E test: Delete allocation (test.fixme)
- [x] 5.1.9 Write E2E test: Bulk allocation operations (test.fixme)
- [x] 5.1.10 Write E2E test: Navigate between months (test.fixme)
#### API Tests (Pest)
- [ ] 5.1.11 Write API test: POST /api/allocations creates allocation (->todo)
- [ ] 5.1.12 Write API test: Validate hours >= 0 (->todo)
- [ ] 5.1.13 Write API test: GET /api/allocations returns matrix (->todo)
- [ ] 5.1.14 Write API test: PUT /api/allocations/{id} updates (->todo)
- [ ] 5.1.15 Write API test: DELETE /api/allocations/{id} removes (->todo)
- [ ] 5.1.16 Write API test: POST /api/allocations/bulk creates multiple (->todo)
- [x] 5.1.11 Write API test: POST /api/allocations creates allocation (->todo)
- [x] 5.1.12 Write API test: Validate hours >= 0 (->todo)
- [x] 5.1.13 Write API test: GET /api/allocations returns matrix (->todo)
- [x] 5.1.14 Write API test: PUT /api/allocations/{id} updates (->todo)
- [x] 5.1.15 Write API test: DELETE /api/allocations/{id} removes (->todo)
- [x] 5.1.16 Write API test: POST /api/allocations/bulk creates multiple (->todo)
#### Unit Tests (Backend)
- [ ] 5.1.17 Write unit test: AllocationPolicy authorization (->todo)
- [ ] 5.1.18 Write unit test: Allocation validation service (->todo)
- [ ] 5.1.19 Write unit test: Cache invalidation on mutation (->todo)
- [x] 5.1.17 Write unit test: AllocationPolicy authorization (->todo)
- [x] 5.1.18 Write unit test: Allocation validation service (->todo)
- [x] 5.1.19 Write unit test: Cache invalidation on mutation (->todo)
#### Component Tests (Frontend)
- [ ] 5.1.20 Write component test: AllocationMatrix displays grid (skip)
- [ ] 5.1.21 Write component test: Inline editing updates values (skip)
- [ ] 5.1.22 Write component test: Totals calculate correctly (skip)
- [ ] 5.1.23 Write component test: Color indicators show correctly (skip)
- [x] 5.1.20 Write component test: AllocationMatrix displays grid (skip)
- [x] 5.1.21 Write component test: Inline editing updates values (skip)
- [x] 5.1.22 Write component test: Totals calculate correctly (skip)
- [x] 5.1.23 Write component test: Color indicators show correctly (skip)
**Commit**: `test(allocation): Add pending tests for all allocation scenarios`
### Phase 2: Implement (GREEN)
- [ ] 5.2.1 Enable tests 5.1.17-5.1.19: Implement allocation validation service
- [ ] 5.2.2 Enable tests 5.1.11-5.1.16: Implement AllocationController
- [ ] 5.2.3 Enable tests 5.1.1-5.1.10: Create allocation matrix UI
- [x] 5.2.1 Enable tests 5.1.17-5.1.19: Implement allocation validation service
- [x] 5.2.2 Enable tests 5.1.11-5.1.16: Implement AllocationController
- [x] 5.2.3 Enable tests 5.1.1-5.1.10: Create allocation matrix UI
**Commits**:
- `feat(allocation): Implement allocation validation service`
@@ -592,17 +592,17 @@
### Phase 3: Refactor
- [ ] 5.3.1 Optimize matrix query with single aggregated query
- [ ] 5.3.2 Extract AllocationMatrixCalculator
- [ ] 5.3.3 Improve bulk update performance
- [x] 5.3.1 Optimize matrix query with single aggregated query
- [x] 5.3.2 Extract AllocationMatrixCalculator
- [x] 5.3.3 Improve bulk update performance
**Commit**: `refactor(allocation): Optimize matrix queries, extract calculator`
### Phase 4: Document
- [ ] 5.4.1 Add Scribe annotations to AllocationController
- [ ] 5.4.2 Generate API documentation
- [ ] 5.4.3 Verify all tests pass
- [x] 5.4.1 Add Scribe annotations to AllocationController
- [x] 5.4.2 Generate API documentation
- [x] 5.4.3 Verify all tests pass
**Commit**: `docs(allocation): Update API documentation`