Archive three completed changes to archive/: - api-resource-standard (70 tasks, 14 resource classes) - capacity-expert-mode (68 tasks, expert mode planning grid) - enhanced-allocation (62 tasks, planning fidelity + reporting) Sync all delta specs to main specs/: - api-resource-standard: API response standardization - capacity-expert-mode: Expert mode toggle, grid, KPIs, batch API - resource-allocation: Month execution comparison, bulk, untracked - untracked-allocation: Null team member support - allocation-indicators: Variance indicators (red/amber/neutral) - monthly-budget: Explicit project-month planning All changes verified and tested (157 tests passing).
231 lines
11 KiB
Markdown
231 lines
11 KiB
Markdown
## 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 (8–20 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).
|