Files
crewli/api/routes/api.php
bert.hausmans 874eeee770 feat: event dashboard metric cards with stats endpoint (UX-02)
Add GET /events/{event}/stats endpoint returning aggregate counts for
persons (by status, approved without shift), pending identity matches,
and shift fill rates. Frontend metric cards component shows four
actionable KPIs on the event overview tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:19:31 +02:00

141 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Http\Controllers\Api\V1\CompanyController;
use App\Http\Controllers\Api\V1\CrowdListController;
use App\Http\Controllers\Api\V1\CrowdTypeController;
use App\Http\Controllers\Api\V1\EventController;
use App\Http\Controllers\Api\V1\FestivalSectionController;
use App\Http\Controllers\Api\V1\InvitationController;
use App\Http\Controllers\Api\V1\LocationController;
use App\Http\Controllers\Api\V1\LoginController;
use App\Http\Controllers\Api\V1\LogoutController;
use App\Http\Controllers\Api\V1\MeController;
use App\Http\Controllers\Api\V1\MemberController;
use App\Http\Controllers\Api\V1\OrganisationController;
use App\Http\Controllers\Api\V1\PersonController;
use App\Http\Controllers\Api\V1\PersonIdentityMatchController;
use App\Http\Controllers\Api\V1\PersonTagController;
use App\Http\Controllers\Api\V1\ShiftController;
use App\Http\Controllers\Api\V1\TimeSlotController;
use App\Http\Controllers\Api\V1\UserOrganisationTagController;
use App\Models\FestivalSection;
use App\Models\Organisation;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| All routes are automatically prefixed with /api/v1
|
*/
// Health check
Route::get('/', fn () => response()->json([
'success' => true,
'message' => 'Crewli API v1',
'timestamp' => now()->toIso8601String(),
]));
// Public auth routes
Route::post('auth/login', LoginController::class);
// Public invitation routes (no auth required)
Route::get('invitations/{token}', [InvitationController::class, 'show']);
Route::post('invitations/{token}/accept', [InvitationController::class, 'accept']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
// Auth
Route::get('auth/me', MeController::class);
Route::post('auth/logout', LogoutController::class);
// Organisations
Route::apiResource('organisations', OrganisationController::class)
->only(['index', 'show', 'store', 'update']);
// Events (nested under organisations)
Route::apiResource('organisations.events', EventController::class)
->only(['index', 'show', 'store', 'update', 'destroy']);
Route::get('organisations/{organisation}/events/{event}/children', [EventController::class, 'children']);
Route::post('organisations/{organisation}/events/{event}/transition', [EventController::class, 'transition']);
// Organisation-scoped resources
Route::prefix('organisations/{organisation}')->group(function () {
Route::apiResource('crowd-types', CrowdTypeController::class)
->except(['show']);
Route::apiResource('companies', CompanyController::class);
// Section categories (autocomplete)
Route::get('section-categories', function (Organisation $organisation) {
Gate::authorize('view', $organisation);
$categories = FestivalSection::query()
->whereIn('event_id', $organisation->events()->select('id'))
->whereNotNull('category')
->distinct()
->orderBy('category')
->pluck('category');
return response()->json(['data' => $categories]);
});
// Person tags (organisation settings)
Route::apiResource('person-tags', PersonTagController::class)
->except(['show']);
Route::get('person-tag-categories', [PersonTagController::class, 'categories']);
// User tag assignments
Route::get('users/{user}/tags', [UserOrganisationTagController::class, 'index']);
Route::post('users/{user}/tags', [UserOrganisationTagController::class, 'store']);
Route::put('users/{user}/tags/sync', [UserOrganisationTagController::class, 'sync']);
Route::delete('users/{user}/tags/{userOrganisationTag}', [UserOrganisationTagController::class, 'destroy']);
// Identity matches
Route::get('identity-matches', [PersonIdentityMatchController::class, 'index']);
Route::get('persons/{person}/identity-match', [PersonIdentityMatchController::class, 'showForPerson']);
Route::post('identity-matches/{personIdentityMatch}/confirm', [PersonIdentityMatchController::class, 'confirm']);
Route::post('identity-matches/{personIdentityMatch}/dismiss', [PersonIdentityMatchController::class, 'dismiss']);
Route::post('identity-matches/bulk-confirm', [PersonIdentityMatchController::class, 'bulkConfirm']);
// Invitations & Members
Route::post('invite', [InvitationController::class, 'invite']);
Route::delete('invitations/{invitation}', [InvitationController::class, 'revoke']);
Route::get('members', [MemberController::class, 'index']);
Route::put('members/{user}', [MemberController::class, 'update']);
Route::delete('members/{user}', [MemberController::class, 'destroy']);
});
// Event-scoped resources
Route::get('events/{event}/stats', [EventController::class, 'stats']);
Route::prefix('events/{event}')->group(function () {
Route::apiResource('locations', LocationController::class)
->except(['show']);
Route::apiResource('sections', FestivalSectionController::class)
->except(['show']);
Route::post('sections/reorder', [FestivalSectionController::class, 'reorder']);
Route::apiResource('time-slots', TimeSlotController::class)
->except(['show']);
// Shifts nested under sections
Route::prefix('sections/{section}')->group(function () {
Route::apiResource('shifts', ShiftController::class)
->except(['show']);
Route::post('shifts/{shift}/assign', [ShiftController::class, 'assign']);
Route::post('shifts/{shift}/claim', [ShiftController::class, 'claim']);
});
Route::apiResource('persons', PersonController::class);
Route::post('persons/{person}/approve', [PersonController::class, 'approve']);
Route::apiResource('crowd-lists', CrowdListController::class)
->except(['show']);
Route::get('crowd-lists/{crowdList}/persons', [CrowdListController::class, 'persons']);
Route::post('crowd-lists/{crowdList}/persons', [CrowdListController::class, 'addPerson']);
Route::delete('crowd-lists/{crowdList}/persons/{person}', [CrowdListController::class, 'removePerson']);
});
});