find($teamMemberId); if (! $teamMember) { return ['valid' => true, 'warning' => null, 'utilization' => 0]; } // Get capacity for the month $capacityService = app(CapacityService::class); $capacityData = $capacityService->calculateIndividualCapacity($teamMemberId, $month); $capacity = $capacityData['hours'] ?? 0; if ($capacity <= 0) { return ['valid' => true, 'warning' => null, 'utilization' => 0]; } // Convert YYYY-MM to YYYY-MM-01 for database query $monthDate = $month . '-01'; // Get existing allocations for this team member in this month $existingHours = Allocation::where('team_member_id', $teamMemberId) ->where('month', $monthDate) ->when($excludeAllocationId, fn ($query) => $query->where('id', '!=', $excludeAllocationId)) ->sum('allocated_hours'); $totalHours = $existingHours + $newHours; $utilization = ($totalHours / $capacity) * 100; // Over-allocated: warn but allow if ($utilization > 100) { $overBy = $totalHours - $capacity; return [ 'valid' => true, 'warning' => "Team member over-allocated by {$overBy} hours ({$utilization}% utilization)", 'utilization' => round($utilization, 1), ]; } return [ 'valid' => true, 'warning' => null, 'utilization' => round($utilization, 1), ]; } /** * Validate an allocation against project approved estimate. * * @return array{valid: bool, indicator: string, message: ?string} */ public function validateApprovedEstimate( string $projectId, string $month, float $newHours, ?string $excludeAllocationId = null ): array { $project = Project::find($projectId); if (! $project || ! $project->approved_estimate) { return ['valid' => true, 'indicator' => 'gray', 'message' => null]; } // Convert YYYY-MM to YYYY-MM-01 for database query $monthDate = $month . '-01'; // Get existing allocations for this project in this month $existingHours = Allocation::where('project_id', $projectId) ->where('month', $monthDate) ->when($excludeAllocationId, fn ($query) => $query->where('id', '!=', $excludeAllocationId)) ->sum('allocated_hours'); $totalHours = $existingHours + $newHours; $approved = (float) $project->approved_estimate; if ($approved <= 0) { return ['valid' => true, 'indicator' => 'gray', 'message' => null]; } $percentage = ($totalHours / $approved) * 100; // Over-allocated: RED indicator if ($percentage > 100) { $overBy = $totalHours - $approved; return [ 'valid' => true, 'indicator' => 'red', 'message' => "{$percentage}% allocated (over by {$overBy} hours). Project will be over-charged.", ]; } // Exactly at estimate: GREEN indicator if ($percentage >= 100) { return [ 'valid' => true, 'indicator' => 'green', 'message' => '100% allocated', ]; } // Under-allocated: YELLOW indicator $underBy = $approved - $totalHours; return [ 'valid' => true, 'indicator' => 'yellow', 'message' => "{$percentage}% allocated (under by {$underBy} hours)", ]; } /** * Get validation results for all allocations in a month. */ public function getAllocationValidation( string $teamMemberId, string $month ): array { $capacityValidation = $this->validateCapacity($teamMemberId, $month, 0); // Convert YYYY-MM to YYYY-MM-01 for database query $monthDate = $month . '-01'; $allocations = Allocation::where('team_member_id', $teamMemberId) ->where('month', $monthDate) ->with('project') ->get(); $projectValidations = $allocations->map(function ($allocation) use ($month) { return $this->validateApprovedEstimate( $allocation->project_id, $month, (float) $allocation->allocated_hours, $allocation->id ); }); return [ 'capacity' => $capacityValidation, 'projects' => $projectValidations, ]; } }