Files
ladill-mini/resources/views/qr-codes/show.blade.php
T
isaaccladandCursor db66d99895 Initial Ladill Mini app — trader payment QRs without styling.
Lean control center at mini.ladill.com: payment QR CRUD, Paystack checkout with 5% fee settlement via Billing API, payments feed, and payouts. QR codes use a fixed black-and-white preset only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 18:43:39 +00:00

344 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>