Deploy Ladill Events / deploy (push) Successful in 48s
Mirror Care: suite platform keys count as ready for attendee email/SMS; Account Messaging is primary, product Bird/SMS keys are advanced. SmsService now prefers channel=suite before customer API keys.
287 lines
10 KiB
PHP
287 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();
|
|
|
|
$events = $account->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,
|
|
'messagingSettingsUrl' => function_exists('ladill_account_url')
|
|
? ladill_account_url('/account/settings/messaging')
|
|
: (string) config('smtp.messaging_settings_url', 'https://account.ladill.com/account/settings/messaging'),
|
|
]);
|
|
}
|
|
|
|
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['integrations_error'] ?? null) {
|
|
return back()->with('error', $preview['integrations_error']);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|