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