Donation checkout via Ladill Pay (9% fee), church/giving QR pages, SSO, and platform scan forwarding. Co-authored-by: Cursor <cursoragent@cursor.com>
344 lines
20 KiB
PHP
344 lines
20 KiB
PHP
<x-user-layout>
|
||
<x-slot name="title">{{ $qrCode->label }}</x-slot>
|
||
|
||
@php
|
||
$qrStyle = $qrCode->style();
|
||
@endphp
|
||
|
||
<div class="mx-auto max-w-6xl space-y-6"
|
||
x-data="qrCustomizer({
|
||
previewUrl: @js(route('events.style-preview')),
|
||
csrf: @js(csrf_token()),
|
||
canonicalSyncUrl: @js(route('events.canonical-image', $qrCode)),
|
||
shortCode: @js($qrCode->short_code),
|
||
qrData: @js($qrCode->encodedPayload()),
|
||
initialPreview: @js($previewDataUri),
|
||
existingLogoPath: @js($qrStyle['logo_path'] ?? null),
|
||
existingLogoUrl: @js($logoDataUri ?? null),
|
||
hasExistingLogo: @js(! empty($qrStyle['logo_path'])),
|
||
style: @js(\App\Support\Qr\QrStyleDefaults::merge($qrStyle)),
|
||
})">
|
||
|
||
@foreach(['success', 'error'] as $flash)
|
||
@if(session($flash))
|
||
<div class="rounded-xl border px-4 py-3 text-sm {{ $flash === 'success' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-red-200 bg-red-50 text-red-700' }}">
|
||
{{ session($flash) }}
|
||
</div>
|
||
@endif
|
||
@endforeach
|
||
|
||
{{-- Mobile page header --}}
|
||
<div class="flex items-center gap-3 py-1 lg:hidden">
|
||
<a href="{{ route('events.index') }}"
|
||
class="flex h-9 w-9 items-center justify-center rounded-xl border border-slate-200 bg-white shadow-sm text-slate-500 hover:text-slate-900 transition">
|
||
<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="M6 18 18 6M6 6l12 12"/>
|
||
</svg>
|
||
</a>
|
||
<span class="min-w-0 flex-1 truncate text-sm font-semibold text-slate-900">{{ $qrCode->label }}</span>
|
||
</div>
|
||
|
||
{{-- Desktop page header --}}
|
||
<div class="hidden lg:block">
|
||
<a href="{{ route('events.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>
|
||
QR Codes
|
||
</a>
|
||
<h1 class="mt-1.5 text-2xl font-bold text-slate-900">{{ $qrCode->label }}</h1>
|
||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {{ $qrCode->is_active ? 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200' : 'bg-slate-100 text-slate-500' }}">
|
||
{{ $qrCode->is_active ? 'Active' : 'Paused' }}
|
||
</span>
|
||
<span class="text-xs text-slate-400">{{ $qrCode->typeLabel() }}</span>
|
||
<code class="rounded-lg bg-slate-100 px-2 py-0.5 text-xs text-slate-600">{{ $qrCode->publicUrl() }}</code>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Main 2-column layout --}}
|
||
<form x-ref="updateForm" action="{{ route('events.update', $qrCode) }}" method="POST" enctype="multipart/form-data"
|
||
class="grid grid-cols-1 gap-8 lg:grid-cols-[380px_1fr]">
|
||
@csrf
|
||
@method('PATCH')
|
||
|
||
{{-- Left: Sticky preview + downloads (desktop only) --}}
|
||
<div class="hidden lg:block lg:sticky lg:top-6 lg:self-start">
|
||
@include('qr-codes.partials.qr-preview-card', [
|
||
'qrCode' => $qrCode,
|
||
'previewDataUri' => $previewDataUri,
|
||
'showDownloads' => true,
|
||
])
|
||
</div>
|
||
|
||
{{-- Right: Controls --}}
|
||
<div class="space-y-5 pb-4 lg:pb-0">
|
||
|
||
{{-- Content first --}}
|
||
<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">Content</h2>
|
||
<p class="mt-0.5 text-xs text-slate-400">
|
||
@if($qrCode->type === \App\Models\QrCode::TYPE_WIFI)
|
||
Auto-join is locked to the network set when this code was created. Editing updates your saved info but won't change the printed code.
|
||
@else
|
||
Edits are free. The printed QR stays the same.
|
||
@endif
|
||
</p>
|
||
</div>
|
||
<div class="space-y-4 p-5">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-600">Label</label>
|
||
<input type="text" name="label" value="{{ $qrCode->label }}"
|
||
class="mt-1.5 w-full rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||
</div>
|
||
@include('qr-codes.partials.type-fields-edit')
|
||
<label class="flex items-center gap-2.5 text-sm text-slate-700">
|
||
<input type="checkbox" name="is_active" value="1" @checked($qrCode->is_active)
|
||
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||
QR is active
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Customizer --}}
|
||
@include('qr-codes.partials.customization-fields', [
|
||
'style' => $qrStyle,
|
||
'moduleStyles' => $moduleStyles,
|
||
'cornerOuterStyles' => $cornerOuterStyles,
|
||
'cornerInnerStyles' => $cornerInnerStyles,
|
||
'frameStyles' => $frameStyles,
|
||
'showRemoveLogo' => ! empty($qrStyle['logo_path']),
|
||
])
|
||
|
||
{{-- Desktop save button --}}
|
||
<button type="submit"
|
||
class="hidden lg:block w-full rounded-xl bg-slate-900 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-700 focus:ring-offset-2">
|
||
Save changes
|
||
</button>
|
||
|
||
</div>
|
||
</form>
|
||
|
||
{{-- Mobile sticky action bar --}}
|
||
<div x-show="!showPreviewModal"
|
||
class="mobile-action-bar lg:hidden">
|
||
<div class="mobile-action-bar__toolbar">
|
||
<button type="button"
|
||
@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"/>
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
|
||
</svg>
|
||
Preview
|
||
</button>
|
||
<button type="button"
|
||
@click="$refs.updateForm?.requestSubmit()"
|
||
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-slate-900 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700">
|
||
Save
|
||
</button>
|
||
</div>
|
||
<div class="mobile-action-bar__inset-fill" aria-hidden="true"></div>
|
||
</div>
|
||
|
||
{{-- Preview modal --}}
|
||
<div x-show="showPreviewModal" x-cloak
|
||
x-transition:enter="transition-opacity duration-200"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
x-transition:leave="transition-opacity duration-150"
|
||
x-transition:leave-start="opacity-100"
|
||
x-transition:leave-end="opacity-0"
|
||
@keydown.escape.window="showPreviewModal = false"
|
||
class="fixed inset-0 z-[60] flex flex-col bg-black/60 backdrop-blur-sm lg:hidden"
|
||
style="padding-bottom: env(safe-area-inset-bottom, 0px)">
|
||
<div class="flex items-center justify-between px-5 py-4">
|
||
<span class="text-sm font-semibold text-white">Preview</span>
|
||
<button type="button" @click="showPreviewModal = false"
|
||
class="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-white hover:bg-white/30 transition">
|
||
<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="M6 18 18 6M6 6l12 12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="flex flex-1 items-center justify-center p-8">
|
||
<div class="w-full max-w-xs overflow-hidden rounded-2xl bg-white shadow-2xl"
|
||
:style="frameStyle === 'thin'
|
||
? 'box-shadow: 0 0 0 2px ' + frameColor + ', 0 25px 50px -12px rgb(0 0 0 / 0.25)'
|
||
: 'box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25), 0 0 0 1px rgb(0 0 0 / 0.06)'">
|
||
<div :class="{
|
||
'p-[8px]': frameStyle === 'thin',
|
||
'p-[14px]': frameStyle === 'scan_me' || frameStyle === 'tap_to_scan',
|
||
'p-4': frameStyle === 'none',
|
||
}">
|
||
<div x-ref="qrPreviewModal"
|
||
class="aspect-square w-full [&>svg]:block [&>svg]:h-full [&>svg]:w-full"></div>
|
||
<div x-show="frameStyle === 'scan_me'" x-cloak
|
||
:style="'border-top: 1px solid ' + frameColor + '40; color: ' + frameColor"
|
||
class="pt-2.5 pb-0.5 text-center">
|
||
<span x-text="(frameText && frameText.trim() ? frameText : 'SCAN ME').toUpperCase()"
|
||
class="text-[11px] font-bold tracking-[0.18em]"></span>
|
||
</div>
|
||
<div x-show="frameStyle === 'tap_to_scan'" x-cloak class="mt-2.5 flex justify-center pb-0.5">
|
||
<span x-text="(frameText && frameText.trim() ? frameText : 'TAP TO SCAN').toUpperCase()"
|
||
:style="'background-color: ' + frameColor + '; color: ' + frameTextColor"
|
||
class="rounded-full px-5 py-1.5 text-[10px] font-bold tracking-widest"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{-- Downloads pinned to bottom of modal --}}
|
||
@php
|
||
$mobileShareUrl = $qrCode->publicUrl();
|
||
$mobileShareText = urlencode($qrCode->label . ' — scan my QR code');
|
||
$mobileShareEnc = urlencode($mobileShareUrl);
|
||
@endphp
|
||
<div class="grid grid-cols-4 gap-2 px-6 pb-6">
|
||
<a href="{{ route('events.download', [$qrCode, 'png']) }}"
|
||
class="flex flex-col items-center gap-1 rounded-xl border border-white/20 bg-white/10 py-3 text-center text-white transition hover:bg-white/20">
|
||
<span class="text-xs font-semibold">PNG</span>
|
||
</a>
|
||
<a href="{{ route('events.download', [$qrCode, 'svg']) }}"
|
||
class="flex flex-col items-center gap-1 rounded-xl border border-white/20 bg-white/10 py-3 text-center text-white transition hover:bg-white/20">
|
||
<span class="text-xs font-semibold">SVG</span>
|
||
</a>
|
||
<a href="{{ route('events.download', [$qrCode, 'pdf']) }}"
|
||
class="flex flex-col items-center gap-1 rounded-xl border border-white/20 bg-white/10 py-3 text-center text-white transition hover:bg-white/20">
|
||
<span class="text-xs font-semibold">PDF</span>
|
||
</a>
|
||
<button type="button"
|
||
@click="if(navigator.share){navigator.share({title:@js($qrCode->label),url:@js($mobileShareUrl)}).catch(()=>{})}else{window.open('https://wa.me/?text={{ $mobileShareText }}%20{{ $mobileShareEnc }}','_blank')}"
|
||
class="flex flex-col items-center gap-1 rounded-xl border border-sky-400/40 bg-sky-500/20 py-3 text-center text-sky-200 transition hover:bg-sky-500/30">
|
||
<span class="text-xs font-semibold">Share</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Orders (Book / Menu / Shop) --}}
|
||
@if(isset($orders) && !is_null($orders))
|
||
<div class="mt-10 space-y-4">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-base font-semibold text-slate-900">Orders</h2>
|
||
<span class="text-xs text-slate-400">{{ $orders->count() }} paid</span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<p class="text-2xl font-bold text-slate-900">{{ $orders->count() }}</p>
|
||
<p class="mt-0.5 text-xs text-slate-400">Total orders</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<p class="text-2xl font-bold text-slate-900">GHS {{ number_format((float) $orders->sum('amount_ghs'), 2) }}</p>
|
||
<p class="mt-0.5 text-xs text-slate-400">Revenue earned</p>
|
||
</div>
|
||
</div>
|
||
|
||
@if($orders->isEmpty())
|
||
<div class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center">
|
||
<p class="text-sm text-slate-400">No paid orders yet.</p>
|
||
</div>
|
||
@else
|
||
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||
<table class="w-full">
|
||
<thead>
|
||
<tr class="border-b border-slate-100 bg-slate-50/70">
|
||
<th class="px-5 py-3 text-left text-xs font-semibold text-slate-500">Customer</th>
|
||
<th class="hidden px-5 py-3 text-left text-xs font-semibold text-slate-500 sm:table-cell">Items</th>
|
||
<th class="px-5 py-3 text-right text-xs font-semibold text-slate-500">Amount</th>
|
||
<th class="px-5 py-3 text-right text-xs font-semibold text-slate-500">Date</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-100">
|
||
@foreach($orders as $order)
|
||
<tr class="transition-colors hover:bg-slate-50/50">
|
||
<td class="px-5 py-3.5">
|
||
<p class="text-sm font-medium text-slate-900">{{ $order->customer_name ?: '—' }}</p>
|
||
<p class="text-xs text-slate-400">{{ $order->customer_email ?: '' }}</p>
|
||
</td>
|
||
<td class="hidden px-5 py-3.5 text-sm text-slate-600 sm:table-cell">
|
||
{{ collect($order->items)->map(fn($i) => ($i['qty'] > 1 ? $i['qty'].'× ' : '').$i['name'])->join(', ') }}
|
||
</td>
|
||
<td class="px-5 py-3.5 text-right text-sm font-semibold text-slate-900 whitespace-nowrap">
|
||
GHS {{ number_format((float) $order->amount_ghs, 2) }}
|
||
</td>
|
||
<td class="px-5 py-3.5 text-right text-xs text-slate-400 whitespace-nowrap">
|
||
{{ $order->paid_at?->format('M j, Y') }}
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Scan analytics --}}
|
||
<div class="mt-10 space-y-5">
|
||
<h2 class="text-base font-semibold text-slate-900">Analytics</h2>
|
||
|
||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['total_scans']) }}</p>
|
||
<p class="mt-0.5 text-xs text-slate-400">Total scans</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['unique_scans']) }}</p>
|
||
<p class="mt-0.5 text-xs text-slate-400">Unique scans</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['scans_7d']) }}</p>
|
||
<p class="mt-0.5 text-xs text-slate-400">Last 7 days</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<p class="text-2xl font-bold text-slate-900">{{ number_format($summary['scans_30d']) }}</p>
|
||
<p class="mt-0.5 text-xs text-slate-400">Last 30 days</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-6 shadow-sm">
|
||
<h3 class="text-sm font-semibold text-slate-900">Scans — last 30 days</h3>
|
||
@php $maxDaily = max(1, $dailyScans->max('total')); @endphp
|
||
<div class="mt-5 flex h-28 items-end gap-px">
|
||
@foreach($dailyScans as $day)
|
||
<div class="group relative flex flex-1 flex-col items-center justify-end"
|
||
title="{{ $day->date }}: {{ $day->total }}">
|
||
<div class="w-full rounded-t-sm bg-indigo-400/70 transition-colors group-hover:bg-indigo-600"
|
||
style="height: {{ max(2, ($day->total / $maxDaily) * 100) }}%"></div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid gap-4 sm:grid-cols-2">
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<h3 class="text-sm font-semibold text-slate-900">Devices</h3>
|
||
<ul class="mt-4 space-y-2.5">
|
||
@forelse($devices as $row)
|
||
<li class="flex items-center justify-between text-sm">
|
||
<span class="text-slate-600">{{ ucfirst($row['label']) }}</span>
|
||
<span class="font-semibold text-slate-900">{{ $row['total'] }}</span>
|
||
</li>
|
||
@empty
|
||
<li class="text-sm text-slate-400">No scans yet</li>
|
||
@endforelse
|
||
</ul>
|
||
</div>
|
||
<div class="rounded-2xl border border-slate-200/80 bg-white p-5 shadow-sm">
|
||
<h3 class="text-sm font-semibold text-slate-900">Browsers</h3>
|
||
<ul class="mt-4 space-y-2.5">
|
||
@forelse($browsers as $row)
|
||
<li class="flex items-center justify-between text-sm">
|
||
<span class="text-slate-600">{{ $row['label'] }}</span>
|
||
<span class="font-semibold text-slate-900">{{ $row['total'] }}</span>
|
||
</li>
|
||
@empty
|
||
<li class="text-sm text-slate-400">No scans yet</li>
|
||
@endforelse
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</x-user-layout>
|