address each point.
**Changes Summary**
This specification updates the `headroom-foundation` change set to
include actuals tracking. The new feature adds a `TeamMember` model for
team members and a `ProjectStatus` model for project statuses.
**Summary of Changes**
1. **Add Team Members**
* Created the `TeamMember` model with attributes: `id`, `name`,
`role`, and `active`.
* Implemented data migration to add all existing users as
`team_member_ids` in the database.
2. **Add Project Statuses**
* Created the `ProjectStatus` model with attributes: `id`, `name`,
`order`, and `is_active`.
* Defined initial project statuses as "Initial" and updated
workflow states accordingly.
3. **Actuals Tracking**
* Introduced a new `Actual` model for tracking actual hours worked
by team members.
* Implemented data migration to add all existing allocations as
`actual_hours` in the database.
* Added methods for updating and deleting actual records.
**Open Issues**
1. **Authorization Policy**: The system does not have an authorization
policy yet, which may lead to unauthorized access or data
modifications.
2. **Project Type Distinguish**: Although project types are
differentiated, there is no distinction between "Billable" and
"Support" in the database.
3. **Cost Reporting**: Revenue forecasts do not include support
projects, and their reporting treatment needs clarification.
**Implementation Roadmap**
1. **Authorization Policy**: Implement an authorization policy to
restrict access to authorized users only.
2. **Distinguish Project Types**: Clarify project type distinction
between "Billable" and "Support".
3. **Cost Reporting**: Enhance revenue forecasting to include support
projects with different reporting treatment.
**Task Assignments**
1. **Authorization Policy**
* Task Owner: John (Automated)
* Description: Implement an authorization policy using Laravel's
built-in middleware.
* Deadline: 2026-03-25
2. **Distinguish Project Types**
* Task Owner: Maria (Automated)
* Description: Update the `ProjectType` model to include a
distinction between "Billable" and "Support".
* Deadline: 2026-04-01
3. **Cost Reporting**
* Task Owner: Alex (Automated)
* Description: Enhance revenue forecasting to include support
projects with different reporting treatment.
* Deadline: 2026-04-15
4.3 KiB
4.3 KiB
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
{
"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
- Future months: Cannot log hours for months after the current month
- Completed projects: Cannot log hours to projects with status Done, Cancelled, or Closed
- Configurable via
ALLOW_ACTUALS_ON_INACTIVE_PROJECTSenv variable
- Configurable via
- 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
// config/actuals.php
return [
'allow_actuals_on_inactive_projects' => env('ALLOW_ACTUALS_ON_INACTIVE_PROJECTS', false),
];