## Architecture Overview The actuals tracking feature follows the existing patterns in the Headroom application: - **Backend**: Laravel API with controller, service, model, and resource classes - **Frontend**: SvelteKit with TypeScript, using service layer for API calls - **Database**: MySQL with UUID primary keys and foreign key constraints ## Data Model ### Database Schema ``` actuals ├── id (UUID, PK) ├── project_id (UUID, FK → projects.id) ├── team_member_id (UUID, FK → team_members.id, nullable for untracked) ├── month (DATE, stored as first day of month) ├── hours_logged (DECIMAL 8,2) ├── notes (TEXT, nullable) ├── created_at (TIMESTAMP) └── updated_at (TIMESTAMP) Indexes: - idx_actuals_project_month (project_id, month) - idx_actuals_member_month (team_member_id, month) ``` ### Unique Constraint The combination of (project_id, team_member_id, month) is the natural key but no explicit unique constraint is enforced. The application handles upserts by checking for existing records. ## API Design ### Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | GET | /api/actuals | List actuals grid for month (paginated) | | POST | /api/actuals | Create/append actual hours | | GET | /api/actuals/{id} | Get single actual with variance | | PUT | /api/actuals/{id} | Update actual hours (replace) | | DELETE | /api/actuals/{id} | Delete actual | ### Index Endpoint Parameters ``` month: required, format Y-m project_ids[]: optional, array of UUIDs team_member_ids[]: optional, array of UUIDs include_inactive: optional, boolean search: optional, string page: optional, integer per_page: optional, integer (1-250, default 25) ``` ### Response Format ```json { "data": [ { "id": "uuid", "project_id": "uuid", "project": { "id", "code", "title", "status", "is_active" }, "team_member_id": "uuid", "team_member": { "id", "name", "is_active" }, "month": "2024-03", "allocated_hours": "80.00", "actual_hours": "75.50", "variance_percentage": -5.6, "variance_display": null, "variance_indicator": "green", "notes": null, "is_readonly": false } ], "meta": { "current_page": 1, "per_page": 25, "total": 100, "last_page": 4 } } ``` ## Frontend Architecture ### Components - **Page**: `src/routes/actuals/+page.svelte` - Main grid page - **Services**: `src/lib/services/actualsService.ts` - API client - **Types**: `src/lib/types/actuals.ts` - TypeScript interfaces ### State Management Uses Svelte 5 runes ($state, $derived) for reactive state: - Filter state: currentPeriod, selectedProjectIds, selectedMemberIds, includeInactive, searchQuery - Pagination state: currentPage, totalPages, totalItems, perPage - Modal state: showModal, selectedCell, formHours, formNotes ### URL Synchronization Filter state is synchronized to URL query parameters for shareability and browser history. ## Business Rules ### Validation Rules 1. **Future months**: Cannot log hours for months after the current month 2. **Completed projects**: Cannot log hours to projects with status Done, Cancelled, or Closed - Configurable via `ALLOW_ACTUALS_ON_INACTIVE_PROJECTS` env variable 3. **Hours must be positive**: Minimum 0, stored as decimal ### Variance Calculation ``` if allocated <= 0: if actual > 0: variance = infinity (∞%) else: variance = 0% else: variance = ((actual - allocated) / allocated) * 100 ``` ### Indicator Thresholds | Variance | Indicator | Color | |----------|-----------|-------| | ±5% | green | Match | | ±20% | yellow | Warning | | >20% | red | Alert | | no data | gray | N/A | ## Security Considerations - JWT authentication required for all endpoints - Authorization handled at middleware level (existing patterns) - Input validation via Laravel Validator - SQL injection prevented via Eloquent ORM ## Performance Considerations - Cartesian grid built in-memory after fetching filtered projects and members - Pagination applied to final grid rows (not database level) - Indexes on (project_id, month) and (team_member_id, month) for efficient lookups - Eager loading of relationships via Eloquent `with()` ## Configuration ```php // config/actuals.php return [ 'allow_actuals_on_inactive_projects' => env('ALLOW_ACTUALS_ON_INACTIVE_PROJECTS', false), ]; ```