validate([ 'team_member_id' => 'required|exists:team_members,id', 'month' => 'nullable|date_format:Y-m', ]); $query = Pto::with('teamMember')->where('team_member_id', $data['team_member_id']); if (! empty($data['month'])) { $start = Carbon::createFromFormat('Y-m', $data['month'])->startOfMonth(); $end = $start->copy()->endOfMonth(); $query->where(function ($statement) use ($start, $end): void { $statement->whereBetween('start_date', [$start, $end]) ->orWhereBetween('end_date', [$start, $end]) ->orWhere(function ($nested) use ($start, $end): void { $nested->where('start_date', '<=', $start) ->where('end_date', '>=', $end); }); }); } $ptos = $query->orderBy('start_date')->get(); return $this->wrapResource(PtoResource::collection($ptos)); } /** * Request PTO * * Create a PTO request for a team member and approve it immediately. * * @group Capacity Planning * * @bodyParam team_member_id string required The team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * @bodyParam start_date string required The first day of the PTO. Example: 2026-02-10 * @bodyParam end_date string required The final day of the PTO. Example: 2026-02-12 * @bodyParam reason string nullable Optional reason for the PTO. * * @response 201 { * "data": { * "id": "550e8400-e29b-41d4-a716-446655440001", * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", * "start_date": "2026-02-10", * "end_date": "2026-02-12", * "status": "approved", * "reason": "Family travel" * } * } */ public function store(Request $request): JsonResponse { $data = $request->validate([ 'team_member_id' => 'required|exists:team_members,id', 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', 'reason' => 'nullable|string', ]); try { $pto = Pto::create(array_merge($data, ['status' => 'approved'])); $months = $this->monthsBetween($pto->start_date, $pto->end_date); $this->capacityService->forgetCapacityCacheForTeamMember($pto->team_member_id, $months); foreach ($months as $month) { $this->capacityService->forgetCapacityCacheForMonth($month); } $pto->load('teamMember'); return $this->wrapResource(new PtoResource($pto), 201); } catch (UniqueConstraintViolationException $e) { return response()->json([ 'message' => 'A PTO request with these details already exists.', 'errors' => [ 'general' => ['A PTO request with these details already exists.'], ], ], 422); } } /** * Approve PTO * * Approve a pending PTO request and refresh the affected capacity caches. * * @group Capacity Planning * * @urlParam id string required The PTO UUID that needs approval. Example: 550e8400-e29b-41d4-a716-446655440001 * * @response { * "data": { * "id": "550e8400-e29b-41d4-a716-446655440001", * "status": "approved" * } * } */ public function approve(string $id): JsonResponse { $pto = Pto::with('teamMember')->findOrFail($id); if ($pto->status !== 'approved') { $pto->status = 'approved'; $pto->save(); $months = $this->monthsBetween($pto->start_date, $pto->end_date); $this->capacityService->forgetCapacityCacheForTeamMember($pto->team_member_id, $months); foreach ($months as $month) { $this->capacityService->forgetCapacityCacheForMonth($month); } } $pto->load('teamMember'); return $this->wrapResource(new PtoResource($pto)); } public function destroy(string $id): JsonResponse { $pto = Pto::find($id); if (! $pto) { return response()->json(['message' => 'PTO not found'], 404); } $months = $this->monthsBetween($pto->start_date, $pto->end_date); $teamMemberId = $pto->team_member_id; $pto->delete(); $this->capacityService->forgetCapacityCacheForTeamMember($teamMemberId, $months); foreach ($months as $month) { $this->capacityService->forgetCapacityCacheForMonth($month); } return response()->json(['message' => 'PTO deleted']); } private function monthsBetween(Carbon|string $start, Carbon|string $end): array { $startMonth = Carbon::create($start)->copy()->startOfMonth(); $endMonth = Carbon::create($end)->copy()->startOfMonth(); $months = []; while ($startMonth <= $endMonth) { $months[] = $startMonth->format('Y-m'); $startMonth->addMonth(); } return $months; } }