Files
ladill-events/app/Http/Controllers/Events/AttendeeController.php
T
isaaccladandCursor 05a6be7efe
Deploy Ladill Events / deploy (push) Successful in 41s
Complete Events–Meet integration phases 2–5.
Programme sync to Meet rooms, wallet-billed mass comms, webhook lifecycle updates, and registration access bridge with join-window gating on public pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 22:31:45 +00:00

290 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Events;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use App\Services\Events\EventCommsService;
use App\Support\Events\EventBadgeZpl;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class AttendeeController extends Controller
{
public function __construct(
private readonly EventCommsService $comms,
) {}
public function hub(Request $request): View
{
$account = ladill_account();
$search = trim((string) $request->query('search', ''));
$events = $account->qrCodes()
->where('type', QrCode::TYPE_EVENT)
->when($search !== '', fn ($q) => $q->where('label', 'like', "%{$search}%"))
->latest()
->get()
->map(function (QrCode $event) {
$event->confirmed_count = $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->count();
$event->checked_in_count = $event->eventRegistrations()
->whereNotNull('checked_in_at')
->count();
return $event;
});
return view('events.attendees-hub', [
'events' => $events,
'search' => $search,
]);
}
public function badgesHub(Request $request): View
{
$account = ladill_account();
$search = trim((string) $request->query('search', ''));
$events = $account->qrCodes()
->where('type', QrCode::TYPE_EVENT)
->when($search !== '', fn ($q) => $q->where('label', 'like', "%{$search}%"))
->latest()
->get()
->map(function (QrCode $event) {
$event->printable_count = $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->count();
return $event;
});
return view('events.badges-hub', [
'events' => $events,
'search' => $search,
]);
}
public function badgePrinting(Request $request, QrCode $event): View
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$isContribution = ($event->content()['mode'] ?? 'ticketing') === 'contributions';
if ($isContribution) {
abort(404);
}
$query = $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->latest();
if ($search = trim((string) $request->query('search', ''))) {
$query->where(function ($q) use ($search) {
$q->where('attendee_name', 'like', "%{$search}%")
->orWhere('attendee_email', 'like', "%{$search}%")
->orWhere('badge_code', 'like', "%{$search}%");
});
}
$registrations = $query->paginate(40)->withQueryString();
$badgeSize = $event->content()['badge_size'] ?? '4x3';
return view('events.badge-printing', [
'qrCode' => $event,
'event' => $event,
'registrations' => $registrations,
'badgeSize' => $badgeSize,
'printableCount' => $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->count(),
]);
}
public function index(Request $request, QrCode $event): View
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$query = $event->eventRegistrations()->latest();
if ($search = trim((string) $request->query('search', ''))) {
$query->where(function ($q) use ($search) {
$q->where('attendee_name', 'like', "%{$search}%")
->orWhere('attendee_email', 'like', "%{$search}%")
->orWhere('badge_code', 'like', "%{$search}%");
});
}
if ($status = $request->query('status')) {
$query->where('status', $status);
}
$registrations = $query->paginate(40)->withQueryString();
$stats = [
'total' => $event->eventRegistrations()->where('status', QrEventRegistration::STATUS_CONFIRMED)->count(),
'checked_in' => $event->eventRegistrations()->whereNotNull('checked_in_at')->count(),
'revenue' => $event->eventRegistrations()->where('status', QrEventRegistration::STATUS_CONFIRMED)->sum('amount_minor') / 100,
];
$content = $event->content();
$format = (string) ($content['format'] ?? 'in_person');
$virtualSessions = collect((array) ($content['virtual_sessions'] ?? []))->filter(fn ($s) => is_array($s));
if (in_array($format, ['virtual', 'hybrid'], true) && $virtualSessions->isNotEmpty()) {
$stats['virtual_sessions'] = $virtualSessions->count();
$stats['virtual_live'] = $virtualSessions->where('meet_status', 'live')->count();
$stats['virtual_ended'] = $virtualSessions->where('meet_status', 'ended')->count();
$stats['virtual_recordings'] = $virtualSessions->filter(fn ($s) => ! empty($s['recording_url']))->count();
}
$programme = $this->programmeFor($event);
$joinUrl = $this->comms->primaryJoinUrl($event);
$commsModes = array_values(array_filter([
$programme ? EventCommsService::MODE_PROGRAMME : null,
$joinUrl !== '' ? EventCommsService::MODE_JOIN : null,
($programme && $joinUrl !== '') ? EventCommsService::MODE_BOTH : null,
]));
return view('events.attendees', [
'qrCode' => $event,
'event' => $event,
'registrations' => $registrations,
'stats' => $stats,
'programme' => $programme,
'commsModes' => $commsModes,
'defaultCommsMode' => $commsModes[0] ?? EventCommsService::MODE_PROGRAMME,
]);
}
public function previewComms(QrCode $event, Request $request): JsonResponse
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$mode = (string) $request->query('mode', EventCommsService::MODE_PROGRAMME);
abort_unless(in_array($mode, [EventCommsService::MODE_PROGRAMME, EventCommsService::MODE_JOIN, EventCommsService::MODE_BOTH], true), 422);
return response()->json($this->comms->preview($event, $mode));
}
public function shareComms(QrCode $event, Request $request): RedirectResponse
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$validated = $request->validate([
'mode' => ['required', Rule::in([
EventCommsService::MODE_PROGRAMME,
EventCommsService::MODE_JOIN,
EventCommsService::MODE_BOTH,
])],
]);
$preview = $this->comms->preview($event, $validated['mode']);
if (! $preview['affordable']) {
return back()->with('error', 'Insufficient wallet balance. Add funds in Billing before sending.');
}
if ($preview['recipients'] === 0) {
return back()->with('error', 'No confirmed attendees to message yet.');
}
$result = $this->comms->send($event, $validated['mode']);
if ($result['emailed'] === 0 && $result['texted'] === 0) {
return back()->with('error', 'No messages were sent — check that attendees have email or phone on file.');
}
return back()->with('success', sprintf(
'Sent — %d email(s), %d SMS, %d skipped.',
$result['emailed'],
$result['texted'],
$result['skipped'],
));
}
/** @deprecated Use shareComms with mode=programme */
public function shareProgramme(QrCode $event): RedirectResponse
{
request()->merge(['mode' => EventCommsService::MODE_PROGRAMME]);
return $this->shareComms($event, request());
}
private function programmeFor(QrCode $event): ?QrCode
{
$programmeId = (int) ($event->content()['programme_qr_id'] ?? 0);
if ($programmeId <= 0) {
return null;
}
return QrCode::where('id', $programmeId)
->where('user_id', $event->user_id)
->where('type', QrCode::TYPE_ITINERARY)
->first();
}
public function badges(Request $request, QrCode $event): View
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$registrations = $this->selectedRegistrations($request, $event);
return view('events.badges', [
'qrCode' => $event,
'event' => $event,
'registrations' => $registrations,
'auto' => $request->boolean('auto'),
]);
}
public function badgesZpl(Request $request, QrCode $event): Response
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$registrations = $this->selectedRegistrations($request, $event);
$zpl = EventBadgeZpl::forRegistrations($event, $registrations);
return response($zpl, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="badges-' . $event->short_code . '.zpl"',
]);
}
public function checkIn(QrCode $event, QrEventRegistration $registration): RedirectResponse
{
$this->authorize('view', $event);
abort_unless($registration->qr_code_id === $event->id, 404);
$registration->update([
'checked_in_at' => $registration->checked_in_at ? null : now(),
]);
return back();
}
/** @return \Illuminate\Support\Collection<int, QrEventRegistration> */
private function selectedRegistrations(Request $request, QrCode $event)
{
$query = $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->latest();
$ids = array_filter((array) $request->query('ids', []));
if (! empty($ids)) {
$query->whereIn('id', $ids);
}
return $query->get();
}
}