From 5e00f319a542a0d50ba8e8b3e9f1c4b3fc043f86 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 7 Jun 2026 12:27:42 +0000 Subject: [PATCH] Split attendees and badge printing; add a 3-step event create flow. Give each area its own sidebar entry and hub pages, and walk new events through details, QR design, then review before publish. Co-authored-by: Cursor --- .../Controllers/Events/AttendeeController.php | 87 ++++++++++ .../views/events/attendees-hub.blade.php | 49 ++++++ resources/views/events/attendees.blade.php | 75 +++------ .../views/events/badge-printing.blade.php | 89 ++++++++++ resources/views/events/badges-hub.blade.php | 54 ++++++ resources/views/layouts/user.blade.php | 2 +- resources/views/partials/sidebar.blade.php | 6 +- resources/views/qr-codes/create.blade.php | 157 +++++++++++++----- .../partials/qr-customizer-script.blade.php | 46 +++++ .../partials/type-fields-create.blade.php | 2 +- .../partials/type-fields-edit.blade.php | 19 ++- routes/web.php | 3 + 12 files changed, 488 insertions(+), 101 deletions(-) create mode 100644 resources/views/events/attendees-hub.blade.php create mode 100644 resources/views/events/badge-printing.blade.php create mode 100644 resources/views/events/badges-hub.blade.php diff --git a/app/Http/Controllers/Events/AttendeeController.php b/app/Http/Controllers/Events/AttendeeController.php index 4470462..f8ed066 100644 --- a/app/Http/Controllers/Events/AttendeeController.php +++ b/app/Http/Controllers/Events/AttendeeController.php @@ -18,6 +18,93 @@ class AttendeeController extends Controller { public function __construct(private readonly SmsService $sms) {} + 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); diff --git a/resources/views/events/attendees-hub.blade.php b/resources/views/events/attendees-hub.blade.php new file mode 100644 index 0000000..ef73b95 --- /dev/null +++ b/resources/views/events/attendees-hub.blade.php @@ -0,0 +1,49 @@ + + Attendees + +
+
+

Attendees

+

Manage registrations, check-ins, and programme sharing for each event.

+
+ +
+ + +
+ +
+ @if($events->isEmpty()) +
+

{{ $search ? 'No events match your search.' : 'No events yet.' }}

+ @unless($search) + + Create your first event + + @endunless +
+ @else + + @endif +
+
+
diff --git a/resources/views/events/attendees.blade.php b/resources/views/events/attendees.blade.php index 2b8565c..8aab6e4 100644 --- a/resources/views/events/attendees.blade.php +++ b/resources/views/events/attendees.blade.php @@ -2,14 +2,7 @@ @php $isContribution = ($qrCode->content()['mode'] ?? 'ticketing') === 'contributions'; @endphp {{ $isContribution ? 'Contributions' : 'Attendees' }} — {{ $qrCode->label }} -
+
{{-- Flash --}} @foreach(['success', 'error'] as $flash) @@ -21,12 +14,22 @@ @endforeach {{-- Header --}} -
- - - {{ $qrCode->label }} - -

{{ $isContribution ? 'Contributions' : 'Attendees' }}

+
+
+ + + Attendees + +

{{ $isContribution ? 'Contributions' : 'Attendees' }}

+

{{ $qrCode->content()['name'] ?? $qrCode->label }}

+
+ @unless($isContribution) + + + Print badges + + @endunless
{{-- Share programme outline --}} @@ -73,30 +76,15 @@
{{-- Toolbar --}} -
-
- - -
- @unless($isContribution) -
- selected - - - Print all badges - Download ZPL -
- @endunless -
+
+ + +
{{-- Table --}}
@@ -104,7 +92,6 @@ - @unless($isContribution)@endunless @unless($isContribution)@endunless @@ -115,14 +102,6 @@ @forelse($registrations as $reg) - @unless($isContribution) - - @endunless @empty - + @endforelse
{{ $isContribution ? 'Contributor' : 'Attendee' }} {{ $isContribution ? 'Category' : 'Ticket' }}Badge
- @if($reg->status === \App\Models\QrEventRegistration::STATUS_CONFIRMED) - - @endif -

{{ $reg->attendee_name }}

{{ $reg->attendee_email }}

@@ -154,7 +133,7 @@ @endunless
{{ $isContribution ? 'No contributions yet.' : 'No registrations yet.' }}
{{ $isContribution ? 'No contributions yet.' : 'No registrations yet.' }}
diff --git a/resources/views/events/badge-printing.blade.php b/resources/views/events/badge-printing.blade.php new file mode 100644 index 0000000..9babc8f --- /dev/null +++ b/resources/views/events/badge-printing.blade.php @@ -0,0 +1,89 @@ + + Badge printing — {{ $qrCode->label }} + +
+ +
+ + + Badge printing + +

{{ $qrCode->content()['name'] ?? $qrCode->label }}

+

{{ number_format($printableCount) }} confirmed attendees · {{ strtoupper($badgeSize) }} badges

+
+ +
+
+ selected + + + Print all badges + Download ZPL +
+ Manage attendees → +
+ +
+ + +
+ +
+
+ + + + + + + + + + + @forelse($registrations as $reg) + + + + + + + @empty + + @endforelse + +
+ @if($registrations->isNotEmpty()) + + @endif + AttendeeTicketBadge code
+ + +

{{ $reg->attendee_name }}

+

{{ $reg->attendee_email }}

+
+ {{ $reg->tier_name }} + {{ $reg->badge_code }}
No confirmed attendees to print yet.
+
+ @if($registrations->hasPages()) +
{{ $registrations->links() }}
+ @endif +
+
+
diff --git a/resources/views/events/badges-hub.blade.php b/resources/views/events/badges-hub.blade.php new file mode 100644 index 0000000..14dfb78 --- /dev/null +++ b/resources/views/events/badges-hub.blade.php @@ -0,0 +1,54 @@ + + Badge printing + +
+
+

Badge printing

+

Select attendees and print badges or download ZPL for label printers.

+
+ +
+ + +
+ +
+ @if($events->isEmpty()) +
+

{{ $search ? 'No events match your search.' : 'No events yet.' }}

+ @unless($search) + + Create your first event + + @endunless +
+ @else +
+ @foreach($events as $event) + @php + $content = $event->content(); + $isContribution = ($content['mode'] ?? 'ticketing') === 'contributions'; + @endphp + @if($isContribution) + @continue + @endif + +
+ +
+
+

{{ $content['name'] ?? $event->label }}

+

Badge size: {{ strtoupper($content['badge_size'] ?? '4x3') }}

+
+ + +
+ @endforeach +
+ @endif +
+
+
diff --git a/resources/views/layouts/user.blade.php b/resources/views/layouts/user.blade.php index 9266d29..0946dcb 100644 --- a/resources/views/layouts/user.blade.php +++ b/resources/views/layouts/user.blade.php @@ -66,7 +66,7 @@ 'homeUrl' => route('events.dashboard'), 'homeActive' => request()->routeIs('events.dashboard'), 'searchUrl' => route('events.index'), - 'searchActive' => request()->routeIs('events.index') || request()->routeIs('events.show') || request()->routeIs('events.attendees*'), + 'searchActive' => request()->routeIs('events.index') || request()->routeIs('events.show') || request()->routeIs('attendees.*') || request()->routeIs('events.attendees*') || request()->routeIs('badges.*') || request()->routeIs('events.badge-printing'), 'notificationsUrl' => route('notifications.index'), 'notificationsActive' => request()->routeIs('notifications.*'), 'unreadUrl' => route('notifications.unread'), diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 7417397..e535dbd 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -8,8 +8,12 @@ $main = [ ['name' => 'Overview', 'route' => route('events.dashboard'), 'active' => request()->routeIs('events.dashboard'), 'icon' => ''], - ['name' => 'Events', 'route' => route('events.index'), 'active' => request()->routeIs('events.index') || request()->routeIs('events.create') || request()->routeIs('events.show') || request()->routeIs('events.attendees*') || request()->routeIs('events.badges*'), + ['name' => 'Events', 'route' => route('events.index'), 'active' => request()->routeIs('events.index') || request()->routeIs('events.create') || request()->routeIs('events.show'), 'icon' => ''], + ['name' => 'Attendees', 'route' => route('attendees.index'), 'active' => request()->routeIs('attendees.*') || request()->routeIs('events.attendees*'), + 'icon' => ''], + ['name' => 'Badge printing', 'route' => route('badges.index'), 'active' => request()->routeIs('badges.*') || request()->routeIs('events.badge-printing') || request()->routeIs('events.badges*'), + 'icon' => ''], ['name' => 'Programmes', 'route' => route('programmes.index'), 'active' => request()->routeIs('programmes.*'), 'icon' => ''], ['name' => 'Payments & Payouts', 'route' => route('events.payouts'), 'active' => request()->routeIs('events.payouts'), diff --git a/resources/views/qr-codes/create.blade.php b/resources/views/qr-codes/create.blade.php index 4f0ca2e..431a3d6 100644 --- a/resources/views/qr-codes/create.blade.php +++ b/resources/views/qr-codes/create.blade.php @@ -16,6 +16,7 @@ price: @js((float) $pricePerQr), topupUrl: @js($topupUrl), type: @js($createType), + wizard: @js(! $isProgramme), })"> @if(session('error')) @@ -40,25 +41,53 @@ {{ $isProgramme ? 'Programmes' : 'Events' }}

{{ $isProgramme ? 'New programme' : 'Create event' }}

-

{{ $isProgramme ? 'Build a schedule outline and share it with attendees.' : 'Set up ticketing, registration, and your event QR.' }}

+

+ {{ $isProgramme ? 'Build a schedule outline and share it with attendees.' : 'Set up your event, then design the QR attendees will scan.' }} +

+ @unless($isProgramme) + {{-- Event creation wizard steps --}} +
+
    + @foreach(['Event details', 'QR code', 'Review & create'] as $i => $label) + @php $n = $i + 1; @endphp +
  1. + {{ $n }} + + @if($n < 3) + + @endif +
  2. + @endforeach +
+
+ @endunless +
+ class="grid grid-cols-1 gap-8" + :class="(!wizard || step >= 2) ? 'lg:grid-cols-[380px_1fr]' : ''" + @submit="wizard ? wizardSubmit($event) : submitOrTopup($event)"> @csrf - {{-- Left: Sticky preview (desktop only) --}} -
@@ -166,8 +227,11 @@
- + - + diff --git a/resources/views/qr-codes/partials/qr-customizer-script.blade.php b/resources/views/qr-codes/partials/qr-customizer-script.blade.php index 654989a..d471090 100644 --- a/resources/views/qr-codes/partials/qr-customizer-script.blade.php +++ b/resources/views/qr-codes/partials/qr-customizer-script.blade.php @@ -21,6 +21,8 @@ csrf: config.csrf ?? null, _canonicalSynced: false, type: config.type ?? 'url', + wizard: Boolean(config.wizard), + step: config.wizard ? 1 : 0, fg: config.style.foreground, bg: config.style.background, ecc: config.style.error_correction, @@ -252,6 +254,50 @@ this.redirectTopup(); } }, + nextStep(event) { + if (!this.wizard) { + return; + } + if (this.step === 1 && !this.validateWizardStep1()) { + event?.preventDefault(); + return; + } + if (this.step < 3) { + this.step++; + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, + prevStep() { + if (this.wizard && this.step > 1) { + this.step--; + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, + validateWizardStep1() { + const form = this.$refs.createForm; + const label = form?.querySelector('[name=label]'); + if (!label?.value?.trim()) { + label?.focus(); + label?.reportValidity?.(); + return false; + } + return label.checkValidity?.() !== false; + }, + wizardSubmit(event) { + if (this.wizard && this.step < 3) { + event?.preventDefault(); + this.nextStep(event); + return; + } + this.submitOrTopup(event); + }, + reviewLabel() { + return this.$refs.createForm?.querySelector('[name=label]')?.value?.trim() || ''; + }, + reviewEventName() { + const eventBlock = this.$refs.createForm?.querySelector('[data-event-name]'); + return eventBlock?.value?.trim() || this.reviewLabel(); + }, submitOrTopup(event) { if (this.balance <= 0 || this.balance < this.price) { event?.preventDefault(); diff --git a/resources/views/qr-codes/partials/type-fields-create.blade.php b/resources/views/qr-codes/partials/type-fields-create.blade.php index e0a5304..2fee804 100644 --- a/resources/views/qr-codes/partials/type-fields-create.blade.php +++ b/resources/views/qr-codes/partials/type-fields-create.blade.php @@ -393,7 +393,7 @@ removeField(i) { this.badgeFields.splice(i, 1); } }">
- diff --git a/resources/views/qr-codes/partials/type-fields-edit.blade.php b/resources/views/qr-codes/partials/type-fields-edit.blade.php index dd7da6a..716b8c7 100644 --- a/resources/views/qr-codes/partials/type-fields-edit.blade.php +++ b/resources/views/qr-codes/partials/type-fields-edit.blade.php @@ -597,11 +597,20 @@
@if (Route::has('events.attendees')) - - - Manage attendees & print badges - +
+ + + Manage attendees + + @if (Route::has('events.badge-printing') && ($c['mode'] ?? 'ticketing') !== 'contributions') + + + Print badges + + @endif +
@endif
diff --git a/routes/web.php b/routes/web.php index 35db51b..5fb7dc0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -55,7 +55,10 @@ Route::middleware(['auth'])->group(function () { Route::get('/events/{event}/preview.png', [QrCodeController::class, 'preview'])->name('events.preview'); Route::get('/events/{event}/download/{format}', [QrCodeController::class, 'download'])->name('events.download')->whereIn('format', ['png', 'svg', 'pdf']); + Route::get('/attendees', [AttendeeController::class, 'hub'])->name('attendees.index'); + Route::get('/badges', [AttendeeController::class, 'badgesHub'])->name('badges.index'); Route::get('/events/{event}/attendees', [AttendeeController::class, 'index'])->name('events.attendees'); + Route::get('/events/{event}/badge-printing', [AttendeeController::class, 'badgePrinting'])->name('events.badge-printing'); Route::get('/events/{event}/badges', [AttendeeController::class, 'badges'])->name('events.badges'); Route::get('/events/{event}/badges.zpl', [AttendeeController::class, 'badgesZpl'])->name('events.badges.zpl'); Route::patch('/events/{event}/attendees/{registration}/check-in', [AttendeeController::class, 'checkIn'])->name('events.attendees.check-in');