diff --git a/openspec/changes/api-resource-standard/.openspec.yaml b/openspec/changes/api-resource-standard/.openspec.yaml deleted file mode 100644 index d2997483..00000000 --- a/openspec/changes/api-resource-standard/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-19 diff --git a/openspec/changes/api-resource-standard/design.md b/openspec/changes/api-resource-standard/design.md deleted file mode 100644 index d000221d..00000000 --- a/openspec/changes/api-resource-standard/design.md +++ /dev/null @@ -1,269 +0,0 @@ -## Technical Design: API Resource Standard - -### Overview -This design establishes Laravel API Resources as the standard for all API responses, ensuring consistent `"data"` wrapper across all endpoints. - ---- - -### Directory Structure - -``` -backend/app/Http/Resources/ -├── BaseResource.php # Abstract base with common utilities -├── UserResource.php -├── RoleResource.php -├── TeamMemberResource.php -├── ProjectStatusResource.php -├── ProjectTypeResource.php -├── ProjectResource.php -├── HolidayResource.php -├── PtoResource.php -├── CapacityResource.php -├── TeamCapacityResource.php -└── RevenueResource.php -``` - ---- - -### Base Resource Design - -```php -toIso8601String(); - } - - /** - * Format decimal values consistently - */ - protected function formatDecimal($value, int $decimals = 2): ?float - { - return $value !== null ? round((float) $value, $decimals) : null; - } -} -``` - ---- - -### Resource Implementation Pattern - -Each resource follows this pattern: - -```php - $this->id, - 'name' => $this->name, - 'role' => new RoleResource($this->whenLoaded('role')), - 'hourly_rate' => $this->formatDecimal($this->hourly_rate), - 'active' => (bool) $this->active, - 'created_at' => $this->formatDate($this->created_at), - 'updated_at' => $this->formatDate($this->updated_at), - ]; - } -} -``` - ---- - -### Controller Update Pattern - -**BEFORE:** -```php -public function index(Request $request): JsonResponse -{ - $members = $this->teamMemberService->getAll(); - return response()->json($members); -} -``` - -**AFTER:** -```php -public function index(Request $request): JsonResponse -{ - $members = $this->teamMemberService->getAll(); - return response()->json(new TeamMemberResource($members)); - // Or for collections: - // return response()->json(TeamMemberResource::collection($members)); -} -``` - ---- - -### Relationship Loading Strategy - -Use Laravel's `whenLoaded()` to conditionally include relationships: - -```php -public function toArray($request): array -{ - return [ - 'id' => $this->id, - 'name' => $this->name, - // Only include role if it was eager loaded - 'role' => new RoleResource($this->whenLoaded('role')), - // Only include team_member if it was eager loaded - 'team_member' => new TeamMemberResource($this->whenLoaded('teamMember')), - ]; -} -``` - -**Controller Responsibility:** -```php -// Eager load relationships before passing to resource -$members = TeamMember::with('role')->get(); -return response()->json(TeamMemberResource::collection($members)); -``` - ---- - -### Frontend API Client Update - -**Create helper function to unwrap responses:** - -```typescript -// src/lib/api/client.ts - -export async function unwrapResponse(response: Response): Promise { - const data = await response.json(); - return data.data as T; -} - -// Usage in API functions: -export async function getTeamMembers(): Promise { - const response = await fetch('/api/team-members'); - return unwrapResponse(response); -} -``` - -**Update all API calls:** -```typescript -// BEFORE: -const members = await response.json(); - -// AFTER: -const members = await unwrapResponse(response); -``` - ---- - -### Test Update Pattern - -**Backend Feature Tests:** -```php -// BEFORE: -$response->assertJson(['name' => 'John Doe']); - -// AFTER: -$response->assertJson(['data' => ['name' => 'John Doe']]); -// Or more explicitly: -$this->assertEquals('John Doe', $response->json('data.name')); -``` - -**Resource Unit Tests:** -```php -test('resource wraps single model in data key', function () { - $member = TeamMember::factory()->create(); - $resource = new TeamMemberResource($member); - - $this->assertArrayHasKey('data', $resource->resolve()); - $this->assertEquals($member->id, $resource->resolve()['data']['id']); -}); - -test('collection wraps in data array', function () { - $members = TeamMember::factory(3)->create(); - $collection = TeamMemberResource::collection($members); - - $this->assertArrayHasKey('data', $collection->resolve()); - $this->assertCount(3, $collection->resolve()['data']); -}); -``` - ---- - -### Caching Considerations - -API Resources don't affect caching strategy - they transform data at response time. Cache storage remains the same: - -```php -// Cache raw model data (not transformed resources) -$cached = Cache::remember('team-members:all', 3600, function () { - return TeamMember::with('role')->get(); -}); - -// Transform on response -return response()->json(TeamMemberResource::collection($cached)); -``` - ---- - -### Error Response Handling - -Error responses remain unchanged (no `"data"` wrapper): - -```php -// Validation errors -return response()->json([ - 'message' => 'The given data was invalid.', - 'errors' => $validator->errors(), -], 422); - -// Not found -return response()->json([ - 'message' => 'Resource not found', -], 404); -``` - ---- - -### Scribe Documentation Updates - -Update all `@response` annotations to show new format: - -```php -/** - * @response { - * "data": { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "name": "John Doe" - * } - * } - */ -``` - ---- - -### Migration Steps - -1. **Phase 1: Resources** - Create all resource classes -2. **Phase 2: Controllers** - Update one controller at a time, run tests -3. **Phase 3: Frontend** - Update API client helper, then each endpoint -4. **Phase 4: Tests** - Update all test assertions -5. **Phase 5: Docs** - Regenerate Scribe documentation - ---- - -### Rollback Plan - -Since this is a breaking change for frontend: -- Commit after each controller update -- Run full test suite before next controller -- If critical issue found, revert specific controller commit diff --git a/openspec/changes/api-resource-standard/proposal.md b/openspec/changes/api-resource-standard/proposal.md deleted file mode 100644 index 10022e82..00000000 --- a/openspec/changes/api-resource-standard/proposal.md +++ /dev/null @@ -1,58 +0,0 @@ -## Why - -The Headroom API currently returns raw JSON responses without a standardized wrapper. This violates the architecture specification (docs/headroom-architecture.md, lines 315-393) which mandates Laravel API Resources for consistent JSON structure. This technical debt creates several problems: - -1. **Inconsistent Response Format**: Some endpoints return arrays, others objects, making client-side parsing error-prone -2. **No Future-Proofing**: Adding metadata (pagination, relationships, meta) requires breaking changes later -3. **Missing Data Wrapper**: Architecture requires `"data"` key for all responses to support future transformations -4. **No Resource Abstraction**: Direct model exposure limits ability to transform data without breaking clients - -Since the application is pre-production and not in active use, we should fix this now to avoid permanent technical debt. - -## What Changes - -### Breaking Changes -- **BREAKING**: All API responses will be wrapped in `"data"` key -- **BREAKING**: Collections will use Laravel's resource collection format with `"data"` array -- **BREAKING**: Frontend API client must unwrap `response.data` instead of direct access - -### New Components -- Create `app/Http/Resources/` directory structure -- Create 11 API Resource classes for consistent transformation -- Create base `BaseResource` with common formatting utilities - -### Updated Components -- Update 6 controllers to use API Resources instead of `response()->json()` -- Update 63 backend tests to expect `"data"` wrapper in responses -- Update frontend API client to handle new response format -- Regenerate Scribe API documentation - -## Capabilities - -### New Capabilities -- `api-resource-standard`: Standardize all API responses using Laravel API Resources with consistent `"data"` wrapper - -### Modified Capabilities -- *(none - this is a refactoring change, no functional requirements change)* - -## Impact - -### Backend (Laravel) -- **Files Created**: 11 new resource classes in `app/Http/Resources/` -- **Files Modified**: 6 controllers in `app/Http/Controllers/Api/` -- **Tests Updated**: 63 feature tests -- **New Tests**: 11+ unit tests for resource classes - -### Frontend (SvelteKit) -- **Files Modified**: API client in `src/lib/api/` directory -- **Breaking**: All API calls must access `response.json().data` instead of `response.json()` - -### Documentation -- **Regenerated**: Scribe API docs will show new response format - -### Dependencies -- No new dependencies required (Laravel's built-in API Resources) - -### Performance -- Minimal impact - API Resources add negligible overhead -- Slight improvement: Consistent caching keys can be optimized diff --git a/openspec/changes/api-resource-standard/specs/api-resource-standard/spec.md b/openspec/changes/api-resource-standard/specs/api-resource-standard/spec.md deleted file mode 100644 index de21627d..00000000 --- a/openspec/changes/api-resource-standard/specs/api-resource-standard/spec.md +++ /dev/null @@ -1,275 +0,0 @@ -## ADDED Requirements - -### Requirement: API Response Standardization -All API responses MUST follow Laravel API Resource format with consistent `"data"` wrapper. - -#### Scenario: Single resource response -- **WHEN** an API endpoint returns a single model -- **THEN** the response MUST have a `"data"` key containing the resource -- **AND** the resource MUST include all required fields defined in the resource class - -**Example:** -```json -{ - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": 150.00, - "active": true, - "created_at": "2026-02-01T10:00:00Z" - } -} -``` - -#### Scenario: Collection response -- **WHEN** an API endpoint returns multiple models -- **THEN** the response MUST have a `"data"` key containing an array -- **AND** each item in the array MUST follow the single resource format - -**Example:** -```json -{ - "data": [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role": { "id": 1, "name": "Backend Developer" }, - "hourly_rate": 150.00, - "active": true, - "created_at": "2026-02-01T10:00:00Z" - }, - { - "id": "660e8400-e29b-41d4-a716-446655440001", - "name": "Jane Smith", - "role": { "id": 2, "name": "Frontend Developer" }, - "hourly_rate": 140.00, - "active": true, - "created_at": "2026-02-01T10:00:00Z" - } - ] -} -``` - -#### Scenario: Nested relationships -- **WHEN** a resource has relationships (e.g., TeamMember has Role) -- **THEN** the relationship MUST be nested within the resource -- **AND** the nested resource SHOULD also follow the standard format - -#### Scenario: Error responses -- **WHEN** an error occurs (validation, not found, etc.) -- **THEN** the response format remains unchanged (no `"data"` wrapper for errors) -- **AND** errors continue to use standard Laravel error format: -```json -{ - "message": "The given data was invalid.", - "errors": { - "field": ["Error message"] - } -} -``` - ---- - -## ADDED Resource Classes - -### Resource: UserResource -**Model:** User -**Purpose:** Transform user data for API responses -**Fields:** -- `id`: UUID -- `name`: string -- `email`: string -- `role`: RoleResource (when loaded) - -**Excluded:** password, remember_token - -### Resource: RoleResource -**Model:** Role -**Purpose:** Transform role data -**Fields:** -- `id`: integer -- `name`: string -- `description`: string|null - -### Resource: TeamMemberResource -**Model:** TeamMember -**Purpose:** Transform team member data -**Fields:** -- `id`: UUID -- `name`: string -- `role`: RoleResource (eager loaded) -- `hourly_rate`: number -- `active`: boolean -- `created_at`: ISO 8601 datetime -- `updated_at`: ISO 8601 datetime - -### Resource: ProjectStatusResource -**Model:** ProjectStatus -**Purpose:** Transform project status data -**Fields:** -- `id`: integer -- `name`: string -- `order`: integer -- `is_active`: boolean -- `is_billable`: boolean - -### Resource: ProjectTypeResource -**Model:** ProjectType -**Purpose:** Transform project type data -**Fields:** -- `id`: integer -- `name`: string -- `description`: string|null - -### Resource: ProjectResource -**Model:** Project -**Purpose:** Transform project data -**Fields:** -- `id`: UUID -- `code`: string -- `title`: string -- `status`: ProjectStatusResource (eager loaded) -- `type`: ProjectTypeResource (eager loaded) -- `approved_estimate`: number|null -- `forecasted_effort`: object (month -> hours mapping) -- `start_date`: date|null -- `end_date`: date|null -- `created_at`: ISO 8601 datetime -- `updated_at`: ISO 8601 datetime - -### Resource: HolidayResource -**Model:** Holiday -**Purpose:** Transform holiday data -**Fields:** -- `id`: UUID -- `date`: date (YYYY-MM-DD) -- `name`: string -- `description`: string|null - -### Resource: PtoResource -**Model:** Pto -**Purpose:** Transform PTO request data -**Fields:** -- `id`: UUID -- `team_member`: TeamMemberResource (when loaded) -- `team_member_id`: UUID -- `start_date`: date -- `end_date`: date -- `reason`: string|null -- `status`: string (pending|approved|rejected) -- `created_at`: ISO 8601 datetime - -### Resource: CapacityResource -**Model:** N/A (calculated data) -**Purpose:** Transform individual capacity calculation -**Fields:** -- `team_member_id`: UUID -- `month`: string (YYYY-MM) -- `working_days`: integer -- `person_days`: number -- `hours`: integer -- `details`: array of day-by-day breakdown - -### Resource: TeamCapacityResource -**Model:** N/A (calculated data) -**Purpose:** Transform team capacity aggregation -**Fields:** -- `month`: string (YYYY-MM) -- `total_person_days`: number -- `total_hours`: integer -- `members`: array of member capacities - -### Resource: RevenueResource -**Model:** N/A (calculated data) -**Purpose:** Transform revenue calculation -**Fields:** -- `month`: string (YYYY-MM) -- `possible_revenue`: number -- `member_revenues`: array of individual revenues - ---- - -## ADDED API Endpoints (Updated Format) - -All existing endpoints remain functionally identical, only response format changes: - -### Auth Endpoints -| Endpoint | Method | Response Type | -|----------|--------|---------------| -| POST /api/auth/login | Auth | UserResource with tokens | -| POST /api/auth/refresh | Auth | UserResource with new token | - -### Team Member Endpoints -| Endpoint | Method | Response Type | -|----------|--------|---------------| -| GET /api/team-members | Collection | TeamMemberResource[] | -| POST /api/team-members | Single | TeamMemberResource | -| GET /api/team-members/{id} | Single | TeamMemberResource | -| PUT /api/team-members/{id} | Single | TeamMemberResource | -| DELETE /api/team-members/{id} | Message | { message: "..." } | - -### Project Endpoints -| Endpoint | Method | Response Type | -|----------|--------|---------------| -| GET /api/projects | Collection | ProjectResource[] | -| POST /api/projects | Single | ProjectResource | -| GET /api/projects/{id} | Single | ProjectResource | -| PUT /api/projects/{id} | Single | ProjectResource | -| PUT /api/projects/{id}/status | Single | ProjectResource | -| PUT /api/projects/{id}/estimate | Single | ProjectResource | -| PUT /api/projects/{id}/forecast | Single | ProjectResource | -| DELETE /api/projects/{id} | Message | { message: "..." } | - -### Capacity Endpoints -| Endpoint | Method | Response Type | -|----------|--------|---------------| -| GET /api/capacity | Single | CapacityResource | -| GET /api/capacity/team | Single | TeamCapacityResource | -| GET /api/capacity/revenue | Single | RevenueResource | - -### Holiday Endpoints -| Endpoint | Method | Response Type | -|----------|--------|---------------| -| GET /api/holidays | Collection | HolidayResource[] | -| POST /api/holidays | Single | HolidayResource | -| DELETE /api/holidays/{id} | Message | { message: "..." } | - -### PTO Endpoints -| Endpoint | Method | Response Type | -|----------|--------|---------------| -| GET /api/ptos | Collection | PtoResource[] | -| POST /api/ptos | Single | PtoResource | -| PUT /api/ptos/{id}/approve | Single | PtoResource | - ---- - -## ADDED Test Requirements - -### Resource Unit Tests -Each resource MUST have unit tests verifying: -1. Single resource returns `"data"` wrapper -2. Collection returns `"data"` array wrapper -3. All expected fields are present -4. Sensitive fields are excluded (e.g., password) -5. Relationships are properly nested -6. Date formatting follows ISO 8601 - -### Feature Test Updates -All 63 existing feature tests MUST be updated to: -1. Assert response has `"data"` key -2. Access nested data via `response.json()['data']` -3. Verify collection responses have `"data"` array - -### E2E Test Verification -All 134 E2E tests MUST pass after frontend API client is updated. - ---- - -## REMOVED - -- Direct `response()->json($model)` calls in controllers -- Raw array/object responses without `"data"` wrapper diff --git a/openspec/changes/api-resource-standard/tasks.md b/openspec/changes/api-resource-standard/tasks.md deleted file mode 100644 index af86894c..00000000 --- a/openspec/changes/api-resource-standard/tasks.md +++ /dev/null @@ -1,203 +0,0 @@ -# Tasks - API Resource Standard - -> **Change**: api-resource-standard -> **Schema**: spec-driven -> **Status**: ✅ COMPLETE - ---- - -## Summary - -| Phase | Status | Progress | Notes | -|-------|--------|----------|-------| -| **1. Foundation** | ✅ Complete | 2/2 | BaseResource created | -| **2. Core Resources** | ✅ Complete | 6/6 | All core resources created | -| **3. Capacity Resources** | ✅ Complete | 5/5 | All capacity resources created | -| **4. Controller Updates** | ✅ Complete | 6/6 | All controllers using resources | -| **5. Frontend API Client** | ✅ Complete | 1/1 | unwrapResponse() in place | -| **6. Test Updates** | ✅ Complete | 63/63 | All tests updated for data wrapper | -| **7. Documentation** | ✅ Complete | 1/1 | Scribe docs regenerated | -| **8. Remediation** | ✅ Complete | - | Fixed test failures, added hooks.server.ts proxy | - -### Final Test Results (2026-02-19) - -| Suite | Tests | Status | -|-------|-------|--------| -| **Backend (Pest)** | 75 passed | ✅ | -| **Frontend Unit (Vitest)** | 10 passed | ✅ | -| **E2E (Playwright)** | 130 passed, 24 skipped | ✅ | -| **API Documentation** | Generated | ✅ | - -**Note:** 4 project modal timing tests marked as `test.fixme()` for later investigation. These are UI timing issues, not functional bugs. - ---- - -## Phase 1: Foundation Resources - -- [x] **1.1** Create `app/Http/Resources/` directory -- [x] **1.2** Create `BaseResource.php` with common utilities: - - `formatDate()` - ISO 8601 date formatting - - `formatDecimal()` - Consistent decimal formatting - - `whenLoaded()` wrapper for relationships - ---- - -## Phase 2: Core Resources - -- [x] **2.1** Create `UserResource.php` - Hide password, include role -- [x] **2.2** Create `RoleResource.php` - Basic fields only -- [x] **2.3** Create `TeamMemberResource.php` - Include RoleResource -- [x] **2.4** Create `ProjectStatusResource.php` - Basic fields -- [x] **2.5** Create `ProjectTypeResource.php` - Basic fields -- [x] **2.6** Create `ProjectResource.php` - Include status and type - ---- - -## Phase 3: Capacity Resources - -- [x] **3.1** Create `HolidayResource.php` - Basic fields -- [x] **3.2** Create `PtoResource.php` - Include team_member when loaded -- [x] **3.3** Create `CapacityResource.php` - Calculated data wrapper -- [x] **3.4** Create `TeamCapacityResource.php` - Team aggregation -- [x] **3.5** Create `RevenueResource.php` - Revenue calculation - ---- - -## Phase 4: Controller Updates - -### 4.1 AuthController -- [x] **4.1.1** Update `login()` to return `UserResource` -- [x] **4.1.2** Update `refresh()` to return `UserResource` - -### 4.2 TeamMemberController -- [x] **4.2.1** Update `index()` to use `TeamMemberResource::collection()` -- [x] **4.2.2** Update `store()` to return `TeamMemberResource` -- [x] **4.2.3** Update `show()` to return `TeamMemberResource` -- [x] **4.2.4** Update `update()` to return `TeamMemberResource` -- [x] **4.2.5** Update `destroy()` response format (keep message) - -### 4.3 ProjectController -- [x] **4.3.1** Update `index()` to use `ProjectResource::collection()` -- [x] **4.3.2** Update `store()` to return `ProjectResource` -- [x] **4.3.3** Update `show()` to return `ProjectResource` -- [x] **4.3.4** Update `update()` to return `ProjectResource` -- [x] **4.3.5** Update `updateStatus()` to return `ProjectResource` -- [x] **4.3.6** Update `updateEstimate()` to return `ProjectResource` -- [x] **4.3.7** Update `updateForecast()` to return `ProjectResource` -- [x] **4.3.8** Update `destroy()` response format (keep message) - -### 4.4 CapacityController -- [x] **4.4.1** Update `individual()` to return `CapacityResource` -- [x] **4.4.2** Update `team()` to return `TeamCapacityResource` -- [x] **4.4.3** Update `revenue()` to return `RevenueResource` - -### 4.5 HolidayController -- [x] **4.5.1** Update `index()` to use `HolidayResource::collection()` -- [x] **4.5.2** Update `store()` to return `HolidayResource` -- [x] **4.5.3** Update `destroy()` response format (keep message) - -### 4.6 PtoController -- [x] **4.6.1** Update `index()` to use `PtoResource::collection()` -- [x] **4.6.2** Update `store()` to return `PtoResource` -- [x] **4.6.3** Update `approve()` to return `PtoResource` - ---- - -## Phase 5: Frontend API Client - -- [x] **5.1** Create `src/lib/api/client.ts` with `unwrapResponse()` helper: - ```typescript - export async function unwrapResponse(response: Response): Promise { - const data = await response.json(); - return data.data as T; - } - ``` -- [x] **5.2** Update `team-members.ts` to use `unwrapResponse()` -- [x] **5.3** Update `projects.ts` to use `unwrapResponse()` -- [x] **5.4** Update `capacity.ts` to use `unwrapResponse()` -- [x] **5.5** Update `auth.ts` to use `unwrapResponse()` -- [x] **5.6** Update any other API client files - ---- - -## Phase 6: Test Updates - -### 6.1 Resource Unit Tests (New) -- [x] **6.1.1** Create `tests/Unit/Resources/UserResourceTest.php` -- [x] **6.1.2** Create `tests/Unit/Resources/RoleResourceTest.php` -- [x] **6.1.3** Create `tests/Unit/Resources/TeamMemberResourceTest.php` -- [x] **6.1.4** Create `tests/Unit/Resources/ProjectResourceTest.php` -- [x] **6.1.5** Create `tests/Unit/Resources/HolidayResourceTest.php` -- [x] **6.1.6** Create `tests/Unit/Resources/PtoResourceTest.php` - -Each test should verify: -- Single resource wraps in `"data"` key -- Collection wraps in `"data"` array -- All expected fields present -- Sensitive fields excluded -- Relationships properly nested -- Date formatting correct - -### 6.2 Feature Test Updates -- [x] **6.2.1** Update `tests/Feature/Auth/AuthTest.php` (15 tests) -- [x] **6.2.2** Update `tests/Feature/TeamMember/TeamMemberTest.php` (8 tests) -- [x] **6.2.3** Update `tests/Feature/Project/ProjectTest.php` (9 tests) -- [x] **6.2.4** Update `tests/Feature/Capacity/CapacityTest.php` (8 tests) - -Update pattern: -```php -// BEFORE -->assertJson(['name' => 'John Doe']); - -// AFTER -->assertJson(['data' => ['name' => 'John Doe']]); -// Or: -->assertEquals('John Doe', $response->json('data.name')); -``` - ---- - -## Phase 7: Documentation - -- [x] **7.1** Update all `@response` annotations in controllers to show new format -- [x] **7.2** Run `php artisan scribe:generate` to regenerate docs -- [x] **7.3** Verify all endpoints show correct `"data"` wrapper in documentation - ---- - -## Phase 8: Verification - -### Test Matrix -| Test Suite | Expected | Status | -|------------|----------|--------| -| Backend Unit | 11+ new tests pass | ⏳ | -| Backend Feature | 63 tests pass | ⏳ | -| Frontend Unit | 32 tests pass | ⏳ | -| E2E | 134 tests pass | ⏳ | - -### API Verification Checklist -- [x] **8.1** GET /api/team-members returns `{ data: [...] }` -- [x] **8.2** GET /api/team-members/{id} returns `{ data: {...} }` -- [x] **8.3** POST /api/team-members returns `{ data: {...} }` -- [x] **8.4** PUT /api/team-members/{id} returns `{ data: {...} }` -- [x] **8.5** GET /api/projects returns `{ data: [...] }` -- [x] **8.6** GET /api/projects/{id} returns `{ data: {...} }` -- [x] **8.7** GET /api/capacity returns `{ data: {...} }` -- [x] **8.8** GET /api/capacity/team returns `{ data: {...} }` -- [x] **8.9** GET /api/capacity/revenue returns `{ data: {...} }` -- [x] **8.10** GET /api/holidays returns `{ data: [...] }` -- [x] **8.11** GET /api/ptos returns `{ data: [...] }` -- [x] **8.12** POST /api/auth/login returns `{ data: {...}, token: "..." }` (check spec) - ---- - -## Post-Implementation - -- [x] Update `docs/headroom-architecture.md` line 784-788 to mark Resources as complete -- [x] Archive this change with `openspec archive api-resource-standard` - ---- - -**Total Tasks**: 11 (resources) + 28 (controller endpoints) + 6 (frontend files) + 69 (tests) + 3 (docs) = **117 tasks** - -**Estimated Time**: 3-4 hours diff --git a/openspec/changes/capacity-expert-mode/.openspec.yaml b/openspec/changes/capacity-expert-mode/.openspec.yaml deleted file mode 100644 index d0ec88b2..00000000 --- a/openspec/changes/capacity-expert-mode/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-20 diff --git a/openspec/changes/capacity-expert-mode/decision-log.md b/openspec/changes/capacity-expert-mode/decision-log.md deleted file mode 100644 index b297e1fe..00000000 --- a/openspec/changes/capacity-expert-mode/decision-log.md +++ /dev/null @@ -1,33 +0,0 @@ -# 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. diff --git a/openspec/changes/capacity-expert-mode/design.md b/openspec/changes/capacity-expert-mode/design.md deleted file mode 100644 index 4ee71630..00000000 --- a/openspec/changes/capacity-expert-mode/design.md +++ /dev/null @@ -1,230 +0,0 @@ -## 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). diff --git a/openspec/changes/capacity-expert-mode/proposal.md b/openspec/changes/capacity-expert-mode/proposal.md deleted file mode 100644 index fa2ec753..00000000 --- a/openspec/changes/capacity-expert-mode/proposal.md +++ /dev/null @@ -1,32 +0,0 @@ -## 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. diff --git a/openspec/changes/capacity-expert-mode/specs/capacity-expert-mode/spec.md b/openspec/changes/capacity-expert-mode/specs/capacity-expert-mode/spec.md deleted file mode 100644 index 99b222bf..00000000 --- a/openspec/changes/capacity-expert-mode/specs/capacity-expert-mode/spec.md +++ /dev/null @@ -1,160 +0,0 @@ -## 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": , "month": "" } }` - -#### 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": "" } }` diff --git a/openspec/changes/capacity-expert-mode/tasks.md b/openspec/changes/capacity-expert-mode/tasks.md deleted file mode 100644 index b380e950..00000000 --- a/openspec/changes/capacity-expert-mode/tasks.md +++ /dev/null @@ -1,112 +0,0 @@ -## 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 `` 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 28–31 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) diff --git a/openspec/changes/enhanced-allocation/.openspec.yaml b/openspec/changes/enhanced-allocation/.openspec.yaml deleted file mode 100644 index e331c975..00000000 --- a/openspec/changes/enhanced-allocation/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-25 diff --git a/openspec/changes/enhanced-allocation/design.md b/openspec/changes/enhanced-allocation/design.md deleted file mode 100644 index f7700148..00000000 --- a/openspec/changes/enhanced-allocation/design.md +++ /dev/null @@ -1,268 +0,0 @@ -# Design: Enhanced Allocation Fidelity - -## Context - -The allocation experience must reflect a strict planning-to-execution model. Current implementation drift introduced incorrect month semantics (`approved_estimate / 12`), mixed concern UIs, and ambiguous status signaling. - -This design aligns implementation with the validated operating model: - -1. **Projects (Lifecycle)**: `approved_estimate` is the lifecycle total. -2. **Project-Month Plan (Intent)**: manager explicitly sets monthly planned effort per project. -3. **Project-Resource Allocation (Execution)**: for selected month, manager allocates effort by team member. -4. **Reporting (Outcome)**: historical/current/future insights built from these three layers. - -## Goals - -1. Implement explicit project-month planning as first-class data. -2. Reframe allocation matrix as execution grid with month-plan and capacity variance. -3. Support untracked effort (`team_member_id = null`) end-to-end. -4. Implement partial bulk success for allocations. -5. Keep visual signaling minimal and decision-focused. - -## Non-Goals - -- Real-time collaboration/websockets. -- Notification systems. -- Export workflows. -- Cosmetic UI experimentation beyond fidelity requirements. - -## Decisions - -### D1: Month Plan Is Explicit (No Derived Monthly Budget) - -- **Decision**: monthly plan is manager-entered and persisted. -- **Rejected**: deriving monthly budget from `approved_estimate / 12`. -- **Reason**: project phasing is dependency-driven, not uniform. - -### D2: Reconciliation vs Lifecycle Total - -For each project: -- `plan_sum = sum(non-null planned_hours across months)` -- compare against `approved_estimate` - - `plan_sum > approved_estimate` -> `OVER` - - `plan_sum < approved_estimate` -> `UNDER` - - `plan_sum == approved_estimate` -> `MATCH` - -Tolerance for MATCH should use decimal-safe comparison in implementation. - -### D3: Blank Month Semantics - -- Planning grid cell can be blank (`null`) and remains visually blank. -- Allocation variance treats missing month plan as `0`. -- Allocation is allowed when month plan is blank. - -### D4: Grid-First Editing - -- Project-month planning: inline grid editing primary. -- Allocation matrix: inline grid editing primary. -- Modal is optional/fallback, not primary path. - -### D5: Untracked Semantics - -- Untracked allocation is represented by `team_member_id = null`. -- Included in project totals and grand totals. -- Excluded from team member capacity/utilization calculations. - -### D6: Visual Status Policy - -- Over = red -- Under = amber -- Match/settled = neutral - -Status emphasis belongs on row/column summary edges and variance cells; avoid noisy color in every interior cell. - -### D7: Forecasted Effort Deprecation - -- `projects.forecasted_effort` is deprecated for this planning workflow. -- New monthly planning source of truth is explicit project-month plan data. - -### D8: Allocation Editing Mode (DECISION: Modal-Primary) - -**Explored**: Grid-first inline editing vs modal-primary workflow. -**Decision**: Modal-primary editing is acceptable for this release. -**Rationale**: -- Modal provides clear focus and validation context -- Current implementation already works well -- Grid-first is a nice-to-have, not blocking for planning fidelity -**Consequence**: Tasks 2.12-2.13 (grid-first editing) intentionally skipped. - -### D9: Reporting API Design (Single Endpoint) - -**Explored**: Separate endpoints per view vs unified endpoint. -**Decision**: Single endpoint `GET /api/reports/allocations` with date-range parameters. -**View Type Inference**: Backend determines "did/is/will" from date range: -- `did` = past months (all dates < current month) -- `is` = current month (dates include current month) -- `will` = future months (all dates > current month) -**Response Shape**: Unified structure regardless of view: -```json -{ - "period": { "start": "2026-01-01", "end": "2026-03-31" }, - "view_type": "is", - "projects": [...], - "members": [...], - "aggregates": { "total_planned", "total_allocated", "total_variance", "status" } -} -``` - -## Data Model - -### New Table: `project_month_plans` - -Recommended schema: -- `id` (uuid) -- `project_id` (uuid FK -> projects.id) -- `month` (date, normalized to first day of month) -- `planned_hours` (decimal, nullable) -- `created_at`, `updated_at` -- unique index on (`project_id`, `month`) - -Notes: -- `planned_hours = null` means blank/unset plan. -- If storage policy prefers non-null planned hours, clear operation must still preserve blank UI semantics via delete row strategy. - -### Existing Tables - -- `projects.approved_estimate`: lifecycle cap (unchanged semantics) -- `allocations.team_member_id`: nullable to support untracked - -## API Design - -### Project-Month Plan APIs - -1. `GET /api/project-month-plans?year=YYYY` - - returns month-plan grid payload by project/month - - includes reconciliation status per project - -2. `PUT /api/project-month-plans/bulk` - - accepts multi-cell upsert payload - - supports setting value and clearing value (blank) - - returns updated rows + reconciliation results - -Example payload: -```json -{ - "year": 2026, - "items": [ - { "project_id": "...", "month": "2026-01", "planned_hours": 1200 }, - { "project_id": "...", "month": "2026-02", "planned_hours": 1400 }, - { "project_id": "...", "month": "2026-03", "planned_hours": 400 }, - { "project_id": "...", "month": "2026-04", "planned_hours": null } - ] -} -``` - -### Allocation APIs - -- Keep `GET /api/allocations?month=YYYY-MM` and CRUD endpoints. -- Enrich response with month-context variance metadata needed for row/column summary rendering. -- `POST /api/allocations/bulk` must support partial success. - -Partial bulk response contract: -```json -{ - "data": [{ "index": 0, "id": "...", "status": "created" }], - "failed": [{ "index": 1, "errors": { "allocated_hours": ["..."] } }], - "summary": { "created": 1, "failed": 1 } -} -``` - -## Computation Rules - -### Lifecycle Reconciliation (Project-Month Plan Surface) - -For each project: -- `plan_sum = SUM(planned_hours where month in planning horizon and value != null)` -- `status = OVER | UNDER | MATCH` compared to `approved_estimate` - -### Allocation Row Variance (Execution Surface) - -For selected month and project: -- `allocated_total = SUM(allocation.allocated_hours for project/month including untracked)` -- `planned_month = planned_hours(project, month) or 0 if missing` -- `row_variance = allocated_total - planned_month` -- status from row_variance sign - -### Allocation Column Variance (Execution Surface) - -For selected month and team member: -- `member_allocated = SUM(allocation.allocated_hours for member/month)` -- `member_capacity = computed month capacity` -- `col_variance = member_allocated - member_capacity` -- exclude `team_member_id = null` rows from this computation - -## UX Specification (Implementation-Oriented) - -### Surface A: Project-Month Plan Grid - -- Rows: projects -- Columns: months -- Right edge: row total + reconciliation status -- Bottom edge: optional month totals for planning visibility -- Cell editing: inline, keyboard-first -- Blank cells remain blank visually -- Color: only summary statuses (red/amber/neutral) - -### Surface B: Project-Resource Allocation Grid (Selected Month) - -- Rows: projects -- Columns: team members + untracked -- Right edge: project row total + row variance status vs planned month -- Bottom edge: member totals + capacity variance status -- Primary editing: inline cell edit (modal not primary) -- Color: red/amber highlights only for over/under summary states - -### Accessibility - -- Do not rely on color alone; include text labels (OVER/UNDER). -- Maintain focus and keyboard navigation in both grids. -- Preserve contrast and readable status labels. - -## Risk Register - -1. **Semantic regression risk**: reintroduction of derived monthly budget logic. - - Mitigation: explicit tests and prohibition in specs. - -2. **Blank vs zero confusion**. - - Mitigation: explicit API contract and UI behavior tests. - -3. **Untracked leakage into capacity metrics**. - - Mitigation: query filters and dedicated tests. - -4. **Bulk partial side effects**. - - Mitigation: per-item validation and clear response contract. - -## Testing Strategy - -### Unit -- Lifecycle reconciliation calculator -- Row/column variance calculators -- Blank-plan-as-zero execution logic - -### Feature/API -- Project-month plan bulk upsert and clear behavior -- Reconciliation status correctness -- Allocation month variance data correctness -- Untracked include/exclude behavior -- Partial bulk success semantics - -### Frontend Grid Tests -- Inline edit commit/cancel/clear -- Keyboard navigation -- Summary status placement and labels -- Blank visual state preservation - -### E2E -- Create lifecycle estimate project -- Enter monthly plan across months and verify reconciliation -- Execute month allocations and verify row/column variances -- Validate untracked behavior -- Validate partial bulk success handling - -## Migration & Rollout Notes - -1. Introduce project-month plan model and APIs. -2. Remove derived budget rendering in allocation UI. -3. Wire allocation UI to explicit month plan for variance. -4. Deprecate `forecasted_effort` usage in this workflow path. -5. Keep backward compatibility for existing allocation records. diff --git a/openspec/changes/enhanced-allocation/docs/reporting-api.md b/openspec/changes/enhanced-allocation/docs/reporting-api.md deleted file mode 100644 index 971810db..00000000 --- a/openspec/changes/enhanced-allocation/docs/reporting-api.md +++ /dev/null @@ -1,130 +0,0 @@ -# Reporting API Contract - -## Overview - -The Reporting API provides aggregated resource planning and execution data for management analysis. It supports three view types (did/is/will) inferred from date ranges. - -## Endpoint - -``` -GET /api/reports/allocations -``` - -## Authentication - -Requires Bearer token authentication. - -## Query Parameters - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `start_date` | string (YYYY-MM-DD) | Yes | Start of reporting period | -| `end_date` | string (YYYY-MM-DD) | Yes | End of reporting period | -| `project_ids[]` | array of UUIDs | No | Filter by specific projects | -| `member_ids[]` | array of UUIDs | No | Filter by specific team members | - -## View Types - -The `view_type` is inferred from the date range: - -| View Type | Date Range | Description | -|-----------|------------|-------------| -| `did` | All dates in past months | Historical execution analysis | -| `is` | Includes current month | Active planning vs execution | -| `will` | All dates in future months | Forecast and capacity planning | - -## Response Structure - -```json -{ - "period": { - "start": "2026-01-01", - "end": "2026-03-31" - }, - "view_type": "will", - "projects": [ - { - "id": "uuid", - "code": "PROJ-001", - "title": "Project Name", - "approved_estimate": 3000.0, - "lifecycle_status": "MATCH", - "plan_sum": 2600.0, - "period_planned": 2600.0, - "period_allocated": 2800.0, - "period_variance": 200.0, - "period_status": "OVER", - "months": [ - { - "month": "2026-01", - "planned_hours": 1200.0, - "is_blank": false, - "allocated_hours": 1300.0, - "variance": 100.0, - "status": "OVER" - } - ] - } - ], - "members": [ - { - "id": "uuid", - "name": "Team Member", - "period_allocated": 400.0, - "projects": [ - { - "project_id": "uuid", - "project_code": "PROJ-001", - "project_title": "Project Name", - "total_hours": 400.0 - } - ] - } - ], - "aggregates": { - "total_planned": 2600.0, - "total_allocated": 2800.0, - "total_variance": 200.0, - "status": "OVER" - } -} -``` - -## Status Values - -| Status | Meaning | Visual | -|--------|---------|--------| -| `OVER` | Allocated > Planned/Capacity | 🔴 Red | -| `UNDER` | Allocated < Planned/Capacity | 🟡 Amber | -| `MATCH` | Allocated = Planned/Capacity | 🟢 Green/Neutral | - -## Blank vs Zero Distinction - -- **Blank plan** (`is_blank: true`): No plan entry exists for the month - - `planned_hours`: `null` - - Used for variance: treated as `0` - -- **Explicit zero** (`is_blank: false`, `planned_hours: 0`): Plan was explicitly set to zero - - `planned_hours`: `0` - - Distinct from blank for reporting accuracy - -## Dependencies - -This endpoint combines data from: - -- `ProjectMonthPlan` - Monthly planning data -- `Allocation` - Resource allocations -- `Project` - Project metadata (approved_estimate) -- `TeamMember` - Team member capacity - -Calculations use: -- `ReconciliationCalculator` - Lifecycle plan vs estimate -- `VarianceCalculator` - Period and monthly variances - -## Error Responses - -| Status | Condition | -|--------|-----------| -| 422 | Missing or invalid date parameters | -| 422 | `end_date` before `start_date` | -| 401 | Unauthorized (missing/invalid token) | diff --git a/openspec/changes/enhanced-allocation/proposal.md b/openspec/changes/enhanced-allocation/proposal.md deleted file mode 100644 index 84bf864c..00000000 --- a/openspec/changes/enhanced-allocation/proposal.md +++ /dev/null @@ -1,89 +0,0 @@ -# Proposal: Enhanced Allocation Fidelity - -## Why - -The current Resource Allocation implementation diverges from intended planning behavior and creates misleading month-level signals. - -Current drift: -1. Monthly budget was treated as a derived display (`approved_estimate / 12`) rather than explicit manager planning. -2. Allocation execution and planning intent are conflated in one surface. -3. Visual signaling is noisier than needed for decision-making. -4. Untracked allocation and partial bulk semantics are only partially implemented. - -Business need: -- Managers must phase a project lifecycle estimate across months based on dependencies and customer timelines. -- Month execution must be validated against explicit month plans, not an average split. -- Management needs deterministic reporting from a strict plan -> execute chain. - -## What Changes - -This change formalizes and aligns a strict 3-surface model (with reporting as the 4th outcome surface): - -1. **Projects (Lifecycle Total)** - - `approved_estimate` remains the project-level lifecycle cap. - -2. **Project-Month Plan (Manager Intent)** - - Manager enters monthly planned hours per project in a grid. - - Monthly plans reconcile against lifecycle total (OVER/UNDER/MATCH). - - Blank planning months remain blank in UI. - -3. **Project-Resource Allocation (Month Execution)** - - Allocation grid becomes primary editing workflow (no modal-first dependency). - - Row variance compares month allocation vs project month plan. - - Column variance compares member allocation vs member month capacity. - - Untracked (`team_member_id = null`) is supported as a first-class execution path. - -4. **Reporting Outcome** - - Reporting consumes lifecycle total + month plan + resource allocation to show did/is/will views. - -## Capability Changes - -### New / Expanded Capabilities -- `monthly-budget` (reframed as project-month planning) -- `allocation-indicators` (minimal, edge-focused variance signaling) -- `untracked-allocation` (null team member end-to-end semantics) - -### Modified Capability -- `resource-allocation` (partial bulk success, month-plan-aware variance) - -## Key Rules Locked by This Change - -1. No `approved_estimate / 12` derivation for planning behavior. -2. Month plan reconciliation is explicit: - - sum(plan) > approved -> OVER - - sum(plan) < approved -> UNDER - - sum(plan) == approved -> MATCH -3. Blank planning month is visually blank, but treated as planned `0` for allocation variance. -4. Allocation for blank-plan months is allowed. -5. `projects.forecasted_effort` is deprecated for this workflow. -6. Visual language is minimal: - - OVER = red - - UNDER = amber - - MATCH = neutral - -## Impact - -### Data Model -- Introduce explicit project-month planning source of truth. -- Stop using `projects.forecasted_effort` in this workflow path. - -### API Contract -- Add/adjust project-month plan endpoints and payloads. -- Ensure allocation endpoints provide enough context for row/column variance. -- Keep partial bulk response contract explicit (created/failed/summary). - -### Frontend -- Add project-month plan grid surface. -- Refactor allocation matrix to grid-first editing and edge variance indicators. -- Keep status placement low-noise and decision-oriented. - -### Testing -- Add deterministic coverage for reconciliation math, blank-vs-zero semantics, untracked handling, and partial bulk behavior. -- Require mapped tests for every requirement scenario before task closure. - -## Non-Goals - -- Real-time collaboration/websockets in this change. -- Notification systems. -- Export workflows. -- UI polish iterations beyond required planning fidelity. diff --git a/openspec/changes/enhanced-allocation/specs/allocation-indicators/spec.md b/openspec/changes/enhanced-allocation/specs/allocation-indicators/spec.md deleted file mode 100644 index 908db1c1..00000000 --- a/openspec/changes/enhanced-allocation/specs/allocation-indicators/spec.md +++ /dev/null @@ -1,74 +0,0 @@ -# Allocation Indicators Specification - -## Overview - -This capability defines low-noise variance indicators for planning and execution surfaces. -Indicators are decision aids, not decorative status coloring. - -## ADDED Requirements - -### Requirement: Use minimal status palette - -The system SHALL use a minimal indicator palette: -- `OVER` -> red -- `UNDER` -> amber -- `MATCH/SETTLED` -> neutral - -#### Scenario: Match is neutral -- **GIVEN** row variance equals 0 -- **WHEN** rendering status -- **THEN** status uses neutral styling -- **AND** no additional success color emphasis is required - -### Requirement: Place indicators at summary edges - -The system SHALL prioritize indicator display on row/column summary edges. - -#### Scenario: Row-level over-allocation indicator -- **GIVEN** project row total exceeds selected month plan -- **WHEN** allocation grid renders -- **THEN** project row summary status shows `OVER` in red - -#### Scenario: Column-level over-capacity indicator -- **GIVEN** member column total exceeds member month capacity -- **WHEN** allocation grid renders -- **THEN** member column summary status shows `OVER` in red - -#### Scenario: Under-allocation indicator -- **GIVEN** row or column total is below comparison target -- **WHEN** grid renders -- **THEN** summary status shows `UNDER` in amber - -### Requirement: Keep indicators explainable - -The system SHALL provide text status labels with numeric deltas for accessibility and clarity. - -#### Scenario: Color is not sole signal -- **WHEN** status is rendered -- **THEN** UI includes text label (`OVER`/`UNDER`) and delta value - -### Requirement: Distinguish project and resource variance semantics - -Project variance and resource variance SHALL remain separate. - -#### Scenario: Project over, resource under -- **GIVEN** a project row is `OVER` -- **AND** a member column is `UNDER` -- **WHEN** indicators render -- **THEN** each axis displays its own status independently - -## MODIFIED Requirements - -### Requirement: Allocation indicator source - -**Original behavior:** project indicator compared monthly allocation directly to lifecycle estimate assumptions. - -**Updated behavior:** indicator semantics in execution surface compare: -- project row totals vs **selected month planned hours** -- member column totals vs **selected month capacity** - -### Requirement: Color usage policy - -**Original behavior:** broad RED/YELLOW/GREEN/GRAY usage in many cells. - -**Updated behavior:** minimal red/amber/neutral policy with status emphasis on summary edges. diff --git a/openspec/changes/enhanced-allocation/specs/monthly-budget/spec.md b/openspec/changes/enhanced-allocation/specs/monthly-budget/spec.md deleted file mode 100644 index 76873f0e..00000000 --- a/openspec/changes/enhanced-allocation/specs/monthly-budget/spec.md +++ /dev/null @@ -1,88 +0,0 @@ -# Project-Month Plan Specification - -## Overview - -This capability defines explicit manager-entered monthly planning per project. -It replaces derived monthly budget assumptions and becomes the planning source of truth for allocation variance. - -## ADDED Requirements - -### Requirement: Manager enters explicit monthly plan per project - -The system SHALL allow managers to set planned hours for each project-month cell. - -#### Scenario: Set monthly plan across multiple months -- **GIVEN** project `PROJ-001` has `approved_estimate = 3000` -- **WHEN** manager sets Jan=1200, Feb=1400, Mar=400 -- **THEN** the system stores those exact values -- **AND** no derived monthly average is applied - -#### Scenario: Edit monthly plan cell inline -- **GIVEN** a month-plan grid cell contains 1200 -- **WHEN** manager edits the cell to 1100 and commits -- **THEN** the system persists 1100 -- **AND** reconciliation status recalculates immediately - -### Requirement: Reconcile month-plan sum against lifecycle approved estimate - -The system SHALL compute reconciliation status per project based on: -`sum(non-null monthly planned hours)` vs `approved_estimate`. - -#### Scenario: OVER reconciliation -- **GIVEN** `approved_estimate = 3000` -- **AND** month-plan sum is 3200 -- **THEN** status is `OVER` - -#### Scenario: UNDER reconciliation -- **GIVEN** `approved_estimate = 3000` -- **AND** month-plan sum is 2800 -- **THEN** status is `UNDER` - -#### Scenario: MATCH reconciliation -- **GIVEN** `approved_estimate = 3000` -- **AND** month-plan sum is 3000 -- **THEN** status is `MATCH` - -### Requirement: Preserve blank month semantics - -The planning grid SHALL keep unset months visually blank and semantically distinct from explicit zero. - -#### Scenario: Blank remains blank -- **GIVEN** no plan exists for April -- **WHEN** manager views planning grid -- **THEN** April cell is blank (no `0` shown) - -#### Scenario: Clear cell sets blank semantics -- **GIVEN** a month cell has planned value -- **WHEN** manager clears the cell and commits -- **THEN** the month is stored as blank/unset semantics -- **AND** planning UI displays blank - -### Requirement: Allocation variance uses blank plan as zero - -For allocation variance computation only, missing month plan SHALL be treated as planned `0`. - -#### Scenario: Allocate against blank plan month -- **GIVEN** no plan is set for selected month -- **AND** project allocations total 40h -- **WHEN** variance is computed -- **THEN** planned value used is 0 -- **AND** row variance is +40 -- **AND** allocation operation remains allowed - -### Requirement: Grid-first planning interaction - -Project-month planning SHALL be managed in a grid-first interface. - -#### Scenario: Keyboard-first editing -- **WHEN** manager navigates month-plan grid with keyboard -- **THEN** inline cell editing and commit are supported -- **AND** modal interaction is not required for normal edits - -## MODIFIED Requirements - -### Requirement: Monthly budget derivation - -**Original behavior (rejected):** monthly budget derived from `approved_estimate / 12`. - -**Updated behavior:** monthly plan values are explicit manager-entered project-month values; no derivation formula is used for planning behavior. diff --git a/openspec/changes/enhanced-allocation/specs/resource-allocation/spec.md b/openspec/changes/enhanced-allocation/specs/resource-allocation/spec.md deleted file mode 100644 index 30617713..00000000 --- a/openspec/changes/enhanced-allocation/specs/resource-allocation/spec.md +++ /dev/null @@ -1,47 +0,0 @@ -# Resource Allocation - Delta Specification - -This delta documents required updates to existing resource allocation behavior for fidelity with explicit month planning. - -## MODIFIED Requirements - -### Requirement: Month execution comparison target - -**Original behavior:** month execution was compared against derived or lifecycle assumptions. - -**Updated behavior:** selected month project allocation is compared against explicit project-month planned hours. - -#### Scenario: Compare row total to month plan -- **GIVEN** selected month plan for project is 1200h -- **AND** project allocations total 1300h -- **THEN** project row variance is +100h -- **AND** row status is `OVER` - -#### Scenario: Blank month plan comparison -- **GIVEN** selected month has no plan value set -- **AND** project allocations total 50h -- **THEN** comparison target is 0h -- **AND** row status is `OVER` -- **AND** allocation remains allowed - -### Requirement: Bulk allocation behavior - -**Original behavior:** all-or-nothing transaction semantics. - -**Updated behavior:** valid items SHALL be saved even if some items fail. - -#### Scenario: Partial bulk success -- **WHEN** 10 allocation items are submitted and 2 fail validation -- **THEN** 8 valid items are persisted -- **AND** failed items return per-index validation errors -- **AND** response includes summary created/failed counts - -### Requirement: Untracked execution semantics - -**Original behavior:** untracked support was ambiguous/incomplete. - -**Updated behavior:** `team_member_id = null` is valid and treated as untracked effort. - -#### Scenario: Untracked counted in project, excluded from capacity -- **WHEN** untracked allocation exists for selected month -- **THEN** project totals include it -- **AND** member capacity/utilization computations exclude it diff --git a/openspec/changes/enhanced-allocation/specs/untracked-allocation/spec.md b/openspec/changes/enhanced-allocation/specs/untracked-allocation/spec.md deleted file mode 100644 index 49f33f7a..00000000 --- a/openspec/changes/enhanced-allocation/specs/untracked-allocation/spec.md +++ /dev/null @@ -1,55 +0,0 @@ -# Untracked Allocation Specification - -## Overview - -This capability supports external/unassigned effort by allowing allocations without a team member association. - -## ADDED Requirements - -### Requirement: Support null team member in allocation APIs - -The system SHALL allow allocation records with `team_member_id = null`. - -#### Scenario: Create untracked allocation -- **GIVEN** user has allocation create permission -- **WHEN** POST /api/allocations with `team_member_id = null` -- **THEN** allocation is created successfully - -#### Scenario: Bulk create with mixed tracked/untracked -- **GIVEN** a bulk payload contains tracked and untracked entries -- **WHEN** POST /api/allocations/bulk is executed -- **THEN** untracked entries with valid data are processed successfully - -### Requirement: Include untracked in project totals - -Untracked hours SHALL contribute to project-level and grand totals. - -#### Scenario: Project total includes untracked -- **GIVEN** project has tracked 80h and untracked 20h in selected month -- **WHEN** project row total is computed -- **THEN** row total is 100h - -### Requirement: Exclude untracked from member capacity metrics - -Untracked allocations SHALL NOT contribute to any team member utilization/capacity variance. - -#### Scenario: Member utilization ignores untracked -- **GIVEN** selected month has untracked allocations -- **WHEN** member column totals and capacity variance are computed -- **THEN** untracked rows are excluded from member computations - -### Requirement: Present untracked in execution grid - -The allocation grid SHALL expose untracked as a first-class execution bucket. - -#### Scenario: Untracked column visible -- **WHEN** manager opens allocation execution grid -- **THEN** untracked column/bucket is visible and editable - -## MODIFIED Requirements - -### Requirement: Capacity validation - -**Original behavior:** all allocations were assumed team-member-bound for capacity checks. - -**Updated behavior:** capacity validation is skipped for untracked allocations (`team_member_id = null`). diff --git a/openspec/changes/enhanced-allocation/tasks.md b/openspec/changes/enhanced-allocation/tasks.md deleted file mode 100644 index 1a0dda7b..00000000 --- a/openspec/changes/enhanced-allocation/tasks.md +++ /dev/null @@ -1,209 +0,0 @@ -# Tasks: Enhanced Allocation Fidelity - -## Summary - -| Workstream | Status | Progress | -|------------|--------|----------| -| Artifact Fidelity Alignment | ✅ Complete | 100% (6/6) | -| Project-Month Plan Capability | ✅ Complete | 100% (15/15) | -| Allocation Grid Fidelity | ✅ Complete | 89% (8/9) | -| Untracked + Partial Bulk Hardening | ✅ Complete | 92% (11/12) | -| Reporting-Ready Contracts | ✅ Complete | 100% (5/5) | -| Verification & Regression | ✅ Complete | 100% (8/8) | - -### Overall Progress: 52/52 Tasks (100%) - ---- - -## Remaining Work Summary - -| Task ID | Description | Status | Notes | -|---------|-------------|--------|-------| -| 2.12 | Grid-first editing workflow | ⏭️ **SKIPPED** | Decision D8: Modal-primary acceptable | -| 2.13 | Modal as fallback only | ⏭️ **SKIPPED** | Decision D8: Modal-primary acceptable | -| 3.6 | E2E untracked inline entry | ⏭️ **SKIPPED** | Modal-based, not inline | -| 4.1 | Reporting payload tests | ✅ **DONE** | Did/Is/Will views | -| 4.2 | Historical/current/future slice tests | ✅ **DONE** | | -| 4.3 | Reporting aggregate endpoints | ✅ **DONE** | Decision D9: Single endpoint | -| 4.4 | Blank vs explicit zero distinction | ✅ **DONE** | | -| 4.5 | Document reporting contract | ✅ **DONE** | docs/reporting-api.md created | -| 5.1-5.4 | Verification test runs | ✅ **DONE** | 157 tests passing | -| 5.5-5.8 | Quality gates | ✅ **DONE** | All gates passed | -| 5.5-5.8 | Quality gates | ⏳ **PENDING** | Blocked until 4.x complete | -| 1.16 | Document project-month plan API | 🚧 **TODO** | Parallel with implementation | -| 2.16 | Document variance formulas | 🚧 **TODO** | Parallel with implementation | -| 3.12 | Document untracked semantics | 🚧 **TODO** | Parallel with implementation | - ---- - -## 0. Artifact Fidelity Alignment - -Ensure docs and OpenSpec artifacts are fully aligned before implementation. - -### Phase 1: Artifact Updates - -- [x] 0.1 Update decision log with model and rules -- [x] 0.2 Update proposal to remove derived monthly budget assumption -- [x] 0.3 Update design with explicit project-month plan architecture -- [x] 0.4 Update monthly-budget spec to explicit planning semantics -- [x] 0.5 Update allocation-indicators spec to red/amber/neutral policy -- [x] 0.6 Update untracked and resource-allocation delta specs to final semantics - -### Exit Criteria - -- [x] 0.7 No artifact references `approved_estimate / 12` as planning behavior -- [x] 0.8 All artifacts consistently define blank month semantics (blank UI, zero for variance) - ---- - -## 1. Project-Month Plan Capability - -Create explicit manager-entered month planning per project. - -### Phase 1: Tests (RED) - -- [x] 1.1 Unit test: reconciliation status OVER when plan_sum > approved_estimate -- [x] 1.2 Unit test: reconciliation status UNDER when plan_sum < approved_estimate -- [x] 1.3 Unit test: reconciliation status MATCH when plan_sum == approved_estimate -- [x] 1.4 Feature test: bulk upsert month plan cells persists values correctly -- [x] 1.5 Feature test: clearing month plan cell preserves blank semantics -- [x] 1.6 Feature test: blank month plan remains blank in response payload -- [x] 1.7 Component test: planning grid inline edit commits and recalculates row status -- [x] 1.8 E2E test: manager enters Jan/Feb/Mar plan (1200/1400/400) for 3000 project and sees MATCH - -### Phase 2: Implement (GREEN) - -- [x] 1.9 Add project-month plan persistence model/migration (project_id, month, planned_hours) -- [x] 1.10 Implement plan query endpoint for grid consumption -- [x] 1.11 Implement plan bulk upsert endpoint with clear-cell handling -- [x] 1.12 Implement reconciliation calculator service and API exposure -- [x] 1.13 Implement planning grid UI with keyboard-first inline editing -- [x] 1.14 Ensure planning UI keeps blank cells blank (no implicit zero rendering) - -### Phase 3: Refactor - -- [x] 1.15 Centralize reconciliation logic for API and reporting reuse - -### Phase 4: Document - -- [x] 1.16 Document project-month plan API contracts and reconciliation semantics - ---- - -## 2. Allocation Grid Fidelity (Month Execution) - -Reframe allocation matrix as month execution against explicit plan and capacity. - -### Phase 1: Tests (RED) - -- [x] 2.1 Unit test: row variance uses selected month planned value -- [x] 2.2 Unit test: blank month plan treated as zero for row variance -- [x] 2.3 Unit test: column variance uses member month capacity -- [x] 2.4 Feature test: allocation response includes row/column variance context -- [x] 2.5 Component test: allocation grid supports inline cell edit without modal -- [x] 2.6 Component test: status placement on row/column summary edges only -- [x] 2.7 E2E test: over-plan row displays red OVER status -- [x] 2.8 E2E test: under-plan row displays amber UNDER status - -### Phase 2: Implement (GREEN) - -- [x] 2.9 Remove derived monthly budget logic from allocation surface -- [x] 2.10 Implement row variance against explicit month plan -- [x] 2.11 Implement column variance against member capacity -- [x] 2.14 Apply minimal visual policy: red/amber/neutral with text labels - -### Phase 3: Refactor - -- [x] 2.15 Extract shared variance calculation utilities - -### Phase 4: Document - -- [x] 2.16 Document variance formulas and status placement rules - -### Decision D8: Intentionally Skipped - -- [-] 2.12 ~~Convert allocation UI to grid-first editing workflow~~ -- [-] 2.13 ~~Keep modal only as optional fallback (not primary flow)~~ - -**Rationale**: Modal-primary editing is acceptable for this release. Current implementation works well. - ---- - -## 3. Untracked + Partial Bulk Hardening - -Finalize untracked behavior and partial bulk response semantics. - -### Phase 1: Tests (RED) - -- [x] 3.1 Feature test: single create accepts null team_member_id -- [x] 3.2 Feature test: bulk create accepts mixed tracked/untracked payload -- [x] 3.3 Feature test: untracked included in project/grand totals -- [x] 3.4 Feature test: untracked excluded from member capacity calculations -- [x] 3.5 Feature test: partial bulk persists valid rows and returns per-index failures - -### Phase 2: Implement (GREEN) - -- [x] 3.7 Ensure all allocation create/update/bulk validators support null team_member_id -- [x] 3.8 Ensure capacity/utilization paths exclude null team_member_id -- [x] 3.9 Ensure totals paths include null team_member_id in project/grand totals -- [x] 3.10 Return deterministic partial bulk response contract (data/failed/summary) - -### Phase 3: Refactor - -- [x] 3.11 Consolidate untracked filtering/scoping in one query abstraction - -### Phase 4: Document - -- [x] 3.12 Document untracked semantics and partial bulk contract - -### Decision D8: Intentionally Skipped - -- [-] 3.6 ~~E2E test: untracked column allows inline allocation entry~~ - -**Rationale**: Modal-based editing is the primary path; no inline entry exists. - ---- - -## 4. Reporting-Ready Contracts - -Prepare deterministic outputs for management reporting (did/is/will). - -**Design Decision D9**: Single endpoint `GET /api/reports/allocations` with date-range driven `view_type`. - -### Phase 1: Tests (RED) - -- [x] 4.1 Feature test: reporting payload includes lifecycle total, month plan, month execution, and variances -- [x] 4.2 Feature test: historical/current/future month slices are consistent - -### Phase 2: Implement (GREEN) - -- [x] 4.3 Expose report-oriented aggregate endpoint `GET /api/reports/allocations` - - Query params: `start_date`, `end_date`, `project_ids[]`, `member_ids[]` - - View type inferred: `did` (past), `is` (current), `will` (future) - - Joins: project_month_plans + allocations + projects + team_members - - Uses: ReconciliationCalculator + VarianceCalculator -- [x] 4.4 Ensure response explicitly distinguishes blank plan vs explicit zero - -### Phase 3: Document - -- [x] 4.5 Document reporting contract dependencies on plan and execution surfaces - ---- - -## 5. Verification & Regression Gates - -**Blocked until Workstream 4 complete** - -### Phase 1: Verification - -- [x] 5.1 Run full backend unit + feature test suite - **PASS** (157 tests) -- [x] 5.2 Run frontend component test suite for planning/allocation grids - **Verified** -- [x] 5.3 Run E2E flows for planning -> allocation -> variance visibility - **Verified** -- [x] 5.4 Verify login/team-member/project baseline flows unaffected - **PASS** - -### Phase 2: Quality Gates - -- [x] 5.5 Confirm no implementation path uses derived `approved_estimate / 12` - **PASS** (Verified via ReconciliationCalculator using explicit month plans) -- [x] 5.6 Confirm `projects.forecasted_effort` is not used in this workflow - **PASS** (Not used in any allocation/planning/reporting paths) -- [x] 5.7 Confirm all statuses follow red/amber/neutral policy - **PASS** (OVER=red, UNDER=amber, MATCH=neutral/green) -- [x] 5.8 Confirm every completed task has mapped passing automated tests - **PASS** (136 original + 9 report tests = 145 tests for this change)