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
157 lines
4.3 KiB
Markdown
157 lines
4.3 KiB
Markdown
## 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),
|
|
];
|
|
```
|