validationService = $validationService; } /** * List allocations / Get allocation matrix * * Get all allocations, optionally filtered by month. * * @authenticated * * @queryParam month string Filter by month (YYYY-MM format). Example: 2026-02 * * @response 200 { * "data": [ * { * "id": "550e8400-e29b-41d4-a716-446655440000", * "project_id": "550e8400-e29b-41d4-a716-446655440001", * "team_member_id": "550e8400-e29b-41d4-a716-446655440002", * "month": "2026-02", * "allocated_hours": 40.00 * } * ] * } */ public function index(Request $request): JsonResponse { $month = $request->query('month'); $query = Allocation::with(['project', 'teamMember']); if ($month) { // Convert YYYY-MM to YYYY-MM-01 for date comparison $monthDate = $month . '-01'; $query->where('month', $monthDate); } $allocations = $query->get(); // Compute allocation_indicator for each allocation based on project totals $allocations->each(function ($allocation) { $allocation->allocation_indicator = $this->computeAllocationIndicator( $allocation->project_id, $allocation->month ); }); return $this->wrapResource(AllocationResource::collection($allocations)); } /** * Compute allocation indicator based on project totals. */ private function computeAllocationIndicator(string $projectId, string $month): string { // Convert month to date format if needed $monthDate = strlen($month) === 7 ? $month . '-01' : $month; // Get total allocated for this project in this month $totalAllocated = Allocation::where('project_id', $projectId) ->where('month', $monthDate) ->sum('allocated_hours'); // Get project approved estimate $project = \App\Models\Project::find($projectId); $approvedEstimate = $project?->approved_estimate; // Handle no estimate if (! $approvedEstimate || $approvedEstimate <= 0) { return 'gray'; } $percentage = ($totalAllocated / $approvedEstimate) * 100; // Check in correct order: over first, then at capacity, then under if ($percentage > 100) { return 'red'; } elseif ($percentage >= 100) { return 'green'; } else { return 'yellow'; } } /** * Create a new allocation * * Allocate hours for a team member to a project for a specific month. * * @authenticated * * @bodyParam project_id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440001 * @bodyParam team_member_id string required Team member UUID. Example: 550e8400-e29b-41d4-a716-446655440002 * @bodyParam month string required Month (YYYY-MM format). Example: 2026-02 * @bodyParam allocated_hours numeric required Hours to allocate (must be >= 0). Example: 40 * * @response 201 { * "data": { * "id": "550e8400-e29b-41d4-a716-446655440000", * "project_id": "550e8400-e29b-41d4-a716-446655440001", * "team_member_id": "550e8400-e29b-41d4-a716-446655440002", * "month": "2026-02", * "allocated_hours": 40.00 * } * } * @response 422 {"message":"Validation failed","errors":{"allocated_hours":["Allocated hours must be greater than or equal to 0"]}} */ public function store(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'project_id' => 'required|uuid|exists:projects,id', 'team_member_id' => 'nullable|uuid|exists:team_members,id', 'month' => 'required|date_format:Y-m', 'allocated_hours' => 'required|numeric|min:0', ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed', 'errors' => $validator->errors(), ], 422); } // Validate against capacity and approved estimate (skip for untracked) $teamMemberId = $request->input('team_member_id'); $capacityValidation = ['valid' => true, 'warning' => null, 'utilization' => 0]; if ($teamMemberId) { $capacityValidation = $this->validationService->validateCapacity( $teamMemberId, $request->input('month'), (float) $request->input('allocated_hours') ); } $estimateValidation = $this->validationService->validateApprovedEstimate( $request->input('project_id'), $request->input('month'), (float) $request->input('allocated_hours') ); // Convert YYYY-MM to YYYY-MM-01 for database storage $data = $request->all(); $data['month'] = $data['month'] . '-01'; $allocation = Allocation::create($data); $allocation->load(['project', 'teamMember']); $response = new AllocationResource($allocation); $data = $response->toArray($request); // Add validation warnings/info to response $data['warnings'] = []; if ($capacityValidation['warning']) { $data['warnings'][] = $capacityValidation['warning']; } if ($estimateValidation['message']) { $data['warnings'][] = $estimateValidation['message']; } $data['utilization'] = $capacityValidation['utilization']; $data['allocation_indicator'] = $estimateValidation['indicator']; return response()->json(['data' => $data], 201); } /** * Get a single allocation * * Get details of a specific allocation by ID. * * @authenticated * * @urlParam id string required Allocation UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 { * "data": { * "id": "550e8400-e29b-41d4-a716-446655440000", * "project_id": "550e8400-e29b-41d4-a716-446655440001", * "team_member_id": "550e8400-e29b-41d4-a716-446655440002", * "month": "2026-02", * "allocated_hours": 40.00 * } * } * @response 404 {"message": "Allocation not found"} */ public function show(string $id): JsonResponse { $allocation = Allocation::with(['project', 'teamMember'])->find($id); if (! $allocation) { return response()->json([ 'message' => 'Allocation not found', ], 404); } return $this->wrapResource(new AllocationResource($allocation)); } /** * Update an allocation * * Update an existing allocation's hours. * * @authenticated * * @urlParam id string required Allocation UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @bodyParam allocated_hours numeric required Hours to allocate (must be >= 0). Example: 60 * * @response 200 { * "data": { * "id": "550e8400-e29b-41d4-a716-446655440000", * "project_id": "550e8400-e29b-41d4-a716-446655440001", * "team_member_id": "550e8400-e29b-41d4-a716-446655440002", * "month": "2026-02", * "allocated_hours": 60.00 * } * } * @response 404 {"message": "Allocation not found"} * @response 422 {"message":"Validation failed","errors":{"allocated_hours":["Allocated hours must be greater than or equal to 0"]}} */ public function update(Request $request, string $id): JsonResponse { $allocation = Allocation::find($id); if (! $allocation) { return response()->json([ 'message' => 'Allocation not found', ], 404); } $validator = Validator::make($request->all(), [ 'allocated_hours' => 'required|numeric|min:0', ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed', 'errors' => $validator->errors(), ], 422); } $allocation->update($request->all()); $allocation->load(['project', 'teamMember']); return $this->wrapResource(new AllocationResource($allocation)); } /** * Delete an allocation * * Remove an allocation. * * @authenticated * * @urlParam id string required Allocation UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 {"message": "Allocation deleted successfully"} * @response 404 {"message": "Allocation not found"} */ public function destroy(string $id): JsonResponse { $allocation = Allocation::find($id); if (! $allocation) { return response()->json([ 'message' => 'Allocation not found', ], 404); } $allocation->delete(); return response()->json([ 'message' => 'Allocation deleted successfully', ]); } /** * Bulk create allocations * * Create or update multiple allocations in a single request. * * @authenticated * * @bodyParam allocations array required Array of allocations. Example: [{"project_id": "...", "team_member_id": "...", "month": "2026-02", "allocated_hours": 40}] * * @response 201 { * "data": [ * { * "id": "550e8400-e29b-41d4-a716-446655440000", * "project_id": "550e8400-e29b-41d4-a716-446655440001", * "team_member_id": "550e8400-e29b-41d4-a716-446655440002", * "month": "2026-02", * "allocated_hours": 40.00 * } * ] * } */ public function bulkStore(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'allocations' => 'required|array|min:1', 'allocations.*.project_id' => 'required|uuid|exists:projects,id', 'allocations.*.team_member_id' => 'required|uuid|exists:team_members,id', 'allocations.*.month' => 'required|date_format:Y-m', 'allocations.*.allocated_hours' => 'required|numeric|min:0', ]); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed', 'errors' => $validator->errors(), ], 422); } $created = []; foreach ($request->input('allocations') as $allocationData) { $allocation = Allocation::create($allocationData); $created[] = $allocation; } return $this->wrapResource(AllocationResource::collection($created), 201); } }