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>
141 lines
6.4 KiB
PHP
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']);
|
|
});
|
|
});
|