Add speaker invitations with portal page and programme host picker.
Deploy Ladill Events / deploy (push) Successful in 47s

Speakers require email, get a personal holding page with programme and stage links, auto-invites when programme hosts are saved, and manual send for events without a schedule; also fix wallet copy on event create and anchor attendee comms.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-03 15:48:32 +00:00
co-authored by Cursor
parent 42d997a599
commit fec24da7bd
20 changed files with 1015 additions and 36 deletions
@@ -667,19 +667,29 @@
@elseif($qrCode->type === \App\Models\QrCode::TYPE_ITINERARY)
@php
$itinDays = $c['days'] ?? [['label' => 'Day 1', 'date' => '', 'items' => [['time' => '', 'title' => '', 'description' => '', 'location' => '', 'host' => '']]]];
$itinDays = $c['days'] ?? [['label' => 'Day 1', 'date' => '', 'items' => [['time' => '', 'title' => '', 'description' => '', 'location' => '', 'host' => '', 'host_speaker_email' => '']]]];
foreach ($itinDays as &$itinDay) {
$itinDay['date'] = \App\Support\Qr\QrDateFormatter::forInput($itinDay['date'] ?? null);
foreach ($itinDay['items'] ?? [] as &$itinItem) {
$itinItem['host_speaker_email'] = $itinItem['host_speaker_email'] ?? '';
}
unset($itinItem);
}
unset($itinDay);
$eventSpeakers = $linkedEventSpeakers ?? [];
@endphp
<div class="space-y-4"
x-data="{
days: {{ Illuminate\Support\Js::from($itinDays) }},
addDay() { this.days.push({ label: 'Day ' + (this.days.length + 1), date: '', items: [{ time: '', title: '', description: '', location: '', host: '' }] }); },
speakers: {{ Illuminate\Support\Js::from($eventSpeakers) }},
addDay() { this.days.push({ label: 'Day ' + (this.days.length + 1), date: '', items: [{ time: '', title: '', description: '', location: '', host: '', host_speaker_email: '' }] }); },
removeDay(i) { this.days.splice(i, 1); },
addItem(d) { this.days[d].items.push({ time: '', title: '', description: '', location: '', host: '' }); },
removeItem(d, i) { this.days[d].items.splice(i, 1); }
addItem(d) { this.days[d].items.push({ time: '', title: '', description: '', location: '', host: '', host_speaker_email: '' }); },
removeItem(d, i) { this.days[d].items.splice(i, 1); },
onHostSpeakerChange(item) {
const match = this.speakers.find(s => s.email === item.host_speaker_email);
item.host = match ? match.name : item.host;
}
}">
<div class="grid gap-3">
<input type="text" name="title" value="{{ $c['title'] ?? '' }}" placeholder="Itinerary title *"
@@ -751,8 +761,22 @@
<div class="grid gap-2 sm:grid-cols-2">
<input type="text" :name="'days['+d+'][items]['+i+'][location]'" x-model="item.location" placeholder="Location (optional)"
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
<input type="text" :name="'days['+d+'][items]['+i+'][host]'" x-model="item.host" placeholder="Host / speaker (optional)"
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
<template x-if="speakers.length > 0">
<div class="space-y-1">
<select :name="'days['+d+'][items]['+i+'][host_speaker_email]'" x-model="item.host_speaker_email" @change="onHostSpeakerChange(item)"
class="w-full rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
<option value="">Choose speaker host (optional)</option>
<template x-for="speaker in speakers" :key="speaker.email">
<option :value="speaker.email" x-text="speaker.name + ' (' + speaker.email + ')'"></option>
</template>
</select>
<input type="hidden" :name="'days['+d+'][items]['+i+'][host]'" :value="item.host">
</div>
</template>
<template x-if="speakers.length === 0">
<input type="text" :name="'days['+d+'][items]['+i+'][host]'" x-model="item.host" placeholder="Host / speaker (optional)"
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
</template>
</div>
<textarea :name="'days['+d+'][items]['+i+'][description]'" x-model="item.description" rows="1" placeholder="Details (optional)"
class="w-full rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30"></textarea>