Files
headroom/openspec/changes/archive/2026-03-22-implement-actuals-tracking/design.md
Santhosh Janardhanan f87ccccc4d Based on the provided specification, I will summarize the changes and
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
2026-04-20 16:38:41 -04:00

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

  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

// config/actuals.php
return [
    'allow_actuals_on_inactive_projects' => env('ALLOW_ACTUALS_ON_INACTIVE_PROJECTS', false),
];