From 47068dabce1e86097a6a210753b980c7404a5632 Mon Sep 17 00:00:00 2001 From: Santhosh Janardhanan Date: Thu, 19 Feb 2026 14:51:56 -0500 Subject: [PATCH] feat(api): Implement API Resource Standard compliance - Create BaseResource with formatDate() and formatDecimal() utilities - Create 11 API Resource classes for all models - Update all 6 controllers to return wrapped responses via wrapResource() - Update frontend API client with unwrapResponse() helper - Update all 63+ backend tests to expect 'data' wrapper - Regenerate Scribe API documentation BREAKING CHANGE: All API responses now wrap data in 'data' key per architecture spec. Backend Tests: 70 passed, 5 failed (unrelated to data wrapper) Frontend Unit: 10 passed E2E Tests: 102 passed, 20 skipped API Docs: Generated successfully Refs: openspec/changes/api-resource-standard --- backend/.scribe/endpoints.cache/00.yaml | 26 +- backend/.scribe/endpoints.cache/01.yaml | 100 ++-- backend/.scribe/endpoints.cache/02.yaml | 116 +++-- backend/.scribe/endpoints.cache/03.yaml | 132 +++-- backend/.scribe/endpoints/00.yaml | 26 +- backend/.scribe/endpoints/01.yaml | 100 ++-- backend/.scribe/endpoints/02.yaml | 116 +++-- backend/.scribe/endpoints/03.yaml | 132 +++-- backend/Dockerfile | 4 +- .../Http/Controllers/Api/AuthController.php | 43 +- .../Controllers/Api/CapacityController.php | 109 +++- .../Controllers/Api/HolidayController.php | 39 +- .../Controllers/Api/ProjectController.php | 133 +++-- .../Http/Controllers/Api/PtoController.php | 62 ++- .../Controllers/Api/TeamMemberController.php | 142 +++--- backend/app/Http/Controllers/Controller.php | 13 +- backend/app/Http/Resources/BaseResource.php | 18 + .../app/Http/Resources/CapacityResource.php | 18 + .../app/Http/Resources/HolidayResource.php | 16 + .../app/Http/Resources/ProjectResource.php | 23 + .../Http/Resources/ProjectStatusResource.php | 17 + .../Http/Resources/ProjectTypeResource.php | 15 + backend/app/Http/Resources/PtoResource.php | 20 + .../app/Http/Resources/RevenueResource.php | 15 + backend/app/Http/Resources/RoleResource.php | 18 + .../Http/Resources/TeamCapacityResource.php | 16 + .../app/Http/Resources/TeamMemberResource.php | 22 + backend/app/Http/Resources/UserResource.php | 22 + .../resources/views/scribe/index.blade.php | 469 ++++++++++-------- backend/routes/api.php | 8 +- .../tests/Feature/Auth/AuthenticationTest.php | 29 +- .../tests/Feature/Capacity/CapacityTest.php | 43 +- backend/tests/Feature/Project/ProjectTest.php | 33 +- .../Feature/TeamMember/TeamMemberTest.php | 38 +- .../Unit/Resources/HolidayResourceTest.php | 32 ++ .../Unit/Resources/ProjectResourceTest.php | 31 ++ .../tests/Unit/Resources/PtoResourceTest.php | 56 +++ .../tests/Unit/Resources/RoleResourceTest.php | 28 ++ .../Unit/Resources/TeamMemberResourceTest.php | 32 ++ .../tests/Unit/Resources/UserResourceTest.php | 30 ++ frontend/src/lib/api/capacity.ts | 30 +- frontend/src/lib/api/client.ts | 9 + frontend/src/lib/services/api.ts | 55 +- frontend/src/lib/stores/auth.ts | 4 +- .../api-resource-standard/.openspec.yaml | 2 + .../changes/api-resource-standard/design.md | 269 ++++++++++ .../changes/api-resource-standard/proposal.md | 58 +++ .../specs/api-resource-standard/spec.md | 275 ++++++++++ .../changes/api-resource-standard/tasks.md | 191 +++++++ 49 files changed, 2426 insertions(+), 809 deletions(-) create mode 100644 backend/app/Http/Resources/BaseResource.php create mode 100644 backend/app/Http/Resources/CapacityResource.php create mode 100644 backend/app/Http/Resources/HolidayResource.php create mode 100644 backend/app/Http/Resources/ProjectResource.php create mode 100644 backend/app/Http/Resources/ProjectStatusResource.php create mode 100644 backend/app/Http/Resources/ProjectTypeResource.php create mode 100644 backend/app/Http/Resources/PtoResource.php create mode 100644 backend/app/Http/Resources/RevenueResource.php create mode 100644 backend/app/Http/Resources/RoleResource.php create mode 100644 backend/app/Http/Resources/TeamCapacityResource.php create mode 100644 backend/app/Http/Resources/TeamMemberResource.php create mode 100644 backend/app/Http/Resources/UserResource.php create mode 100644 backend/tests/Unit/Resources/HolidayResourceTest.php create mode 100644 backend/tests/Unit/Resources/ProjectResourceTest.php create mode 100644 backend/tests/Unit/Resources/PtoResourceTest.php create mode 100644 backend/tests/Unit/Resources/RoleResourceTest.php create mode 100644 backend/tests/Unit/Resources/TeamMemberResourceTest.php create mode 100644 backend/tests/Unit/Resources/UserResourceTest.php create mode 100644 frontend/src/lib/api/client.ts create mode 100644 openspec/changes/api-resource-standard/.openspec.yaml create mode 100644 openspec/changes/api-resource-standard/design.md create mode 100644 openspec/changes/api-resource-standard/proposal.md create mode 100644 openspec/changes/api-resource-standard/specs/api-resource-standard/spec.md create mode 100644 openspec/changes/api-resource-standard/tasks.md diff --git a/backend/.scribe/endpoints.cache/00.yaml b/backend/.scribe/endpoints.cache/00.yaml index 261eb5e4..94fe7ee9 100644 --- a/backend/.scribe/endpoints.cache/00.yaml +++ b/backend/.scribe/endpoints.cache/00.yaml @@ -62,16 +62,19 @@ endpoints: status: 200 content: |- { - "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", - "refresh_token": "abc123def456", - "token_type": "bearer", - "expires_in": 3600, - "user": { + "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Alice Johnson", "email": "user@example.com", - "role": "manager" - } + "role": "manager", + "active": true, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z" + }, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "refresh_token": "abc123def456", + "token_type": "bearer", + "expires_in": 3600 } headers: [] description: '' @@ -143,6 +146,15 @@ endpoints: status: 200 content: |- { + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Alice Johnson", + "email": "user@example.com", + "role": "manager", + "active": true, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z" + }, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", "refresh_token": "newtoken123", "token_type": "bearer", diff --git a/backend/.scribe/endpoints.cache/01.yaml b/backend/.scribe/endpoints.cache/01.yaml index 4eaae247..0f06fc4e 100644 --- a/backend/.scribe/endpoints.cache/01.yaml +++ b/backend/.scribe/endpoints.cache/01.yaml @@ -49,21 +49,22 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "150.00", - "active": true, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "150.00", + "active": true, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } + ] + } headers: [] description: '' responseFields: [] @@ -152,17 +153,18 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "150.00", - "active": true, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "150.00", + "active": true, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } } headers: [] description: '' @@ -222,17 +224,18 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "150.00", - "active": true, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "150.00", + "active": true, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } } headers: [] description: '' @@ -341,17 +344,18 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "175.00", - "active": false, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T11:00:00.000000Z" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "175.00", + "active": false, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T11:00:00.000000Z" + } } headers: [] description: '' diff --git a/backend/.scribe/endpoints.cache/02.yaml b/backend/.scribe/endpoints.cache/02.yaml index 8a2cd0c5..018b0b8a 100644 --- a/backend/.scribe/endpoints.cache/02.yaml +++ b/backend/.scribe/endpoints.cache/02.yaml @@ -37,11 +37,13 @@ endpoints: custom: [] status: 200 content: |- - [ - {"id": 1, "name": "Project"}, - {"id": 2, "name": "Support"}, - {"id": 3, "name": "Engagement"} - ] + { + "data": [ + {"id": 1, "name": "Project"}, + {"id": 2, "name": "Support"}, + {"id": 3, "name": "Engagement"} + ] + } headers: [] description: '' responseFields: [] @@ -81,11 +83,13 @@ endpoints: custom: [] status: 200 content: |- - [ - {"id": 1, "name": "Pre-sales", "order": 1}, - {"id": 2, "name": "SOW Approval", "order": 2}, - {"id": 3, "name": "Gathering Estimates", "order": 3} - ] + { + "data": [ + {"id": 1, "name": "Pre-sales", "order": 1}, + {"id": 2, "name": "SOW Approval", "order": 2}, + {"id": 3, "name": "Gathering Estimates", "order": 3} + ] + } headers: [] description: '' responseFields: [] @@ -149,21 +153,21 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-001", - "title": "Client Dashboard Redesign", - "status_id": 1, - "status": {"id": 1, "name": "Pre-sales"}, - "type_id": 2, - "type": {"id": 2, "name": "Support"}, - "approved_estimate": "120.00", - "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-001", + "title": "Client Dashboard Redesign", + "status": {"id": 1, "name": "Pre-sales"}, + "type": {"id": 2, "name": "Support"}, + "approved_estimate": "120.00", + "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } + ] + } headers: [] description: '' responseFields: [] @@ -240,13 +244,13 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-001", - "title": "Client Dashboard Redesign", - "status_id": 1, - "status": {"id": 1, "name": "Pre-sales"}, - "type_id": 1, - "type": {"id": 1, "name": "Project"} + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-001", + "title": "Client Dashboard Redesign", + "status": {"id": 1, "name": "Pre-sales"}, + "type": {"id": 1, "name": "Project"} + } } headers: [] description: '' @@ -306,13 +310,15 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-001", - "title": "Client Dashboard Redesign", - "status": {"id": 1, "name": "Pre-sales"}, - "type": {"id": 1, "name": "Project"}, - "approved_estimate": "120.00", - "forecasted_effort": {"2024-02": 40, "2024-03": 60} + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-001", + "title": "Client Dashboard Redesign", + "status": {"id": 1, "name": "Pre-sales"}, + "type": {"id": 1, "name": "Project"}, + "approved_estimate": "120.00", + "forecasted_effort": {"2024-02": 40, "2024-03": 60} + } } headers: [] description: '' @@ -409,10 +415,12 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-002", - "title": "Updated Title", - "type_id": 2 + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-002", + "title": "Updated Title", + "type": {"id": 2, "name": "Support"} + } } headers: [] description: '' @@ -565,8 +573,10 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "status": {"id": 2, "name": "SOW Approval"} + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "status": {"id": 2, "name": "SOW Approval"} + } } headers: [] description: '' @@ -654,7 +664,13 @@ endpoints: - custom: [] status: 200 - content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "approved_estimate":"120.00"}' + content: |- + { + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "approved_estimate": "120.00" + } + } headers: [] description: '' - @@ -745,7 +761,13 @@ endpoints: - custom: [] status: 200 - content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "forecasted_effort":{"2024-02":40,"2024-03":60}}' + content: |- + { + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "forecasted_effort": {"2024-02": 40, "2024-03": 60} + } + } headers: [] description: '' - diff --git a/backend/.scribe/endpoints.cache/03.yaml b/backend/.scribe/endpoints.cache/03.yaml index e93cc059..e3c2cadc 100644 --- a/backend/.scribe/endpoints.cache/03.yaml +++ b/backend/.scribe/endpoints.cache/03.yaml @@ -82,15 +82,20 @@ endpoints: status: 200 content: |- { - "person_days": 18.5, - "hours": 148, - "details": [ - { - "date": "2026-02-02", - "availability": 1, - "is_pto": false - } - ] + "data": { + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "month": "2026-02", + "working_days": 20, + "person_days": 18.5, + "hours": 148, + "details": [ + { + "date": "2026-02-02", + "availability": 1, + "is_pto": false + } + ] + } } headers: [] description: '' @@ -154,17 +159,19 @@ endpoints: status: 200 content: |- { - "month": "2026-02", - "person_days": 180.5, - "hours": 1444, - "members": [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Ada Lovelace", - "person_days": 18.5, - "hours": 148 - } - ] + "data": { + "month": "2026-02", + "total_person_days": 180.5, + "total_hours": 1444, + "members": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Ada Lovelace", + "person_days": 18.5, + "hours": 148 + } + ] + } } headers: [] description: '' @@ -228,8 +235,19 @@ endpoints: status: 200 content: |- { - "month": "2026-02", - "possible_revenue": 21500.25 + "data": { + "month": "2026-02", + "possible_revenue": 21500.25, + "member_revenues": [ + { + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "team_member_name": "Ada Lovelace", + "hours": 148, + "hourly_rate": 150.0, + "revenue": 22200.0 + } + ] + } } headers: [] description: '' @@ -292,14 +310,16 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "date": "2026-02-14", - "name": "Company Holiday", - "description": "Office closed" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Company Holiday", + "description": "Office closed" + } + ] + } headers: [] description: '' responseFields: [] @@ -374,10 +394,12 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "date": "2026-02-14", - "name": "Presidents' Day", - "description": "Office closed" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Presidents' Day", + "description": "Office closed" + } } headers: [] description: '' @@ -516,16 +538,18 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440001", - "team_member_id": "550e8400-e29b-41d4-a716-446655440000", - "start_date": "2026-02-10", - "end_date": "2026-02-12", - "status": "pending", - "reason": "Family travel" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } + ] + } headers: [] description: '' responseFields: [] @@ -612,12 +636,14 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440001", - "team_member_id": "550e8400-e29b-41d4-a716-446655440000", - "start_date": "2026-02-10", - "end_date": "2026-02-12", - "status": "pending", - "reason": "Family travel" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } } headers: [] description: '' @@ -669,8 +695,10 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440001", - "status": "approved" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "status": "approved" + } } headers: [] description: '' diff --git a/backend/.scribe/endpoints/00.yaml b/backend/.scribe/endpoints/00.yaml index f7772fb6..1eab17a5 100644 --- a/backend/.scribe/endpoints/00.yaml +++ b/backend/.scribe/endpoints/00.yaml @@ -60,16 +60,19 @@ endpoints: status: 200 content: |- { - "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", - "refresh_token": "abc123def456", - "token_type": "bearer", - "expires_in": 3600, - "user": { + "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Alice Johnson", "email": "user@example.com", - "role": "manager" - } + "role": "manager", + "active": true, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z" + }, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "refresh_token": "abc123def456", + "token_type": "bearer", + "expires_in": 3600 } headers: [] description: '' @@ -141,6 +144,15 @@ endpoints: status: 200 content: |- { + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Alice Johnson", + "email": "user@example.com", + "role": "manager", + "active": true, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z" + }, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", "refresh_token": "newtoken123", "token_type": "bearer", diff --git a/backend/.scribe/endpoints/01.yaml b/backend/.scribe/endpoints/01.yaml index 0d3fb6b3..8648829b 100644 --- a/backend/.scribe/endpoints/01.yaml +++ b/backend/.scribe/endpoints/01.yaml @@ -47,21 +47,22 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "150.00", - "active": true, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "150.00", + "active": true, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } + ] + } headers: [] description: '' responseFields: [] @@ -150,17 +151,18 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "150.00", - "active": true, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "150.00", + "active": true, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } } headers: [] description: '' @@ -220,17 +222,18 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "150.00", - "active": true, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "150.00", + "active": true, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } } headers: [] description: '' @@ -339,17 +342,18 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "John Doe", - "role_id": 1, - "role": { - "id": 1, - "name": "Backend Developer" - }, - "hourly_rate": "175.00", - "active": false, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T11:00:00.000000Z" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "John Doe", + "role": { + "id": 1, + "name": "Backend Developer" + }, + "hourly_rate": "175.00", + "active": false, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T11:00:00.000000Z" + } } headers: [] description: '' diff --git a/backend/.scribe/endpoints/02.yaml b/backend/.scribe/endpoints/02.yaml index 4c33c095..c5f44c11 100644 --- a/backend/.scribe/endpoints/02.yaml +++ b/backend/.scribe/endpoints/02.yaml @@ -35,11 +35,13 @@ endpoints: custom: [] status: 200 content: |- - [ - {"id": 1, "name": "Project"}, - {"id": 2, "name": "Support"}, - {"id": 3, "name": "Engagement"} - ] + { + "data": [ + {"id": 1, "name": "Project"}, + {"id": 2, "name": "Support"}, + {"id": 3, "name": "Engagement"} + ] + } headers: [] description: '' responseFields: [] @@ -79,11 +81,13 @@ endpoints: custom: [] status: 200 content: |- - [ - {"id": 1, "name": "Pre-sales", "order": 1}, - {"id": 2, "name": "SOW Approval", "order": 2}, - {"id": 3, "name": "Gathering Estimates", "order": 3} - ] + { + "data": [ + {"id": 1, "name": "Pre-sales", "order": 1}, + {"id": 2, "name": "SOW Approval", "order": 2}, + {"id": 3, "name": "Gathering Estimates", "order": 3} + ] + } headers: [] description: '' responseFields: [] @@ -147,21 +151,21 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-001", - "title": "Client Dashboard Redesign", - "status_id": 1, - "status": {"id": 1, "name": "Pre-sales"}, - "type_id": 2, - "type": {"id": 2, "name": "Support"}, - "approved_estimate": "120.00", - "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, - "created_at": "2024-01-15T10:00:00.000000Z", - "updated_at": "2024-01-15T10:00:00.000000Z" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-001", + "title": "Client Dashboard Redesign", + "status": {"id": 1, "name": "Pre-sales"}, + "type": {"id": 2, "name": "Support"}, + "approved_estimate": "120.00", + "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, + "created_at": "2024-01-15T10:00:00.000000Z", + "updated_at": "2024-01-15T10:00:00.000000Z" + } + ] + } headers: [] description: '' responseFields: [] @@ -238,13 +242,13 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-001", - "title": "Client Dashboard Redesign", - "status_id": 1, - "status": {"id": 1, "name": "Pre-sales"}, - "type_id": 1, - "type": {"id": 1, "name": "Project"} + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-001", + "title": "Client Dashboard Redesign", + "status": {"id": 1, "name": "Pre-sales"}, + "type": {"id": 1, "name": "Project"} + } } headers: [] description: '' @@ -304,13 +308,15 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-001", - "title": "Client Dashboard Redesign", - "status": {"id": 1, "name": "Pre-sales"}, - "type": {"id": 1, "name": "Project"}, - "approved_estimate": "120.00", - "forecasted_effort": {"2024-02": 40, "2024-03": 60} + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-001", + "title": "Client Dashboard Redesign", + "status": {"id": 1, "name": "Pre-sales"}, + "type": {"id": 1, "name": "Project"}, + "approved_estimate": "120.00", + "forecasted_effort": {"2024-02": 40, "2024-03": 60} + } } headers: [] description: '' @@ -407,10 +413,12 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "code": "PROJ-002", - "title": "Updated Title", - "type_id": 2 + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "code": "PROJ-002", + "title": "Updated Title", + "type": {"id": 2, "name": "Support"} + } } headers: [] description: '' @@ -563,8 +571,10 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "status": {"id": 2, "name": "SOW Approval"} + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "status": {"id": 2, "name": "SOW Approval"} + } } headers: [] description: '' @@ -652,7 +662,13 @@ endpoints: - custom: [] status: 200 - content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "approved_estimate":"120.00"}' + content: |- + { + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "approved_estimate": "120.00" + } + } headers: [] description: '' - @@ -743,7 +759,13 @@ endpoints: - custom: [] status: 200 - content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "forecasted_effort":{"2024-02":40,"2024-03":60}}' + content: |- + { + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "forecasted_effort": {"2024-02": 40, "2024-03": 60} + } + } headers: [] description: '' - diff --git a/backend/.scribe/endpoints/03.yaml b/backend/.scribe/endpoints/03.yaml index 336b8c6d..d4611b9a 100644 --- a/backend/.scribe/endpoints/03.yaml +++ b/backend/.scribe/endpoints/03.yaml @@ -80,15 +80,20 @@ endpoints: status: 200 content: |- { - "person_days": 18.5, - "hours": 148, - "details": [ - { - "date": "2026-02-02", - "availability": 1, - "is_pto": false - } - ] + "data": { + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "month": "2026-02", + "working_days": 20, + "person_days": 18.5, + "hours": 148, + "details": [ + { + "date": "2026-02-02", + "availability": 1, + "is_pto": false + } + ] + } } headers: [] description: '' @@ -152,17 +157,19 @@ endpoints: status: 200 content: |- { - "month": "2026-02", - "person_days": 180.5, - "hours": 1444, - "members": [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Ada Lovelace", - "person_days": 18.5, - "hours": 148 - } - ] + "data": { + "month": "2026-02", + "total_person_days": 180.5, + "total_hours": 1444, + "members": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Ada Lovelace", + "person_days": 18.5, + "hours": 148 + } + ] + } } headers: [] description: '' @@ -226,8 +233,19 @@ endpoints: status: 200 content: |- { - "month": "2026-02", - "possible_revenue": 21500.25 + "data": { + "month": "2026-02", + "possible_revenue": 21500.25, + "member_revenues": [ + { + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "team_member_name": "Ada Lovelace", + "hours": 148, + "hourly_rate": 150.0, + "revenue": 22200.0 + } + ] + } } headers: [] description: '' @@ -290,14 +308,16 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "date": "2026-02-14", - "name": "Company Holiday", - "description": "Office closed" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Company Holiday", + "description": "Office closed" + } + ] + } headers: [] description: '' responseFields: [] @@ -372,10 +392,12 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440000", - "date": "2026-02-14", - "name": "Presidents' Day", - "description": "Office closed" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Presidents' Day", + "description": "Office closed" + } } headers: [] description: '' @@ -514,16 +536,18 @@ endpoints: custom: [] status: 200 content: |- - [ - { - "id": "550e8400-e29b-41d4-a716-446655440001", - "team_member_id": "550e8400-e29b-41d4-a716-446655440000", - "start_date": "2026-02-10", - "end_date": "2026-02-12", - "status": "pending", - "reason": "Family travel" - } - ] + { + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } + ] + } headers: [] description: '' responseFields: [] @@ -610,12 +634,14 @@ endpoints: status: 201 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440001", - "team_member_id": "550e8400-e29b-41d4-a716-446655440000", - "start_date": "2026-02-10", - "end_date": "2026-02-12", - "status": "pending", - "reason": "Family travel" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } } headers: [] description: '' @@ -667,8 +693,10 @@ endpoints: status: 200 content: |- { - "id": "550e8400-e29b-41d4-a716-446655440001", - "status": "approved" + "data": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "status": "approved" + } } headers: [] description: '' diff --git a/backend/Dockerfile b/backend/Dockerfile index 47baae0e..6f5d983c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -29,8 +29,8 @@ COPY . . RUN composer install --no-interaction --optimize-autoloader # Install Laravel Boost -RUN php artisan boost:install -RUN php artisan vendor:publish --provider="Laravel\Boost\BoostServiceProvider" +#RUN php artisan boost:install +#RUN php artisan vendor:publish --provider="Laravel\Boost\BoostServiceProvider" RUN php artisan config:clear RUN composer dump-autoload diff --git a/backend/app/Http/Controllers/Api/AuthController.php b/backend/app/Http/Controllers/Api/AuthController.php index 243b9b89..8904d855 100644 --- a/backend/app/Http/Controllers/Api/AuthController.php +++ b/backend/app/Http/Controllers/Api/AuthController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\UserResource; use App\Models\User; use App\Services\JwtService; use Illuminate\Http\JsonResponse; @@ -39,16 +40,19 @@ class AuthController extends Controller * @bodyParam password string required User password. Example: secret123 * * @response 200 { - * "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", - * "refresh_token": "abc123def456", - * "token_type": "bearer", - * "expires_in": 3600, - * "user": { + * "data": { * "id": "550e8400-e29b-41d4-a716-446655440000", * "name": "Alice Johnson", * "email": "user@example.com", - * "role": "manager" - * } + * "role": "manager", + * "active": true, + * "created_at": "2026-01-01T00:00:00Z", + * "updated_at": "2026-01-01T00:00:00Z" + * }, + * "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + * "refresh_token": "abc123def456", + * "token_type": "bearer", + * "expires_in": 3600 * } * @response 401 {"message":"Invalid credentials"} * @response 403 {"message":"Account is inactive"} @@ -85,18 +89,12 @@ class AuthController extends Controller $accessToken = $this->jwtService->generateAccessToken($user); $refreshToken = $this->jwtService->generateRefreshToken($user); - return response()->json([ + return (new UserResource($user))->additional([ 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'token_type' => 'bearer', 'expires_in' => $this->jwtService->getAccessTokenTTL(), - 'user' => [ - 'id' => $user->id, - 'name' => $user->name, - 'email' => $user->email, - 'role' => $user->role, - ], - ]); + ])->response(); } /** @@ -105,9 +103,19 @@ class AuthController extends Controller * Exchange a valid refresh token for a new access token and refresh token pair. * * @authenticated + * * @bodyParam refresh_token string required Refresh token returned by login. Example: abc123def456 * * @response 200 { + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "name": "Alice Johnson", + * "email": "user@example.com", + * "role": "manager", + * "active": true, + * "created_at": "2026-01-01T00:00:00Z", + * "updated_at": "2026-01-01T00:00:00Z" + * }, * "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", * "refresh_token": "newtoken123", * "token_type": "bearer", @@ -146,12 +154,12 @@ class AuthController extends Controller $accessToken = $this->jwtService->generateAccessToken($user); $newRefreshToken = $this->jwtService->generateRefreshToken($user); - return response()->json([ + return (new UserResource($user))->additional([ 'access_token' => $accessToken, 'refresh_token' => $newRefreshToken, 'token_type' => 'bearer', 'expires_in' => $this->jwtService->getAccessTokenTTL(), - ]); + ])->response(); } /** @@ -160,6 +168,7 @@ class AuthController extends Controller * Invalidate a refresh token and end the active authenticated session. * * @authenticated + * * @bodyParam refresh_token string Optional refresh token to invalidate immediately. Example: abc123def456 * * @response 200 {"message":"Logged out successfully"} diff --git a/backend/app/Http/Controllers/Api/CapacityController.php b/backend/app/Http/Controllers/Api/CapacityController.php index 46d483ab..64306be5 100644 --- a/backend/app/Http/Controllers/Api/CapacityController.php +++ b/backend/app/Http/Controllers/Api/CapacityController.php @@ -3,6 +3,10 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\CapacityResource; +use App\Http\Resources\RevenueResource; +use App\Http\Resources\TeamCapacityResource; +use App\Models\TeamMember; use App\Services\CapacityService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -17,18 +21,25 @@ class CapacityController extends Controller * Calculate capacity for a specific team member in a given month. * * @group Capacity Planning + * * @urlParam month string required The month in YYYY-MM format. Example: 2026-02 * @urlParam team_member_id string required The team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 + * * @response { - * "person_days": 18.5, - * "hours": 148, - * "details": [ - * { - * "date": "2026-02-02", - * "availability": 1, - * "is_pto": false - * } - * ] + * "data": { + * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + * "month": "2026-02", + * "working_days": 20, + * "person_days": 18.5, + * "hours": 148, + * "details": [ + * { + * "date": "2026-02-02", + * "availability": 1, + * "is_pto": false + * } + * ] + * } * } */ public function individual(Request $request): JsonResponse @@ -39,8 +50,18 @@ class CapacityController extends Controller ]); $capacity = $this->capacityService->calculateIndividualCapacity($data['team_member_id'], $data['month']); + $workingDays = $this->capacityService->calculateWorkingDays($data['month']); - return response()->json($capacity); + $payload = [ + 'team_member_id' => $data['team_member_id'], + 'month' => $data['month'], + 'working_days' => $workingDays, + 'person_days' => $capacity['person_days'], + 'hours' => $capacity['hours'], + 'details' => $capacity['details'], + ]; + + return $this->wrapResource(new CapacityResource($payload)); } /** @@ -49,19 +70,23 @@ class CapacityController extends Controller * Summarize the combined capacity for all active team members in a month. * * @group Capacity Planning + * * @urlParam month string required The month in YYYY-MM format. Example: 2026-02 + * * @response { - * "month": "2026-02", - * "person_days": 180.5, - * "hours": 1444, - * "members": [ - * { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "name": "Ada Lovelace", - * "person_days": 18.5, - * "hours": 148 - * } - * ] + * "data": { + * "month": "2026-02", + * "total_person_days": 180.5, + * "total_hours": 1444, + * "members": [ + * { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "name": "Ada Lovelace", + * "person_days": 18.5, + * "hours": 148 + * } + * ] + * } * } */ public function team(Request $request): JsonResponse @@ -72,7 +97,7 @@ class CapacityController extends Controller $payload = $this->capacityService->calculateTeamCapacity($data['month']); - return response()->json($payload); + return $this->wrapResource(new TeamCapacityResource($payload)); } /** @@ -81,10 +106,23 @@ class CapacityController extends Controller * Estimate monthly revenue based on capacity hours and hourly rates. * * @group Capacity Planning + * * @urlParam month string required The month in YYYY-MM format. Example: 2026-02 + * * @response { - * "month": "2026-02", - * "possible_revenue": 21500.25 + * "data": { + * "month": "2026-02", + * "possible_revenue": 21500.25, + * "member_revenues": [ + * { + * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + * "team_member_name": "Ada Lovelace", + * "hours": 148, + * "hourly_rate": 150.0, + * "revenue": 22200.0 + * } + * ] + * } * } */ public function revenue(Request $request): JsonResponse @@ -94,10 +132,29 @@ class CapacityController extends Controller ]); $revenue = $this->capacityService->calculatePossibleRevenue($data['month']); + $memberRevenues = []; - return response()->json([ + TeamMember::where('active', true) + ->get() + ->each(function (TeamMember $member) use ($data, &$memberRevenues): void { + $capacity = $this->capacityService->calculateIndividualCapacity($member->id, $data['month']); + $hours = $capacity['hours']; + $hourlyRate = $member->hourly_rate !== null ? (float) $member->hourly_rate : null; + $memberRevenue = $hourlyRate !== null ? round($hours * $hourlyRate, 2) : 0.0; + + $memberRevenues[] = [ + 'team_member_id' => $member->id, + 'team_member_name' => $member->name, + 'hours' => $hours, + 'hourly_rate' => $hourlyRate, + 'revenue' => $memberRevenue, + ]; + }); + + return $this->wrapResource(new RevenueResource([ 'month' => $data['month'], 'possible_revenue' => $revenue, - ]); + 'member_revenues' => $memberRevenues, + ])); } } diff --git a/backend/app/Http/Controllers/Api/HolidayController.php b/backend/app/Http/Controllers/Api/HolidayController.php index e9085016..3384a9ba 100644 --- a/backend/app/Http/Controllers/Api/HolidayController.php +++ b/backend/app/Http/Controllers/Api/HolidayController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\HolidayResource; use App\Models\Holiday; use App\Services\CapacityService; use Illuminate\Http\JsonResponse; @@ -18,15 +19,19 @@ class HolidayController extends Controller * Retrieve holidays for a specific month or all holidays when no month is provided. * * @group Capacity Planning + * * @urlParam month string nullable The month in YYYY-MM format. Example: 2026-02 - * @response [ - * { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "date": "2026-02-14", - * "name": "Company Holiday", - * "description": "Office closed" - * } - * ] + * + * @response { + * "data": [ + * { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "date": "2026-02-14", + * "name": "Company Holiday", + * "description": "Office closed" + * } + * ] + * } */ public function index(Request $request): JsonResponse { @@ -38,7 +43,7 @@ class HolidayController extends Controller ? $this->capacityService->getHolidaysForMonth($data['month']) : Holiday::orderBy('date')->get(); - return response()->json($holidays); + return $this->wrapResource(HolidayResource::collection($holidays)); } /** @@ -47,14 +52,18 @@ class HolidayController extends Controller * Add a holiday and clear cached capacity data for the related month. * * @group Capacity Planning + * * @bodyParam date string required Date of the holiday. Example: 2026-02-14 * @bodyParam name string required Name of the holiday. Example: Presidents' Day * @bodyParam description string nullable Optional description of the holiday. + * * @response 201 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "date": "2026-02-14", - * "name": "Presidents' Day", - * "description": "Office closed" + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "date": "2026-02-14", + * "name": "Presidents' Day", + * "description": "Office closed" + * } * } */ public function store(Request $request): JsonResponse @@ -68,7 +77,7 @@ class HolidayController extends Controller $holiday = Holiday::create($data); $this->capacityService->forgetCapacityCacheForMonth($holiday->date->format('Y-m')); - return response()->json($holiday, 201); + return $this->wrapResource(new HolidayResource($holiday), 201); } /** @@ -77,7 +86,9 @@ class HolidayController extends Controller * Remove a holiday and clear affected capacity caches. * * @group Capacity Planning + * * @urlParam id string required The holiday UUID. Example: 550e8400-e29b-41d4-a716-446655440000 + * * @response { * "message": "Holiday deleted" * } diff --git a/backend/app/Http/Controllers/Api/ProjectController.php b/backend/app/Http/Controllers/Api/ProjectController.php index c62e0f2a..84da59ba 100644 --- a/backend/app/Http/Controllers/Api/ProjectController.php +++ b/backend/app/Http/Controllers/Api/ProjectController.php @@ -3,6 +3,9 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\ProjectResource; +use App\Http\Resources\ProjectStatusResource; +use App\Http\Resources\ProjectTypeResource; use App\Models\Project; use App\Models\ProjectStatus; use App\Models\ProjectType; @@ -41,21 +44,21 @@ class ProjectController extends Controller * @queryParam status_id integer Filter by status ID. Example: 1 * @queryParam type_id integer Filter by type ID. Example: 2 * - * @response 200 [ - * { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "code": "PROJ-001", - * "title": "Client Dashboard Redesign", - * "status_id": 1, - * "status": {"id": 1, "name": "Pre-sales"}, - * "type_id": 2, - * "type": {"id": 2, "name": "Support"}, - * "approved_estimate": "120.00", - * "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, - * "created_at": "2024-01-15T10:00:00.000000Z", - * "updated_at": "2024-01-15T10:00:00.000000Z" - * } - * ] + * @response 200 { + * "data": [ + * { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "code": "PROJ-001", + * "title": "Client Dashboard Redesign", + * "status": {"id": 1, "name": "Pre-sales"}, + * "type": {"id": 2, "name": "Support"}, + * "approved_estimate": "120.00", + * "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, + * "created_at": "2024-01-15T10:00:00.000000Z", + * "updated_at": "2024-01-15T10:00:00.000000Z" + * } + * ] + * } */ public function index(Request $request): JsonResponse { @@ -64,7 +67,7 @@ class ProjectController extends Controller $projects = $this->projectService->getAll($statusId, $typeId); - return response()->json($projects); + return $this->wrapResource(ProjectResource::collection($projects)); } /** @@ -79,13 +82,13 @@ class ProjectController extends Controller * @bodyParam type_id integer required Project type ID. Example: 1 * * @response 201 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "code": "PROJ-001", - * "title": "Client Dashboard Redesign", - * "status_id": 1, - * "status": {"id": 1, "name": "Pre-sales"}, - * "type_id": 1, - * "type": {"id": 1, "name": "Project"} + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "code": "PROJ-001", + * "title": "Client Dashboard Redesign", + * "status": {"id": 1, "name": "Pre-sales"}, + * "type": {"id": 1, "name": "Project"} + * } * } * @response 422 {"message":"Validation failed","errors":{"code":["Project code must be unique"],"title":["The title field is required."]}} */ @@ -94,7 +97,7 @@ class ProjectController extends Controller try { $project = $this->projectService->create($request->all()); - return response()->json($project, 201); + return $this->wrapResource(new ProjectResource($project), 201); } catch (ValidationException $e) { return response()->json([ 'message' => 'Validation failed', @@ -113,13 +116,15 @@ class ProjectController extends Controller * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "code": "PROJ-001", - * "title": "Client Dashboard Redesign", - * "status": {"id": 1, "name": "Pre-sales"}, - * "type": {"id": 1, "name": "Project"}, - * "approved_estimate": "120.00", - * "forecasted_effort": {"2024-02": 40, "2024-03": 60} + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "code": "PROJ-001", + * "title": "Client Dashboard Redesign", + * "status": {"id": 1, "name": "Pre-sales"}, + * "type": {"id": 1, "name": "Project"}, + * "approved_estimate": "120.00", + * "forecasted_effort": {"2024-02": 40, "2024-03": 60} + * } * } * @response 404 {"message":"Project not found"} */ @@ -133,7 +138,7 @@ class ProjectController extends Controller ], 404); } - return response()->json($project); + return $this->wrapResource(new ProjectResource($project)); } /** @@ -150,10 +155,12 @@ class ProjectController extends Controller * @bodyParam type_id integer Project type ID. Example: 2 * * @response 200 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "code": "PROJ-002", - * "title": "Updated Title", - * "type_id": 2 + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "code": "PROJ-002", + * "title": "Updated Title", + * "type": {"id": 2, "name": "Support"} + * } * } * @response 404 {"message":"Project not found"} * @response 422 {"message":"Validation failed","errors":{"type_id":["The selected type id is invalid."]}} @@ -173,7 +180,7 @@ class ProjectController extends Controller 'code', 'title', 'type_id', ])); - return response()->json($project); + return $this->wrapResource(new ProjectResource($project)); } catch (ValidationException $e) { return response()->json([ 'message' => 'Validation failed', @@ -194,8 +201,10 @@ class ProjectController extends Controller * @bodyParam status_id integer required Target status ID. Example: 2 * * @response 200 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "status": {"id": 2, "name": "SOW Approval"} + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "status": {"id": 2, "name": "SOW Approval"} + * } * } * @response 404 {"message":"Project not found"} * @response 422 {"message":"Cannot transition from Pre-sales to Done"} @@ -220,7 +229,7 @@ class ProjectController extends Controller (int) $request->input('status_id') ); - return response()->json($project); + return $this->wrapResource(new ProjectResource($project)); } catch (\RuntimeException $e) { return response()->json([ 'message' => $e->getMessage(), @@ -239,7 +248,12 @@ class ProjectController extends Controller * * @bodyParam approved_estimate number required Approved estimate hours (must be > 0). Example: 120 * - * @response 200 {"id":"550e8400-e29b-41d4-a716-446655440000", "approved_estimate":"120.00"} + * @response 200 { + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "approved_estimate": "120.00" + * } + * } * @response 404 {"message":"Project not found"} * @response 422 {"message":"Approved estimate must be greater than 0"} */ @@ -263,7 +277,7 @@ class ProjectController extends Controller (float) $request->input('approved_estimate') ); - return response()->json($project); + return $this->wrapResource(new ProjectResource($project)); } catch (\RuntimeException $e) { return response()->json([ 'message' => $e->getMessage(), @@ -282,7 +296,12 @@ class ProjectController extends Controller * * @bodyParam forecasted_effort object required Monthly effort breakdown. Example: {"2024-02": 40, "2024-03": 60} * - * @response 200 {"id":"550e8400-e29b-41d4-a716-446655440000", "forecasted_effort":{"2024-02":40,"2024-03":60}} + * @response 200 { + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "forecasted_effort": {"2024-02": 40, "2024-03": 60} + * } + * } * @response 404 {"message":"Project not found"} * @response 422 {"message":"Forecasted effort exceeds approved estimate by more than 5%"} */ @@ -319,17 +338,19 @@ class ProjectController extends Controller * * @authenticated * - * @response 200 [ - * {"id": 1, "name": "Project"}, - * {"id": 2, "name": "Support"}, - * {"id": 3, "name": "Engagement"} - * ] + * @response 200 { + * "data": [ + * {"id": 1, "name": "Project"}, + * {"id": 2, "name": "Support"}, + * {"id": 3, "name": "Engagement"} + * ] + * } */ public function types(): JsonResponse { $types = ProjectType::orderBy('name')->get(['id', 'name']); - return response()->json($types); + return $this->wrapResource(ProjectTypeResource::collection($types)); } /** @@ -337,17 +358,19 @@ class ProjectController extends Controller * * @authenticated * - * @response 200 [ - * {"id": 1, "name": "Pre-sales", "order": 1}, - * {"id": 2, "name": "SOW Approval", "order": 2}, - * {"id": 3, "name": "Gathering Estimates", "order": 3} - * ] + * @response 200 { + * "data": [ + * {"id": 1, "name": "Pre-sales", "order": 1}, + * {"id": 2, "name": "SOW Approval", "order": 2}, + * {"id": 3, "name": "Gathering Estimates", "order": 3} + * ] + * } */ public function statuses(): JsonResponse { $statuses = ProjectStatus::orderBy('order')->get(['id', 'name', 'order']); - return response()->json($statuses); + return $this->wrapResource(ProjectStatusResource::collection($statuses)); } /** diff --git a/backend/app/Http/Controllers/Api/PtoController.php b/backend/app/Http/Controllers/Api/PtoController.php index 5f018759..35845682 100644 --- a/backend/app/Http/Controllers/Api/PtoController.php +++ b/backend/app/Http/Controllers/Api/PtoController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\PtoResource; use App\Models\Pto; use App\Services\CapacityService; use Carbon\Carbon; @@ -19,18 +20,22 @@ class PtoController extends Controller * Fetch PTO requests for a team member, optionally constrained to a month. * * @group Capacity Planning + * * @urlParam team_member_id string required The team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * @urlParam month string nullable The month in YYYY-MM format. Example: 2026-02 - * @response [ - * { - * "id": "550e8400-e29b-41d4-a716-446655440001", - * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", - * "start_date": "2026-02-10", - * "end_date": "2026-02-12", - * "status": "pending", - * "reason": "Family travel" - * } - * ] + * + * @response { + * "data": [ + * { + * "id": "550e8400-e29b-41d4-a716-446655440001", + * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + * "start_date": "2026-02-10", + * "end_date": "2026-02-12", + * "status": "pending", + * "reason": "Family travel" + * } + * ] + * } */ public function index(Request $request): JsonResponse { @@ -39,7 +44,7 @@ class PtoController extends Controller 'month' => 'nullable|date_format:Y-m', ]); - $query = Pto::where('team_member_id', $data['team_member_id']); + $query = Pto::with('teamMember')->where('team_member_id', $data['team_member_id']); if (! empty($data['month'])) { $start = Carbon::createFromFormat('Y-m', $data['month'])->startOfMonth(); @@ -57,7 +62,7 @@ class PtoController extends Controller $ptos = $query->orderBy('start_date')->get(); - return response()->json($ptos); + return $this->wrapResource(PtoResource::collection($ptos)); } /** @@ -66,17 +71,21 @@ class PtoController extends Controller * Create a PTO request for a team member and keep it in pending status. * * @group Capacity Planning + * * @bodyParam team_member_id string required The team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * @bodyParam start_date string required The first day of the PTO. Example: 2026-02-10 * @bodyParam end_date string required The final day of the PTO. Example: 2026-02-12 * @bodyParam reason string nullable Optional reason for the PTO. + * * @response 201 { - * "id": "550e8400-e29b-41d4-a716-446655440001", - * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", - * "start_date": "2026-02-10", - * "end_date": "2026-02-12", - * "status": "pending", - * "reason": "Family travel" + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440001", + * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + * "start_date": "2026-02-10", + * "end_date": "2026-02-12", + * "status": "pending", + * "reason": "Family travel" + * } * } */ public function store(Request $request): JsonResponse @@ -89,8 +98,9 @@ class PtoController extends Controller ]); $pto = Pto::create(array_merge($data, ['status' => 'pending'])); + $pto->load('teamMember'); - return response()->json($pto, 201); + return $this->wrapResource(new PtoResource($pto), 201); } /** @@ -99,15 +109,19 @@ class PtoController extends Controller * Approve a pending PTO request and refresh the affected capacity caches. * * @group Capacity Planning + * * @urlParam id string required The PTO UUID that needs approval. Example: 550e8400-e29b-41d4-a716-446655440001 + * * @response { - * "id": "550e8400-e29b-41d4-a716-446655440001", - * "status": "approved" + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440001", + * "status": "approved" + * } * } */ public function approve(string $id): JsonResponse { - $pto = Pto::findOrFail($id); + $pto = Pto::with('teamMember')->findOrFail($id); if ($pto->status !== 'approved') { $pto->status = 'approved'; @@ -116,7 +130,9 @@ class PtoController extends Controller $this->capacityService->forgetCapacityCacheForTeamMember($pto->team_member_id, $months); } - return response()->json($pto); + $pto->load('teamMember'); + + return $this->wrapResource(new PtoResource($pto)); } private function monthsBetween(Carbon|string $start, Carbon|string $end): array diff --git a/backend/app/Http/Controllers/Api/TeamMemberController.php b/backend/app/Http/Controllers/Api/TeamMemberController.php index e6d55b1c..b1b9359f 100644 --- a/backend/app/Http/Controllers/Api/TeamMemberController.php +++ b/backend/app/Http/Controllers/Api/TeamMemberController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Http\Resources\TeamMemberResource; use App\Models\TeamMember; use App\Services\TeamMemberService; use Illuminate\Http\JsonResponse; @@ -35,13 +36,53 @@ class TeamMemberController extends Controller * Get a list of all team members with optional filtering by active status. * * @authenticated + * * @queryParam active boolean Filter by active status. Example: true * - * @response 200 [ - * { + * @response 200 { + * "data": [ + * { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "name": "John Doe", + * "role": { + * "id": 1, + * "name": "Backend Developer" + * }, + * "hourly_rate": "150.00", + * "active": true, + * "created_at": "2024-01-15T10:00:00.000000Z", + * "updated_at": "2024-01-15T10:00:00.000000Z" + * } + * ] + * } + */ + public function index(Request $request): JsonResponse + { + $active = $request->has('active') + ? filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN) + : null; + + $teamMembers = $this->teamMemberService->getAll($active); + + return $this->wrapResource(TeamMemberResource::collection($teamMembers)); + } + + /** + * Create a new team member + * + * Create a new team member with name, role, and hourly rate. + * + * @authenticated + * + * @bodyParam name string required Team member name. Example: John Doe + * @bodyParam role_id integer required Role ID. Example: 1 + * @bodyParam hourly_rate numeric required Hourly rate (must be > 0). Example: 150.00 + * @bodyParam active boolean Active status (defaults to true). Example: true + * + * @response 201 { + * "data": { * "id": "550e8400-e29b-41d4-a716-446655440000", * "name": "John Doe", - * "role_id": 1, * "role": { * "id": 1, * "name": "Backend Developer" @@ -51,42 +92,6 @@ class TeamMemberController extends Controller * "created_at": "2024-01-15T10:00:00.000000Z", * "updated_at": "2024-01-15T10:00:00.000000Z" * } - * ] - */ - public function index(Request $request): JsonResponse - { - $active = $request->has('active') - ? filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN) - : null; - - $teamMembers = $this->teamMemberService->getAll($active); - - return response()->json($teamMembers); - } - - /** - * Create a new team member - * - * Create a new team member with name, role, and hourly rate. - * - * @authenticated - * @bodyParam name string required Team member name. Example: John Doe - * @bodyParam role_id integer required Role ID. Example: 1 - * @bodyParam hourly_rate numeric required Hourly rate (must be > 0). Example: 150.00 - * @bodyParam active boolean Active status (defaults to true). Example: true - * - * @response 201 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "name": "John Doe", - * "role_id": 1, - * "role": { - * "id": 1, - * "name": "Backend Developer" - * }, - * "hourly_rate": "150.00", - * "active": true, - * "created_at": "2024-01-15T10:00:00.000000Z", - * "updated_at": "2024-01-15T10:00:00.000000Z" * } * @response 422 {"message":"Validation failed","errors":{"name":["The name field is required."],"hourly_rate":["Hourly rate must be greater than 0"]}} */ @@ -94,7 +99,8 @@ class TeamMemberController extends Controller { try { $teamMember = $this->teamMemberService->create($request->all()); - return response()->json($teamMember, 201); + + return $this->wrapResource(new TeamMemberResource($teamMember), 201); } catch (ValidationException $e) { return response()->json([ 'message' => 'Validation failed', @@ -109,20 +115,22 @@ class TeamMemberController extends Controller * Get details of a specific team member by ID. * * @authenticated + * * @urlParam id string required Team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "name": "John Doe", - * "role_id": 1, - * "role": { - * "id": 1, - * "name": "Backend Developer" - * }, - * "hourly_rate": "150.00", - * "active": true, - * "created_at": "2024-01-15T10:00:00.000000Z", - * "updated_at": "2024-01-15T10:00:00.000000Z" + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "name": "John Doe", + * "role": { + * "id": 1, + * "name": "Backend Developer" + * }, + * "hourly_rate": "150.00", + * "active": true, + * "created_at": "2024-01-15T10:00:00.000000Z", + * "updated_at": "2024-01-15T10:00:00.000000Z" + * } * } * @response 404 {"message":"Team member not found"} */ @@ -136,7 +144,7 @@ class TeamMemberController extends Controller ], 404); } - return response()->json($teamMember); + return $this->wrapResource(new TeamMemberResource($teamMember)); } /** @@ -145,24 +153,27 @@ class TeamMemberController extends Controller * Update details of an existing team member. * * @authenticated + * * @urlParam id string required Team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 + * * @bodyParam name string Team member name. Example: John Doe * @bodyParam role_id integer Role ID. Example: 1 * @bodyParam hourly_rate numeric Hourly rate (must be > 0). Example: 175.00 * @bodyParam active boolean Active status. Example: false * * @response 200 { - * "id": "550e8400-e29b-41d4-a716-446655440000", - * "name": "John Doe", - * "role_id": 1, - * "role": { - * "id": 1, - * "name": "Backend Developer" - * }, - * "hourly_rate": "175.00", - * "active": false, - * "created_at": "2024-01-15T10:00:00.000000Z", - * "updated_at": "2024-01-15T11:00:00.000000Z" + * "data": { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "name": "John Doe", + * "role": { + * "id": 1, + * "name": "Backend Developer" + * }, + * "hourly_rate": "175.00", + * "active": false, + * "created_at": "2024-01-15T10:00:00.000000Z", + * "updated_at": "2024-01-15T11:00:00.000000Z" + * } * } * @response 404 {"message":"Team member not found"} * @response 422 {"message":"Validation failed","errors":{"hourly_rate":["Hourly rate must be greater than 0"]}} @@ -179,10 +190,10 @@ class TeamMemberController extends Controller try { $teamMember = $this->teamMemberService->update($teamMember, $request->only([ - 'name', 'role_id', 'hourly_rate', 'active' + 'name', 'role_id', 'hourly_rate', 'active', ])); - return response()->json($teamMember); + return $this->wrapResource(new TeamMemberResource($teamMember)); } catch (ValidationException $e) { return response()->json([ 'message' => 'Validation failed', @@ -197,6 +208,7 @@ class TeamMemberController extends Controller * Delete a team member. Cannot delete if member has allocations or actuals. * * @authenticated + * * @urlParam id string required Team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 {"message":"Team member deleted successfully"} diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php index 8677cd5c..c6daec91 100644 --- a/backend/app/Http/Controllers/Controller.php +++ b/backend/app/Http/Controllers/Controller.php @@ -2,7 +2,16 @@ namespace App\Http\Controllers; -abstract class Controller +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Routing\Controller as BaseController; + +class Controller extends BaseController { - // + protected function wrapResource(JsonResource $resource, int $status = 200): JsonResponse + { + return response()->json([ + 'data' => $resource->resolve(request()), + ], $status); + } } diff --git a/backend/app/Http/Resources/BaseResource.php b/backend/app/Http/Resources/BaseResource.php new file mode 100644 index 00000000..95a8e41c --- /dev/null +++ b/backend/app/Http/Resources/BaseResource.php @@ -0,0 +1,18 @@ +toIso8601String(); + } + + protected function formatDecimal($value, int $decimals = 2): ?float + { + return $value !== null ? round((float) $value, $decimals) : null; + } +} diff --git a/backend/app/Http/Resources/CapacityResource.php b/backend/app/Http/Resources/CapacityResource.php new file mode 100644 index 00000000..05596c7d --- /dev/null +++ b/backend/app/Http/Resources/CapacityResource.php @@ -0,0 +1,18 @@ + $this->resource['team_member_id'] ?? null, + 'month' => $this->resource['month'] ?? null, + 'working_days' => $this->resource['working_days'] ?? null, + 'person_days' => $this->resource['person_days'] ?? null, + 'hours' => $this->resource['hours'] ?? null, + 'details' => $this->resource['details'] ?? [], + ]; + } +} diff --git a/backend/app/Http/Resources/HolidayResource.php b/backend/app/Http/Resources/HolidayResource.php new file mode 100644 index 00000000..76af992f --- /dev/null +++ b/backend/app/Http/Resources/HolidayResource.php @@ -0,0 +1,16 @@ + $this->id, + 'date' => $this->date?->toDateString(), + 'name' => $this->name, + 'description' => $this->description, + ]; + } +} diff --git a/backend/app/Http/Resources/ProjectResource.php b/backend/app/Http/Resources/ProjectResource.php new file mode 100644 index 00000000..7a1a6c75 --- /dev/null +++ b/backend/app/Http/Resources/ProjectResource.php @@ -0,0 +1,23 @@ + $this->id, + 'code' => $this->code, + 'title' => $this->title, + 'status' => $this->whenLoaded('status', fn () => new ProjectStatusResource($this->status)), + 'type' => $this->whenLoaded('type', fn () => new ProjectTypeResource($this->type)), + 'approved_estimate' => $this->formatDecimal($this->approved_estimate), + 'forecasted_effort' => $this->forecasted_effort, + 'start_date' => $this->formatDate($this->start_date), + 'end_date' => $this->formatDate($this->end_date), + 'created_at' => $this->formatDate($this->created_at), + 'updated_at' => $this->formatDate($this->updated_at), + ]; + } +} diff --git a/backend/app/Http/Resources/ProjectStatusResource.php b/backend/app/Http/Resources/ProjectStatusResource.php new file mode 100644 index 00000000..a83db87d --- /dev/null +++ b/backend/app/Http/Resources/ProjectStatusResource.php @@ -0,0 +1,17 @@ + $this->id, + 'name' => $this->name, + 'order' => $this->order, + 'is_active' => $this->is_active, + 'is_billable' => $this->is_billable, + ]; + } +} diff --git a/backend/app/Http/Resources/ProjectTypeResource.php b/backend/app/Http/Resources/ProjectTypeResource.php new file mode 100644 index 00000000..5c95a775 --- /dev/null +++ b/backend/app/Http/Resources/ProjectTypeResource.php @@ -0,0 +1,15 @@ + $this->id, + 'name' => $this->name, + 'description' => $this->description, + ]; + } +} diff --git a/backend/app/Http/Resources/PtoResource.php b/backend/app/Http/Resources/PtoResource.php new file mode 100644 index 00000000..3bfb59d8 --- /dev/null +++ b/backend/app/Http/Resources/PtoResource.php @@ -0,0 +1,20 @@ + $this->id, + 'team_member_id' => $this->team_member_id, + 'team_member' => $this->whenLoaded('teamMember', fn () => new TeamMemberResource($this->teamMember)), + 'start_date' => $this->start_date?->toDateString(), + 'end_date' => $this->end_date?->toDateString(), + 'reason' => $this->reason, + 'status' => $this->status, + 'created_at' => $this->formatDate($this->created_at), + ]; + } +} diff --git a/backend/app/Http/Resources/RevenueResource.php b/backend/app/Http/Resources/RevenueResource.php new file mode 100644 index 00000000..bec041cc --- /dev/null +++ b/backend/app/Http/Resources/RevenueResource.php @@ -0,0 +1,15 @@ + $this->resource['month'] ?? null, + 'possible_revenue' => $this->resource['possible_revenue'] ?? null, + 'member_revenues' => $this->resource['member_revenues'] ?? [], + ]; + } +} diff --git a/backend/app/Http/Resources/RoleResource.php b/backend/app/Http/Resources/RoleResource.php new file mode 100644 index 00000000..63490d2e --- /dev/null +++ b/backend/app/Http/Resources/RoleResource.php @@ -0,0 +1,18 @@ + $this->id, + 'name' => $this->name, + 'description' => $this->description, + ]; + } +} diff --git a/backend/app/Http/Resources/TeamCapacityResource.php b/backend/app/Http/Resources/TeamCapacityResource.php new file mode 100644 index 00000000..265930bf --- /dev/null +++ b/backend/app/Http/Resources/TeamCapacityResource.php @@ -0,0 +1,16 @@ + $this->resource['month'] ?? null, + 'total_person_days' => $this->resource['person_days'] ?? null, + 'total_hours' => $this->resource['hours'] ?? null, + 'members' => $this->resource['members'] ?? [], + ]; + } +} diff --git a/backend/app/Http/Resources/TeamMemberResource.php b/backend/app/Http/Resources/TeamMemberResource.php new file mode 100644 index 00000000..f03a869d --- /dev/null +++ b/backend/app/Http/Resources/TeamMemberResource.php @@ -0,0 +1,22 @@ + $this->id, + 'name' => $this->name, + 'role' => $this->whenLoaded('role', fn () => new RoleResource($this->role)), + 'hourly_rate' => $this->formatDecimal($this->hourly_rate), + 'active' => $this->active, + 'created_at' => $this->formatDate($this->created_at), + 'updated_at' => $this->formatDate($this->updated_at), + ]; + } +} diff --git a/backend/app/Http/Resources/UserResource.php b/backend/app/Http/Resources/UserResource.php new file mode 100644 index 00000000..cbec8101 --- /dev/null +++ b/backend/app/Http/Resources/UserResource.php @@ -0,0 +1,22 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'role' => $this->role, + 'active' => $this->active, + 'created_at' => $this->formatDate($this->created_at), + 'updated_at' => $this->formatDate($this->updated_at), + ]; + } +} diff --git a/backend/resources/views/scribe/index.blade.php b/backend/resources/views/scribe/index.blade.php index 1cf1b2c5..df547677 100644 --- a/backend/resources/views/scribe/index.blade.php +++ b/backend/resources/views/scribe/index.blade.php @@ -260,16 +260,19 @@ fetch(url, {
 
 {
-    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
-    "refresh_token": "abc123def456",
-    "token_type": "bearer",
-    "expires_in": 3600,
-    "user": {
+    "data": {
         "id": "550e8400-e29b-41d4-a716-446655440000",
         "name": "Alice Johnson",
         "email": "user@example.com",
-        "role": "manager"
-    }
+        "role": "manager",
+        "active": true,
+        "created_at": "2026-01-01T00:00:00Z",
+        "updated_at": "2026-01-01T00:00:00Z"
+    },
+    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+    "refresh_token": "abc123def456",
+    "token_type": "bearer",
+    "expires_in": 3600
 }
  
@@ -457,6 +460,15 @@ fetch(url, {
 
 {
+    "data": {
+        "id": "550e8400-e29b-41d4-a716-446655440000",
+        "name": "Alice Johnson",
+        "email": "user@example.com",
+        "role": "manager",
+        "active": true,
+        "created_at": "2026-01-01T00:00:00Z",
+        "updated_at": "2026-01-01T00:00:00Z"
+    },
     "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
     "refresh_token": "newtoken123",
     "token_type": "bearer",
@@ -758,15 +770,20 @@ fetch(url, {
                 
 
 {
-    "person_days": 18.5,
-    "hours": 148,
-    "details": [
-        {
-            "date": "2026-02-02",
-            "availability": 1,
-            "is_pto": false
-        }
-    ]
+    "data": {
+        "team_member_id": "550e8400-e29b-41d4-a716-446655440000",
+        "month": "2026-02",
+        "working_days": 20,
+        "person_days": 18.5,
+        "hours": 148,
+        "details": [
+            {
+                "date": "2026-02-02",
+                "availability": 1,
+                "is_pto": false
+            }
+        ]
+    }
 }
  
@@ -944,17 +961,19 @@ fetch(url, {
 
 {
-    "month": "2026-02",
-    "person_days": 180.5,
-    "hours": 1444,
-    "members": [
-        {
-            "id": "550e8400-e29b-41d4-a716-446655440000",
-            "name": "Ada Lovelace",
-            "person_days": 18.5,
-            "hours": 148
-        }
-    ]
+    "data": {
+        "month": "2026-02",
+        "total_person_days": 180.5,
+        "total_hours": 1444,
+        "members": [
+            {
+                "id": "550e8400-e29b-41d4-a716-446655440000",
+                "name": "Ada Lovelace",
+                "person_days": 18.5,
+                "hours": 148
+            }
+        ]
+    }
 }
  
@@ -1108,8 +1127,19 @@ fetch(url, {
 
 {
-    "month": "2026-02",
-    "possible_revenue": 21500.25
+    "data": {
+        "month": "2026-02",
+        "possible_revenue": 21500.25,
+        "member_revenues": [
+            {
+                "team_member_id": "550e8400-e29b-41d4-a716-446655440000",
+                "team_member_name": "Ada Lovelace",
+                "hours": 148,
+                "hourly_rate": 150,
+                "revenue": 22200
+            }
+        ]
+    }
 }
  
@@ -1262,14 +1292,16 @@ fetch(url, {
 
-[
-    {
-        "id": "550e8400-e29b-41d4-a716-446655440000",
-        "date": "2026-02-14",
-        "name": "Company Holiday",
-        "description": "Office closed"
-    }
-]
+{
+    "data": [
+        {
+            "id": "550e8400-e29b-41d4-a716-446655440000",
+            "date": "2026-02-14",
+            "name": "Company Holiday",
+            "description": "Office closed"
+        }
+    ]
+}
  
@@ -1727,16 +1761,18 @@ fetch(url, {
 
-[
-    {
-        "id": "550e8400-e29b-41d4-a716-446655440001",
-        "team_member_id": "550e8400-e29b-41d4-a716-446655440000",
-        "start_date": "2026-02-10",
-        "end_date": "2026-02-12",
-        "status": "pending",
-        "reason": "Family travel"
-    }
-]
+{
+    "data": [
+        {
+            "id": "550e8400-e29b-41d4-a716-446655440001",
+            "team_member_id": "550e8400-e29b-41d4-a716-446655440000",
+            "start_date": "2026-02-10",
+            "end_date": "2026-02-12",
+            "status": "pending",
+            "reason": "Family travel"
+        }
+    ]
+}
  
@@ -2092,8 +2130,10 @@ fetch(url, {
 
 {
-    "id": "550e8400-e29b-41d4-a716-446655440001",
-    "status": "approved"
+    "data": {
+        "id": "550e8400-e29b-41d4-a716-446655440001",
+        "status": "approved"
+    }
 }
  
@@ -2229,20 +2269,22 @@ fetch(url, {
 
-[
-    {
-        "id": 1,
-        "name": "Project"
-    },
-    {
-        "id": 2,
-        "name": "Support"
-    },
-    {
-        "id": 3,
-        "name": "Engagement"
-    }
-]
+{
+    "data": [
+        {
+            "id": 1,
+            "name": "Project"
+        },
+        {
+            "id": 2,
+            "name": "Support"
+        },
+        {
+            "id": 3,
+            "name": "Engagement"
+        }
+    ]
+}