## 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),
];
```
