Extract Ladill Events as a standalone events suite at events.ladill.com.
Deploy Ladill QR Plus / deploy (push) Successful in 28s

Full control center for ticketed events, contributions, attendees, badges,
and programmes — not a QR utility clone. Includes SSO shell, import command,
and platform cutover runbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-07 09:39:53 +00:00
co-authored by Cursor
commit d8dbc83e2d
246 changed files with 34279 additions and 0 deletions
@@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Events;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use App\Notifications\EventProgrammeSharedNotification;
use App\Services\Billing\SmsService;
use App\Support\Events\EventBadgeZpl;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Notification;
use Illuminate\View\View;
class AttendeeController extends Controller
{
public function __construct(private readonly SmsService $sms) {}
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,
];
$programme = $this->programmeFor($event);
return view('events.attendees', [
'qrCode' => $event,
'event' => $event,
'registrations' => $registrations,
'stats' => $stats,
'programme' => $programme,
]);
}
public function shareProgramme(QrCode $event): RedirectResponse
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$programme = $this->programmeFor($event);
if (! $programme) {
return back()->with('error', 'No programme outline is attached to this event yet.');
}
$recipients = $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->get();
$eventName = $event->content()['name'] ?? $event->label;
$programmeUrl = $programme->publicUrl();
$emailed = 0;
$texted = 0;
foreach ($recipients as $reg) {
if ($reg->attendee_email) {
Notification::route('mail', $reg->attendee_email)
->notify(new EventProgrammeSharedNotification($event, $programme, $reg->attendee_name));
$emailed++;
}
if ($reg->attendee_phone) {
$this->sms->send(
$reg->attendee_phone,
sprintf(
'Hi %s, the programme for %s is here: %s',
explode(' ', $reg->attendee_name ?? 'there')[0],
$eventName,
$programmeUrl
)
);
$texted++;
}
}
if ($emailed === 0 && $texted === 0) {
return back()->with('error', 'No confirmed attendees with contact details to share with yet.');
}
return back()->with('success', "Programme shared — {$emailed} email(s) and {$texted} SMS sent.");
}
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();
}
}