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>
This commit is contained in:
2026-04-10 16:19:31 +02:00
parent b094018eeb
commit 874eeee770
9 changed files with 546 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ use App\Http\Requests\Api\V1\UpdateEventRequest;
use App\Http\Resources\Api\V1\EventResource;
use App\Models\Event;
use App\Models\Organisation;
use App\Models\PersonIdentityMatch;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
@@ -124,4 +125,57 @@ final class EventController extends Controller
return EventResource::collection($children);
}
public function stats(Event $event): JsonResponse
{
Gate::authorize('view', $event);
$personCounts = $event->persons()
->selectRaw("
COUNT(*) as total,
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved,
SUM(CASE WHEN status IN ('pending', 'applied') THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected
")
->first();
$approvedWithoutShift = $event->persons()
->where('status', 'approved')
->whereDoesntHave('shiftAssignments')
->count();
$pendingMatches = PersonIdentityMatch::pending()
->whereHas('person', fn ($q) => $q->where('event_id', $event->id))
->count();
$shifts = $event->festivalSections()
->with(['shifts' => fn ($q) => $q->withCount([
'shiftAssignments' => fn ($q) => $q->where('status', 'approved'),
])])
->get()
->flatMap->shifts;
$shiftsTotal = $shifts->count();
$shiftsFilled = $shifts->filter(
fn ($s) => $s->shift_assignments_count >= $s->slots_total
)->count();
$total = (int) $personCounts->total;
$approved = (int) $personCounts->approved;
$pending = (int) $personCounts->pending;
$rejected = (int) $personCounts->rejected;
return response()->json(['data' => [
'persons_total' => $total,
'persons_approved' => $approved,
'persons_pending' => $pending,
'persons_rejected' => $rejected,
'persons_other' => $total - $approved - $pending - $rejected,
'persons_approved_without_shift' => $approvedWithoutShift,
'pending_identity_matches' => $pendingMatches,
'shifts_total' => $shiftsTotal,
'shifts_filled' => $shiftsFilled,
'shifts_understaffed' => $shiftsTotal - $shiftsFilled,
]]);
}
}