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
This commit is contained in:
2026-04-20 16:38:41 -04:00
parent 90c15c70b7
commit f87ccccc4d
261 changed files with 54496 additions and 126 deletions

View File

@@ -0,0 +1,264 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\UtilizationService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* @group Utilization
*
* API endpoints for utilization calculations.
*
* Utilization measures how effectively team member capacity is being used.
* It's calculated as (Allocated Hours / Capacity) × 100%.
*
* **Color Indicators:**
* - `gray`: Under-utilized (<70%)
* - `blue`: Low utilization (70-80%)
* - `green`: Optimal (80-100%)
* - `yellow`: High utilization (100-110%)
* - `red`: Over-allocated (>110%)
*/
class UtilizationController extends Controller
{
use AuthorizesRequests;
public function __construct(
private UtilizationService $utilizationService
) {}
/**
* Get Running (YTD) Utilization
*
* Calculate year-to-date utilization for a specific team member.
* Running utilization = (Allocated hours Jan-current) / (Capacity Jan-current) × 100%
*
* @authenticated
*
* @queryParam team_member_id string required UUID of the team member. Example: 550e8400-e29b-41d4-a716-446655440000
* @queryParam month string required Month in Y-m format (calculates from Jan of this year to this month). Example: 2026-03
*
* @response 200 {
* "capacity_ytd": 480.0,
* "allocated_ytd": 450.0,
* "utilization": 93.8,
* "indicator": "green",
* "months_included": 3
* }
* @response 422 {"message":"Validation failed","errors":{"team_member_id":["The selected team member id is invalid."]}}
*/
public function running(Request $request): JsonResponse
{
$this->authorize('viewRunningUtilization', \App\Models\TeamMember::class);
$request->validate([
'team_member_id' => 'required|uuid|exists:team_members,id',
'month' => 'required|date_format:Y-m',
]);
$result = $this->utilizationService->calculateRunningUtilization(
$request->team_member_id,
$request->month
);
return response()->json($result);
}
/**
* Get Overall (Monthly) Utilization
*
* Calculate utilization for a specific team member in a specific month.
* Overall utilization = (Allocated hours this month) / (Capacity this month) × 100%
*
* @authenticated
*
* @queryParam team_member_id string required UUID of the team member. Example: 550e8400-e29b-41d4-a716-446655440000
* @queryParam month string required Month in Y-m format. Example: 2026-02
*
* @response 200 {
* "capacity": 160.0,
* "allocated": 140.0,
* "utilization": 87.5,
* "indicator": "green"
* }
* @response 422 {"message":"Validation failed","errors":{"month":["The month format is invalid."]}}
*/
public function overall(Request $request): JsonResponse
{
$this->authorize('viewOverallUtilization', \App\Models\TeamMember::class);
$request->validate([
'team_member_id' => 'required|uuid|exists:team_members,id',
'month' => 'required|date_format:Y-m',
]);
$result = $this->utilizationService->calculateOverallUtilization(
$request->team_member_id,
$request->month
);
return response()->json($result);
}
/**
* Get Combined Utilization Data
*
* Get both overall (monthly) and running (YTD) utilization for a team member.
*
* @authenticated
*
* @queryParam team_member_id string required UUID of the team member. Example: 550e8400-e29b-41d4-a716-446655440000
* @queryParam month string required Month in Y-m format. Example: 2026-03
*
* @response 200 {
* "overall": {
* "capacity": 160.0,
* "allocated": 140.0,
* "utilization": 87.5,
* "indicator": "green"
* },
* "running": {
* "capacity_ytd": 480.0,
* "allocated_ytd": 450.0,
* "utilization": 93.8,
* "indicator": "green",
* "months_included": 3
* }
* }
*/
public function data(Request $request): JsonResponse
{
$this->authorize('viewUtilization', \App\Models\TeamMember::class);
$request->validate([
'team_member_id' => 'required|uuid|exists:team_members,id',
'month' => 'required|date_format:Y-m',
]);
$result = $this->utilizationService->getUtilizationData(
$request->team_member_id,
$request->month
);
return response()->json($result);
}
/**
* Get Team Utilization
*
* Calculate average utilization across all active team members for a specific month.
*
* @authenticated
*
* @queryParam month string required Month in Y-m format. Example: 2026-02
*
* @response 200 {
* "average_utilization": 85.4,
* "average_indicator": "green",
* "member_count": 3,
* "by_member": {
* "550e8400-e29b-41d4-a716-446655440000": {
* "capacity": 160.0,
* "allocated": 140.0,
* "utilization": 87.5,
* "indicator": "green"
* }
* }
* }
*/
public function team(Request $request): JsonResponse
{
$this->authorize('viewTeamUtilization', \App\Models\TeamMember::class);
$request->validate([
'month' => 'required|date_format:Y-m',
]);
$result = $this->utilizationService->calculateTeamUtilization(
$request->month
);
return response()->json($result);
}
/**
* Get Team Running Utilization (YTD)
*
* Calculate average year-to-date utilization across all active team members.
*
* @authenticated
*
* @queryParam month string required Month in Y-m format. Example: 2026-03
*
* @response 200 {
* "average_utilization": 88.2,
* "average_indicator": "green",
* "member_count": 3,
* "by_member": {}
* }
*/
public function teamRunning(Request $request): JsonResponse
{
$this->authorize('viewTeamRunningUtilization', \App\Models\TeamMember::class);
$request->validate([
'month' => 'required|date_format:Y-m',
]);
$result = $this->utilizationService->calculateTeamRunningUtilization(
$request->month
);
return response()->json($result);
}
/**
* Get Utilization Trend
*
* Get utilization data for a team member over a range of months.
*
* @authenticated
*
* @queryParam team_member_id string required UUID of the team member. Example: 550e8400-e29b-41d4-a716-446655440000
* @queryParam start_month string required Start month in Y-m format. Example: 2026-01
* @queryParam end_month string required End month in Y-m format (must be >= start_month). Example: 2026-06
*
* @response 200 [
* {
* "month": "2026-01",
* "utilization": 75.0,
* "indicator": "blue",
* "capacity": 176.0,
* "allocated": 132.0
* },
* {
* "month": "2026-02",
* "utilization": 87.5,
* "indicator": "green",
* "capacity": 160.0,
* "allocated": 140.0
* }
* ]
*/
public function trend(Request $request): JsonResponse
{
$this->authorize('viewUtilizationTrend', \App\Models\TeamMember::class);
$request->validate([
'team_member_id' => 'required|uuid|exists:team_members,id',
'start_month' => 'required|date_format:Y-m',
'end_month' => 'required|date_format:Y-m|after_or_equal:start_month',
]);
$result = $this->utilizationService->getUtilizationTrend(
$request->team_member_id,
$request->start_month,
$request->end_month
);
return response()->json($result);
}
}