## 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" | , 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).