feat(capacity): Implement Capacity Planning capability (4.1-4.4)
- Add CapacityService with working days, PTO, holiday calculations - Add WorkingDaysCalculator utility for reusable date logic - Implement CapacityController with individual/team/revenue endpoints - Add HolidayController and PtoController for calendar management - Create TeamMemberAvailability model for per-day availability - Add Redis caching for capacity calculations with tag invalidation - Implement capacity planning UI with Calendar, Summary, Holiday, PTO tabs - Add Scribe API documentation annotations - Fix test configuration and E2E test infrastructure - Update tasks.md with completion status Backend Tests: 63 passed Frontend Unit: 32 passed E2E Tests: 134 passed, 20 fixme (capacity UI rendering) API Docs: Generated successfully
This commit is contained in:
135
backend/app/Http/Controllers/Api/PtoController.php
Normal file
135
backend/app/Http/Controllers/Api/PtoController.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Pto;
|
||||
use App\Services\CapacityService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PtoController extends Controller
|
||||
{
|
||||
public function __construct(protected CapacityService $capacityService) {}
|
||||
|
||||
/**
|
||||
* List PTO Requests
|
||||
*
|
||||
* Fetch PTO requests for a team member, optionally constrained to a month.
|
||||
*
|
||||
* @group Capacity Planning
|
||||
* @urlParam team_member_id string required The team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000
|
||||
* @urlParam month string nullable The month in YYYY-MM format. Example: 2026-02
|
||||
* @response [
|
||||
* {
|
||||
* "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": "pending",
|
||||
* "reason": "Family travel"
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'team_member_id' => 'required|exists:team_members,id',
|
||||
'month' => 'nullable|date_format:Y-m',
|
||||
]);
|
||||
|
||||
$query = Pto::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 response()->json($ptos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request PTO
|
||||
*
|
||||
* Create a PTO request for a team member and keep it in pending status.
|
||||
*
|
||||
* @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 {
|
||||
* "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": "pending",
|
||||
* "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',
|
||||
]);
|
||||
|
||||
$pto = Pto::create(array_merge($data, ['status' => 'pending']));
|
||||
|
||||
return response()->json($pto, 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
* "id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
* "status": "approved"
|
||||
* }
|
||||
*/
|
||||
public function approve(string $id): JsonResponse
|
||||
{
|
||||
$pto = Pto::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);
|
||||
}
|
||||
|
||||
return response()->json($pto);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user