qrCodes() ->where('type', QrCode::TYPE_EVENT) ->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, ]); } public function badgesHub(Request $request): View { $account = ladill_account(); $events = $account->qrCodes() ->where('type', QrCode::TYPE_EVENT) ->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, ]); } 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 */ 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(); } }