Ticket number
+ + +Now serving
+Please proceed to your counter when called
+Please wait
+Your ticket number will appear here when it is called.
+diff --git a/app/Http/Controllers/Qms/DisplayPublicController.php b/app/Http/Controllers/Qms/DisplayPublicController.php index 3f9a229..2ecb10c 100644 --- a/app/Http/Controllers/Qms/DisplayPublicController.php +++ b/app/Http/Controllers/Qms/DisplayPublicController.php @@ -19,6 +19,7 @@ class DisplayPublicController extends Controller public function show(string $token): View { $screen = $this->displays->findByToken($token) ?? abort(404); + $screen->loadMissing(['organization', 'branch']); $this->displays->touch($screen); return view('qms.display.public', [ diff --git a/app/Http/Controllers/Qms/DisplayScreenController.php b/app/Http/Controllers/Qms/DisplayScreenController.php index 5789834..6336b92 100644 --- a/app/Http/Controllers/Qms/DisplayScreenController.php +++ b/app/Http/Controllers/Qms/DisplayScreenController.php @@ -44,9 +44,23 @@ class DisplayScreenController extends Controller { $this->authorizeAbility($request, 'displays.view'); $this->authorizeOwner($request, $display); - $display->load('branch'); + $display->load(['branch', 'organization']); - return view('qms.displays.show', compact('display')); + $member = app(OrganizationResolver::class)->memberFor($request->user()); + $permissions = app(QmsPermissions::class); + + $queueIds = array_map('intval', $display->service_queue_ids ?? []); + $queues = ServiceQueue::query() + ->whereIn('id', $queueIds) + ->orderBy('name') + ->get(); + + return view('qms.displays.show', [ + 'display' => $display, + 'queues' => $queues, + 'publicUrl' => route('qms.display.public', $display->access_token), + 'canManage' => $permissions->can($member, 'displays.manage'), + ]); } public function create(Request $request): View diff --git a/app/Services/Qms/DisplayService.php b/app/Services/Qms/DisplayService.php index ea3fb76..5b09e41 100644 --- a/app/Services/Qms/DisplayService.php +++ b/app/Services/Qms/DisplayService.php @@ -26,6 +26,7 @@ class DisplayService */ public function payload(DisplayScreen $screen): array { + $screen->loadMissing(['branch', 'organization']); $queueIds = $screen->service_queue_ids ?? []; $queues = ServiceQueue::query() ->whereIn('id', $queueIds) @@ -62,6 +63,8 @@ class DisplayService 'screen' => [ 'name' => $screen->name, 'layout' => $screen->layout, + 'branch_name' => $screen->branch?->name, + 'organization_name' => $screen->organization?->name, ], 'now_serving' => $nowServing, 'waiting_count' => $waiting, diff --git a/resources/css/app.css b/resources/css/app.css index b7680e8..cb0d870 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -267,3 +267,53 @@ html.qr-mobile-page body { box-shadow: 0 1px 2px rgb(15 23 42 / 0.08); pointer-events: none; } + +/* Public queue display — waiting-area TV screens */ +.qms-display { + min-height: 100vh; + background: + radial-gradient(ellipse 80% 50% at 50% -10%, rgb(49 46 129 / 0.45), transparent), + linear-gradient(180deg, rgb(15 23 42) 0%, rgb(2 6 23) 100%); + color: #fff; +} + +.qms-display__grid-bg { + background-image: + linear-gradient(rgb(255 255 255 / 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgb(255 255 255 / 0.03) 1px, transparent 1px); + background-size: 48px 48px; +} + +.qms-display__ticket { + background: linear-gradient(145deg, rgb(67 56 202) 0%, rgb(79 70 229) 45%, rgb(99 102 241) 100%); + box-shadow: + 0 0 0 1px rgb(255 255 255 / 0.08), + 0 20px 40px -12px rgb(0 0 0 / 0.45); +} + +.qms-display__ticket-number { + font-variant-numeric: tabular-nums; + letter-spacing: -0.04em; + text-shadow: 0 2px 16px rgb(0 0 0 / 0.25); +} + +.qms-display__stat { + background: rgb(255 255 255 / 0.04); + border: 1px solid rgb(255 255 255 / 0.08); + backdrop-filter: blur(8px); +} + +.qms-display__live-dot { + animation: qms-display-pulse 2s ease-in-out infinite; +} + +@keyframes qms-display-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + +.qms-display__unlock { + background: + radial-gradient(ellipse 60% 40% at 50% 0%, rgb(79 70 229 / 0.35), transparent), + rgb(2 6 23 / 0.96); +} diff --git a/resources/js/kiosk-flow.js b/resources/js/kiosk-flow.js index e575392..dabfc82 100644 --- a/resources/js/kiosk-flow.js +++ b/resources/js/kiosk-flow.js @@ -99,6 +99,8 @@ export function registerKioskFlow(Alpine) { isAnnouncing: false, speechReady: false, voices: [], + clock: '', + dateLabel: '', dataUrl: typeof config === 'string' ? config : config.dataUrl, playedUrlTemplate: config.playedUrl ?? null, pollMs: config.pollMs ?? 1200, @@ -267,6 +269,8 @@ export function registerKioskFlow(Alpine) { init() { this.primeSpeech(); + this.updateClock(); + setInterval(() => this.updateClock(), 1000); try { if (sessionStorage.getItem(DISPLAY_AUDIO_KEY) === '1') { @@ -279,6 +283,16 @@ export function registerKioskFlow(Alpine) { this.poll(); setInterval(() => this.poll(), this.pollMs); }, + + updateClock() { + const now = new Date(); + this.clock = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + this.dateLabel = now.toLocaleDateString([], { + weekday: 'long', + month: 'long', + day: 'numeric', + }); + }, })); Alpine.data('mobileTracker', (pollUrl) => ({ diff --git a/resources/views/qms/display/public.blade.php b/resources/views/qms/display/public.blade.php index 52c9192..4f0d3a2 100644 --- a/resources/views/qms/display/public.blade.php +++ b/resources/views/qms/display/public.blade.php @@ -1,5 +1,5 @@ - +
@@ -7,58 +7,145 @@Tap to enable voice announcements
-Browsers require one tap before the display can speak ticket calls.
+Tap to enable announcements
+Your browser needs one interaction before this screen can call tickets aloud.
Waiting
- + {{-- Main --}} +Now serving
+Please proceed to your counter when called
+Ticket number
+ + +Please wait
+Your ticket number will appear here when it is called.
++ {{ $display->branch?->name ?? 'No branch' }} + · {{ $layoutLabel }} layout + @if ($display->last_seen_at) + · Last seen {{ $display->last_seen_at->diffForHumans() }} + @endif +
+Layout: {{ config('qms.display_layouts.'.$display->layout) }}
- + +Tickets from these queues appear on this screen and trigger voice calls.
+ + @if ($queues->isEmpty()) +No queues assigned
+Edit this display and select the service queues for your waiting area.
+ @if ($canManage) + Assign queues + @endif +{{ $queue->name }}
++ {{ $queue->is_paused ? 'Paused' : 'Accepting tickets' }} +
+Open this link on a TV, tablet, or kiosk in your waiting area.
+Live preview
+ + + Customer view + ++ Voice announcements require a tap on the display screen once per browser session. +
+