Split attendees and badge printing; add a 3-step event create flow.
Deploy Ladill Events / deploy (push) Successful in 35s
Deploy Ladill Events / deploy (push) Successful in 35s
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<x-user-layout>
|
||||
<x-slot name="title">Attendees</x-slot>
|
||||
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Attendees</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Manage registrations, check-ins, and programme sharing for each event.</p>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="flex items-center gap-2">
|
||||
<input type="text" name="search" value="{{ $search }}" placeholder="Search events…"
|
||||
class="w-full max-w-sm rounded-xl border border-slate-200 px-3.5 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<button type="submit" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">Search</button>
|
||||
</form>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
@if($events->isEmpty())
|
||||
<div class="px-6 py-14 text-center">
|
||||
<p class="text-sm text-slate-500">{{ $search ? 'No events match your search.' : 'No events yet.' }}</p>
|
||||
@unless($search)
|
||||
<a href="{{ route('events.create') }}" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700 transition">
|
||||
Create your first event
|
||||
</a>
|
||||
@endunless
|
||||
</div>
|
||||
@else
|
||||
<div class="divide-y divide-slate-100">
|
||||
@foreach($events as $event)
|
||||
@php $content = $event->content(); @endphp
|
||||
<a href="{{ route('events.attendees', $event) }}" class="flex items-center gap-4 px-6 py-4 transition hover:bg-slate-50">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-slate-900">{{ $content['name'] ?? $event->label }}</p>
|
||||
<p class="truncate text-xs text-slate-500">{{ $event->label }}</p>
|
||||
</div>
|
||||
<div class="hidden text-right text-xs text-slate-500 sm:block">
|
||||
<p class="font-semibold text-slate-900">{{ number_format($event->confirmed_count) }} registered</p>
|
||||
<p>{{ number_format($event->checked_in_count) }} checked in</p>
|
||||
</div>
|
||||
<svg class="h-4 w-4 shrink-0 text-slate-300" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-user-layout>
|
||||
@@ -2,14 +2,7 @@
|
||||
@php $isContribution = ($qrCode->content()['mode'] ?? 'ticketing') === 'contributions'; @endphp
|
||||
<x-slot name="title">{{ $isContribution ? 'Contributions' : 'Attendees' }} — {{ $qrCode->label }}</x-slot>
|
||||
|
||||
<div class="mx-auto max-w-5xl space-y-6"
|
||||
x-data="{
|
||||
selected: [],
|
||||
toggle(id) { const i = this.selected.indexOf(id); i === -1 ? this.selected.push(id) : this.selected.splice(i, 1); },
|
||||
get qs() { return this.selected.map(id => 'ids[]=' + id).join('&'); },
|
||||
printSelected() { window.open(@js(route('events.badges', $qrCode)) + '?auto=1&' + this.qs, '_blank'); },
|
||||
zplSelected() { window.location = @js(route('events.badges.zpl', $qrCode)) + '?' + this.qs; }
|
||||
}">
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
|
||||
{{-- Flash --}}
|
||||
@foreach(['success', 'error'] as $flash)
|
||||
@@ -21,12 +14,22 @@
|
||||
@endforeach
|
||||
|
||||
{{-- Header --}}
|
||||
<div>
|
||||
<a href="{{ route('events.show', $qrCode) }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
|
||||
{{ $qrCode->label }}
|
||||
</a>
|
||||
<h1 class="mt-1.5 text-2xl font-bold text-slate-900">{{ $isContribution ? 'Contributions' : 'Attendees' }}</h1>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<a href="{{ route('attendees.index') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
|
||||
Attendees
|
||||
</a>
|
||||
<h1 class="mt-1.5 text-2xl font-bold text-slate-900">{{ $isContribution ? 'Contributions' : 'Attendees' }}</h1>
|
||||
<p class="mt-0.5 text-sm text-slate-500">{{ $qrCode->content()['name'] ?? $qrCode->label }}</p>
|
||||
</div>
|
||||
@unless($isContribution)
|
||||
<a href="{{ route('events.badge-printing', $qrCode) }}"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-xl border border-violet-200 bg-violet-50 px-4 py-2.5 text-sm font-semibold text-violet-700 hover:bg-violet-100 transition">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25Z"/></svg>
|
||||
Print badges
|
||||
</a>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
{{-- Share programme outline --}}
|
||||
@@ -73,30 +76,15 @@
|
||||
</div>
|
||||
|
||||
{{-- Toolbar --}}
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<form method="GET" class="flex items-center gap-2">
|
||||
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search name, email, code…"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<select name="status" onchange="this.form.submit()" class="rounded-xl border border-slate-200 px-3 py-2 text-sm text-slate-600 focus:border-indigo-400 focus:outline-none">
|
||||
<option value="">All</option>
|
||||
<option value="confirmed" @selected(request('status') === 'confirmed')>Confirmed</option>
|
||||
<option value="pending" @selected(request('status') === 'pending')>Pending</option>
|
||||
</select>
|
||||
</form>
|
||||
@unless($isContribution)
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span x-show="selected.length" x-cloak class="text-xs font-medium text-slate-500"><span x-text="selected.length"></span> selected</span>
|
||||
<button type="button" x-show="selected.length" x-cloak @click="printSelected()"
|
||||
class="rounded-xl bg-indigo-600 px-3.5 py-2 text-xs font-semibold text-white hover:bg-indigo-700 transition">Print selected</button>
|
||||
<button type="button" x-show="selected.length" x-cloak @click="zplSelected()"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-50 transition">ZPL selected</button>
|
||||
<a href="{{ route('events.badges', $qrCode) }}?auto=1" target="_blank"
|
||||
class="rounded-xl bg-slate-900 px-3.5 py-2 text-xs font-semibold text-white hover:bg-slate-800 transition">Print all badges</a>
|
||||
<a href="{{ route('events.badges.zpl', $qrCode) }}"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-50 transition">Download ZPL</a>
|
||||
</div>
|
||||
@endunless
|
||||
</div>
|
||||
<form method="GET" class="flex items-center gap-2">
|
||||
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search name, email, code…"
|
||||
class="w-full max-w-sm rounded-xl border border-slate-200 px-3.5 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<select name="status" onchange="this.form.submit()" class="rounded-xl border border-slate-200 px-3 py-2 text-sm text-slate-600 focus:border-indigo-400 focus:outline-none">
|
||||
<option value="">All</option>
|
||||
<option value="confirmed" @selected(request('status') === 'confirmed')>Confirmed</option>
|
||||
<option value="pending" @selected(request('status') === 'pending')>Pending</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
@@ -104,7 +92,6 @@
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="border-b border-slate-100 bg-slate-50/70 text-left text-xs uppercase tracking-wide text-slate-400">
|
||||
<tr>
|
||||
@unless($isContribution)<th class="px-4 py-3 w-8"></th>@endunless
|
||||
<th class="px-4 py-3">{{ $isContribution ? 'Contributor' : 'Attendee' }}</th>
|
||||
<th class="px-4 py-3">{{ $isContribution ? 'Category' : 'Ticket' }}</th>
|
||||
@unless($isContribution)<th class="px-4 py-3">Badge</th>@endunless
|
||||
@@ -115,14 +102,6 @@
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($registrations as $reg)
|
||||
<tr class="hover:bg-slate-50/60">
|
||||
@unless($isContribution)
|
||||
<td class="px-4 py-3">
|
||||
@if($reg->status === \App\Models\QrEventRegistration::STATUS_CONFIRMED)
|
||||
<input type="checkbox" :checked="selected.includes({{ $reg->id }})" @change="toggle({{ $reg->id }})"
|
||||
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
@endif
|
||||
</td>
|
||||
@endunless
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-semibold text-slate-800">{{ $reg->attendee_name }}</p>
|
||||
<p class="text-xs text-slate-400">{{ $reg->attendee_email }}</p>
|
||||
@@ -154,7 +133,7 @@
|
||||
@endunless
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="{{ $isContribution ? 3 : 6 }}" class="px-4 py-12 text-center text-sm text-slate-400">{{ $isContribution ? 'No contributions yet.' : 'No registrations yet.' }}</td></tr>
|
||||
<tr><td colspan="{{ $isContribution ? 3 : 5 }}" class="px-4 py-12 text-center text-sm text-slate-400">{{ $isContribution ? 'No contributions yet.' : 'No registrations yet.' }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<x-user-layout>
|
||||
<x-slot name="title">Badge printing — {{ $qrCode->label }}</x-slot>
|
||||
|
||||
<div class="mx-auto max-w-5xl space-y-6"
|
||||
x-data="{
|
||||
selected: [],
|
||||
toggle(id) { const i = this.selected.indexOf(id); i === -1 ? this.selected.push(id) : this.selected.splice(i, 1); },
|
||||
toggleAll(ids) { this.selected = this.selected.length === ids.length ? [] : [...ids]; },
|
||||
get qs() { return this.selected.map(id => 'ids[]=' + id).join('&'); },
|
||||
printSelected() { window.open(@js(route('events.badges', $qrCode)) + '?auto=1&' + this.qs, '_blank'); },
|
||||
zplSelected() { window.location = @js(route('events.badges.zpl', $qrCode)) + '?' + this.qs; }
|
||||
}">
|
||||
|
||||
<div>
|
||||
<a href="{{ route('badges.index') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
|
||||
Badge printing
|
||||
</a>
|
||||
<h1 class="mt-1.5 text-2xl font-bold text-slate-900">{{ $qrCode->content()['name'] ?? $qrCode->label }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ number_format($printableCount) }} confirmed attendees · {{ strtoupper($badgeSize) }} badges</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span x-show="selected.length" x-cloak class="text-xs font-medium text-slate-500"><span x-text="selected.length"></span> selected</span>
|
||||
<button type="button" x-show="selected.length" x-cloak @click="printSelected()"
|
||||
class="rounded-xl bg-indigo-600 px-3.5 py-2 text-xs font-semibold text-white hover:bg-indigo-700 transition">Print selected</button>
|
||||
<button type="button" x-show="selected.length" x-cloak @click="zplSelected()"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-50 transition">ZPL selected</button>
|
||||
<a href="{{ route('events.badges', $qrCode) }}?auto=1" target="_blank"
|
||||
class="rounded-xl bg-slate-900 px-3.5 py-2 text-xs font-semibold text-white hover:bg-slate-800 transition">Print all badges</a>
|
||||
<a href="{{ route('events.badges.zpl', $qrCode) }}"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-50 transition">Download ZPL</a>
|
||||
</div>
|
||||
<a href="{{ route('events.attendees', $qrCode) }}" class="text-xs font-semibold text-indigo-600 hover:text-indigo-800">Manage attendees →</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="flex items-center gap-2">
|
||||
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search name, email, code…"
|
||||
class="w-full max-w-sm rounded-xl border border-slate-200 px-3.5 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<button type="submit" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">Search</button>
|
||||
</form>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="border-b border-slate-100 bg-slate-50/70 text-left text-xs uppercase tracking-wide text-slate-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3 w-8">
|
||||
@if($registrations->isNotEmpty())
|
||||
<input type="checkbox"
|
||||
@change="toggleAll(@js($registrations->pluck('id')->all()))"
|
||||
:checked="selected.length === @js($registrations->count()) && selected.length > 0"
|
||||
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
@endif
|
||||
</th>
|
||||
<th class="px-4 py-3">Attendee</th>
|
||||
<th class="px-4 py-3">Ticket</th>
|
||||
<th class="px-4 py-3">Badge code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($registrations as $reg)
|
||||
<tr class="hover:bg-slate-50/60">
|
||||
<td class="px-4 py-3">
|
||||
<input type="checkbox" :checked="selected.includes({{ $reg->id }})" @change="toggle({{ $reg->id }})"
|
||||
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-semibold text-slate-800">{{ $reg->attendee_name }}</p>
|
||||
<p class="text-xs text-slate-400">{{ $reg->attendee_email }}</p>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="font-medium text-slate-700">{{ $reg->tier_name }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3"><code class="rounded bg-slate-100 px-2 py-0.5 text-xs">{{ $reg->badge_code }}</code></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="px-4 py-12 text-center text-sm text-slate-400">No confirmed attendees to print yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if($registrations->hasPages())
|
||||
<div class="border-t border-slate-100 px-4 py-3">{{ $registrations->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-user-layout>
|
||||
@@ -0,0 +1,54 @@
|
||||
<x-user-layout>
|
||||
<x-slot name="title">Badge printing</x-slot>
|
||||
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Badge printing</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Select attendees and print badges or download ZPL for label printers.</p>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="flex items-center gap-2">
|
||||
<input type="text" name="search" value="{{ $search }}" placeholder="Search events…"
|
||||
class="w-full max-w-sm rounded-xl border border-slate-200 px-3.5 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<button type="submit" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">Search</button>
|
||||
</form>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
@if($events->isEmpty())
|
||||
<div class="px-6 py-14 text-center">
|
||||
<p class="text-sm text-slate-500">{{ $search ? 'No events match your search.' : 'No events yet.' }}</p>
|
||||
@unless($search)
|
||||
<a href="{{ route('events.create') }}" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700 transition">
|
||||
Create your first event
|
||||
</a>
|
||||
@endunless
|
||||
</div>
|
||||
@else
|
||||
<div class="divide-y divide-slate-100">
|
||||
@foreach($events as $event)
|
||||
@php
|
||||
$content = $event->content();
|
||||
$isContribution = ($content['mode'] ?? 'ticketing') === 'contributions';
|
||||
@endphp
|
||||
@if($isContribution)
|
||||
@continue
|
||||
@endif
|
||||
<a href="{{ route('events.badge-printing', $event) }}" class="flex items-center gap-4 px-6 py-4 transition hover:bg-slate-50">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-violet-50 text-violet-600">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25Z"/></svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-slate-900">{{ $content['name'] ?? $event->label }}</p>
|
||||
<p class="truncate text-xs text-slate-500">Badge size: {{ strtoupper($content['badge_size'] ?? '4x3') }}</p>
|
||||
</div>
|
||||
<div class="hidden text-right text-xs text-slate-500 sm:block">
|
||||
<p class="font-semibold text-slate-900">{{ number_format($event->printable_count) }} printable</p>
|
||||
</div>
|
||||
<svg class="h-4 w-4 shrink-0 text-slate-300" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-user-layout>
|
||||
@@ -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'),
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
$main = [
|
||||
['name' => 'Overview', 'route' => route('events.dashboard'), 'active' => request()->routeIs('events.dashboard'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12 11.2 3.05c.44-.44 1.15-.44 1.59 0L21.75 12M4.5 9.75v10.5a.75.75 0 0 0 .75.75H9.75v-6a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75v6h4.5a.75.75 0 0 0 .75-.75V9.75" />'],
|
||||
['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' => '<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />'],
|
||||
['name' => 'Attendees', 'route' => route('attendees.index'), 'active' => request()->routeIs('attendees.*') || request()->routeIs('events.attendees*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />'],
|
||||
['name' => 'Badge printing', 'route' => route('badges.index'), 'active' => request()->routeIs('badges.*') || request()->routeIs('events.badge-printing') || request()->routeIs('events.badges*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25Z" />'],
|
||||
['name' => 'Programmes', 'route' => route('programmes.index'), 'active' => request()->routeIs('programmes.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25Z" />'],
|
||||
['name' => 'Payments & Payouts', 'route' => route('events.payouts'), 'active' => request()->routeIs('events.payouts'),
|
||||
|
||||
@@ -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' }}
|
||||
</a>
|
||||
<h1 class="mt-2 text-2xl font-bold text-slate-900">{{ $isProgramme ? 'New programme' : 'Create event' }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $isProgramme ? 'Build a schedule outline and share it with attendees.' : 'Set up ticketing, registration, and your event QR.' }}</p>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
{{ $isProgramme ? 'Build a schedule outline and share it with attendees.' : 'Set up your event, then design the QR attendees will scan.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@unless($isProgramme)
|
||||
{{-- Event creation wizard steps --}}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white px-4 py-4 sm:px-6">
|
||||
<ol class="flex items-center justify-between gap-2">
|
||||
@foreach(['Event details', 'QR code', 'Review & create'] as $i => $label)
|
||||
@php $n = $i + 1; @endphp
|
||||
<li class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-bold transition"
|
||||
:class="step >= {{ $n }} ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-400'">{{ $n }}</span>
|
||||
<span class="hidden truncate text-xs font-semibold sm:block"
|
||||
:class="step >= {{ $n }} ? 'text-slate-900' : 'text-slate-400'">{{ $label }}</span>
|
||||
@if($n < 3)
|
||||
<span class="mx-1 hidden h-px flex-1 bg-slate-200 sm:block"></span>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
@endunless
|
||||
|
||||
<form x-ref="createForm" action="{{ route('events.store') }}" method="POST" enctype="multipart/form-data"
|
||||
class="grid grid-cols-1 gap-8 lg:grid-cols-[380px_1fr]"
|
||||
@submit="submitOrTopup($event)">
|
||||
class="grid grid-cols-1 gap-8"
|
||||
:class="(!wizard || step >= 2) ? 'lg:grid-cols-[380px_1fr]' : ''"
|
||||
@submit="wizard ? wizardSubmit($event) : submitOrTopup($event)">
|
||||
@csrf
|
||||
<input type="hidden" name="type" value="{{ $createType }}">
|
||||
|
||||
{{-- Left: Sticky preview (desktop only) --}}
|
||||
<div class="hidden lg:block lg:sticky lg:top-6 lg:self-start">
|
||||
@include('qr-codes.partials.qr-preview-card', ['showDownloads' => false])
|
||||
{{-- QR preview (step 2 for events, always for programmes) --}}
|
||||
<div class="lg:sticky lg:top-6 lg:self-start"
|
||||
x-show="!wizard || step === 2 || step === 3"
|
||||
x-cloak
|
||||
:class="(!wizard || step >= 2) ? 'block' : 'hidden'">
|
||||
<div class="hidden lg:block">
|
||||
@include('qr-codes.partials.qr-preview-card', ['showDownloads' => false])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Right: Controls --}}
|
||||
<div class="space-y-5 pb-4 lg:pb-0">
|
||||
<div class="space-y-5 pb-4 lg:pb-0"
|
||||
:class="wizard && step >= 2 ? 'lg:col-start-2' : (wizard && step === 1 ? 'lg:col-span-full' : '')">
|
||||
|
||||
{{-- Content first --}}
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||||
{{-- Step 1 / Programme: Event details --}}
|
||||
<div x-show="!wizard || step === 1" class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||||
<div class="border-b border-slate-100 bg-slate-50/70 px-5 py-4">
|
||||
<h2 class="text-sm font-semibold text-slate-900">{{ $isProgramme ? 'Programme details' : 'Event details' }}</h2>
|
||||
<p class="mt-0.5 text-xs text-slate-400">{{ $isProgramme ? 'Title, schedule, and venue for this outline.' : 'Name, dates, ticketing, and registration settings.' }}</p>
|
||||
@@ -72,7 +101,6 @@
|
||||
placeholder="{{ $isProgramme ? 'e.g. Annual conference programme' : 'e.g. Tech summit 2026' }}">
|
||||
</div>
|
||||
|
||||
{{-- Custom link --}}
|
||||
<div x-data="{
|
||||
slug: @js(old('custom_short_code', '')),
|
||||
status: '',
|
||||
@@ -109,27 +137,16 @@
|
||||
placeholder="my-brand">
|
||||
</div>
|
||||
<div class="mt-1.5 flex items-center gap-1.5 text-[11px]">
|
||||
<template x-if="status === 'checking'">
|
||||
<span class="text-slate-400">Checking…</span>
|
||||
</template>
|
||||
<template x-if="status === 'checking'"><span class="text-slate-400">Checking…</span></template>
|
||||
<template x-if="status === 'available'">
|
||||
<span class="flex items-center gap-1 text-green-600">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
Available — your link will be <span x-text="baseUrl + '/' + slug.trim()" class="font-medium text-green-700"></span>
|
||||
Available
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="status === 'taken'">
|
||||
<span class="flex items-center gap-1 text-red-500">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
||||
Already taken — choose another
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="status === 'invalid'">
|
||||
<span class="text-red-500">3–20 chars, letters, numbers and hyphens only (no leading/trailing hyphens)</span>
|
||||
</template>
|
||||
<template x-if="status === ''">
|
||||
<span class="text-slate-400">Leave blank to auto-generate</span>
|
||||
</template>
|
||||
<template x-if="status === 'taken'"><span class="text-red-500">Already taken</span></template>
|
||||
<template x-if="status === 'invalid'"><span class="text-red-500">Invalid format</span></template>
|
||||
<template x-if="status === ''"><span class="text-slate-400">Leave blank to auto-generate</span></template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,28 +154,72 @@
|
||||
@include('qr-codes.partials.type-fields-create')
|
||||
</div>
|
||||
|
||||
@unless($isProgramme)
|
||||
<p class="rounded-xl bg-slate-50 px-3.5 py-2.5 text-xs leading-5 text-slate-400">
|
||||
You will design the QR code in the next step. Details can be updated anytime without reprinting.
|
||||
</p>
|
||||
@else
|
||||
<p class="rounded-xl bg-slate-50 px-3.5 py-2.5 text-xs leading-5 text-slate-400">
|
||||
Attendees scan your Ladill event link — update details anytime without reprinting the QR.
|
||||
</p>
|
||||
@endunless
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Customization --}}
|
||||
@include('qr-codes.partials.customization-fields', [
|
||||
'style' => $defaultStyle,
|
||||
'moduleStyles' => $moduleStyles,
|
||||
'cornerOuterStyles' => $cornerOuterStyles,
|
||||
'cornerInnerStyles' => $cornerInnerStyles,
|
||||
'frameStyles' => $frameStyles,
|
||||
])
|
||||
{{-- Step 2: QR customization (events) or inline (programmes) --}}
|
||||
<div x-show="!wizard || step === 2">
|
||||
@include('qr-codes.partials.customization-fields', [
|
||||
'style' => $defaultStyle,
|
||||
'moduleStyles' => $moduleStyles,
|
||||
'cornerOuterStyles' => $cornerOuterStyles,
|
||||
'cornerInnerStyles' => $cornerInnerStyles,
|
||||
'frameStyles' => $frameStyles,
|
||||
])
|
||||
</div>
|
||||
|
||||
{{-- Desktop submit button --}}
|
||||
<button type="button"
|
||||
@click="submitOrTopup($event)"
|
||||
class="hidden lg:block w-full rounded-xl bg-indigo-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
{{ $isProgramme ? 'Create programme' : 'Create event' }}
|
||||
</button>
|
||||
{{-- Step 3: Review & create (events only) --}}
|
||||
<div x-show="wizard && step === 3" x-cloak class="space-y-5">
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||||
<div class="border-b border-slate-100 bg-slate-50/70 px-5 py-4">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Review your event</h2>
|
||||
<p class="mt-0.5 text-xs text-slate-400">Confirm details before publishing. GHS {{ number_format($pricePerQr, 2) }} will be charged from your QR balance.</p>
|
||||
</div>
|
||||
<div class="space-y-3 p-5 text-sm">
|
||||
<div class="flex justify-between gap-4 border-b border-slate-100 pb-3">
|
||||
<span class="text-slate-500">Event</span>
|
||||
<span class="font-semibold text-slate-900 text-right" x-text="reviewEventName()"></span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 border-b border-slate-100 pb-3">
|
||||
<span class="text-slate-500">Admin label</span>
|
||||
<span class="font-semibold text-slate-900 text-right" x-text="reviewLabel()"></span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<span class="text-slate-500">QR balance after</span>
|
||||
<span class="font-semibold text-slate-900">GHS <span x-text="Math.max(0, balance - price).toFixed(2)"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:hidden">
|
||||
@include('qr-codes.partials.qr-preview-card', ['showDownloads' => false])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Desktop navigation --}}
|
||||
<div class="hidden gap-3 lg:flex">
|
||||
<button type="button" x-show="wizard && step > 1" x-cloak @click="prevStep()"
|
||||
class="rounded-xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">
|
||||
Back
|
||||
</button>
|
||||
<button type="button" x-show="wizard && step < 3" x-cloak @click="nextStep()"
|
||||
class="flex-1 rounded-xl bg-indigo-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
|
||||
Continue
|
||||
</button>
|
||||
<button type="button" x-show="!wizard || step === 3" @click="wizardSubmit($event)"
|
||||
class="flex-1 rounded-xl bg-indigo-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
|
||||
{{ $isProgramme ? 'Create programme' : 'Create event' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -166,8 +227,11 @@
|
||||
<div x-show="!showPreviewModal"
|
||||
class="mobile-action-bar lg:hidden">
|
||||
<div class="mobile-action-bar__toolbar">
|
||||
<button type="button"
|
||||
@click="showPreviewModal = true"
|
||||
<button type="button" x-show="wizard && step > 1" x-cloak @click="prevStep()"
|
||||
class="rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-semibold text-slate-600">
|
||||
Back
|
||||
</button>
|
||||
<button type="button" x-show="wizard && (step === 2 || step === 3)" @click="showPreviewModal = true"
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-xl border border-slate-200 bg-slate-50 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
|
||||
@@ -175,8 +239,11 @@
|
||||
</svg>
|
||||
Preview
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="submitOrTopup($event)"
|
||||
<button type="button" x-show="wizard && step < 3" x-cloak @click="nextStep()"
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-indigo-600 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
|
||||
Continue
|
||||
</button>
|
||||
<button type="button" x-show="!wizard || step === 3" @click="wizardSubmit($event)"
|
||||
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-indigo-600 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
|
||||
Create
|
||||
</button>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -393,7 +393,7 @@
|
||||
removeField(i) { this.badgeFields.splice(i, 1); }
|
||||
}">
|
||||
<div class="grid gap-3">
|
||||
<input type="text" name="name" value="{{ old('name') }}" placeholder="Event name *"
|
||||
<input type="text" name="name" value="{{ old('name') }}" placeholder="Event name *" data-event-name
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<input type="text" name="tagline" value="{{ old('tagline') }}" placeholder="Tagline (e.g. Africa's biggest dev conference)"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
|
||||
@@ -597,11 +597,20 @@
|
||||
</div>
|
||||
|
||||
@if (Route::has('events.attendees'))
|
||||
<a href="{{ route('events.attendees', $qrCode) }}"
|
||||
class="flex items-center justify-center gap-2 rounded-xl border border-indigo-200 bg-indigo-50 px-4 py-3 text-sm font-semibold text-indigo-700 hover:bg-indigo-100 transition">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg>
|
||||
Manage attendees & print badges
|
||||
</a>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<a href="{{ route('events.attendees', $qrCode) }}"
|
||||
class="flex items-center justify-center gap-2 rounded-xl border border-indigo-200 bg-indigo-50 px-4 py-3 text-sm font-semibold text-indigo-700 hover:bg-indigo-100 transition">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg>
|
||||
Manage attendees
|
||||
</a>
|
||||
@if (Route::has('events.badge-printing') && ($c['mode'] ?? 'ticketing') !== 'contributions')
|
||||
<a href="{{ route('events.badge-printing', $qrCode) }}"
|
||||
class="flex items-center justify-center gap-2 rounded-xl border border-violet-200 bg-violet-50 px-4 py-3 text-sm font-semibold text-violet-700 hover:bg-violet-100 transition">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25Z"/></svg>
|
||||
Print badges
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user