Files
headroom/backend/app/Http/Controllers/Api/UtilizationController.php
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

265 lines
7.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}