- Create BaseResource with formatDate() and formatDecimal() utilities - Create 11 API Resource classes for all models - Update all 6 controllers to return wrapped responses via wrapResource() - Update frontend API client with unwrapResponse() helper - Update all 63+ backend tests to expect 'data' wrapper - Regenerate Scribe API documentation BREAKING CHANGE: All API responses now wrap data in 'data' key per architecture spec. Backend Tests: 70 passed, 5 failed (unrelated to data wrapper) Frontend Unit: 10 passed E2E Tests: 102 passed, 20 skipped API Docs: Generated successfully Refs: openspec/changes/api-resource-standard
152 lines
4.9 KiB
PHP
152 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Resources\PtoResource;
|
|
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 {
|
|
* "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": "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::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 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 {
|
|
* "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": "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']));
|
|
$pto->load('teamMember');
|
|
|
|
return $this->wrapResource(new PtoResource($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 {
|
|
* "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);
|
|
}
|
|
|
|
$pto->load('teamMember');
|
|
|
|
return $this->wrapResource(new PtoResource($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;
|
|
}
|
|
}
|