Add self-service kiosk for queue ticket issuance.
Deploy Ladill Queue / deploy (push) Successful in 36s
Deploy Ladill Queue / deploy (push) Successful in 36s
Customers can pick a service, optionally enter details, and receive a ticket on a branded touchscreen flow with admin-configurable queues and auto-reset. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,19 +1,89 @@
|
||||
<x-app-layout title="Register device">
|
||||
<div class="mx-auto max-w-lg">
|
||||
<form method="POST" action="{{ route('qms.devices.store') }}" class="space-y-4 rounded-2xl border bg-white p-6">
|
||||
<form
|
||||
method="POST"
|
||||
action="{{ route('qms.devices.store') }}"
|
||||
class="space-y-4 rounded-2xl border bg-white p-6"
|
||||
x-data="{ type: '{{ old('type', 'kiosk') }}' }"
|
||||
>
|
||||
@csrf
|
||||
<div><label class="block text-sm font-medium">Name</label><input name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||
<div><label class="block text-sm font-medium">Type</label>
|
||||
<select name="type" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceTypes as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Name</label>
|
||||
<input name="name" value="{{ old('name') }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Type</label>
|
||||
<select name="type" x-model="type" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceTypes as $k => $v)
|
||||
<option value="{{ $k }}" @selected(old('type', 'kiosk') === $k)>{{ $v }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div><label class="block text-sm font-medium">Branch</label>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Branch</label>
|
||||
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">—</option>
|
||||
@foreach ($branches as $b)<option value="{{ $b->id }}">{{ $b->name }}</option>@endforeach
|
||||
@foreach ($branches as $b)
|
||||
<option value="{{ $b->id }}" @selected((string) old('branch_id') === (string) $b->id)>{{ $b->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500">Optional. Limits which queues appear on this kiosk.</p>
|
||||
</div>
|
||||
|
||||
<div x-show="type === 'kiosk'" x-cloak class="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-900">Kiosk queues</p>
|
||||
<p class="mt-1 text-xs text-slate-500">Leave unchecked to show all active queues for the branch.</p>
|
||||
<div class="mt-3 space-y-2">
|
||||
@foreach ($queues as $q)
|
||||
<label class="flex gap-2 text-sm text-slate-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="queue_ids[]"
|
||||
value="{{ $q->id }}"
|
||||
@checked(in_array($q->id, old('queue_ids', [])))
|
||||
>
|
||||
{{ $q->name }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Welcome message</label>
|
||||
<input
|
||||
name="welcome_message"
|
||||
value="{{ old('welcome_message') }}"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm"
|
||||
placeholder="Tap a service below to get your ticket"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" name="collect_name" value="1" @checked(old('collect_name'))>
|
||||
Ask for name
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" name="collect_phone" value="1" @checked(old('collect_phone'))>
|
||||
Ask for phone
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Auto-reset (seconds)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="reset_seconds"
|
||||
min="5"
|
||||
max="120"
|
||||
value="{{ old('reset_seconds', 15) }}"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm"
|
||||
>
|
||||
<p class="mt-1 text-xs text-slate-500">Return to the welcome screen after a ticket is issued.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,50 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full bg-slate-900">
|
||||
<html lang="en" class="h-full overflow-hidden bg-slate-50 font-sans">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Kiosk · {{ $device->name }}</title>
|
||||
<meta name="theme-color" content="#f8fafc">
|
||||
<title>{{ $device->name }} · Queue Kiosk</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet">
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center p-6 text-white" x-data="kioskFlow({ issueUrl: '{{ route('qms.kiosk.device.issue', $device->device_token) }}', csrf: '{{ csrf_token() }}' })">
|
||||
<div class="w-full max-w-lg">
|
||||
<h1 class="mb-8 text-center text-3xl font-bold">{{ $device->name }}</h1>
|
||||
|
||||
<template x-if="step === 'select'">
|
||||
<div class="space-y-3">
|
||||
<p class="mb-4 text-center text-slate-300">Select a service</p>
|
||||
@foreach ($queues as $queue)
|
||||
<button type="button" @click="selectQueue({{ $queue->id }})"
|
||||
class="w-full rounded-2xl border border-slate-600 bg-slate-800 px-6 py-5 text-left text-xl font-semibold hover:border-indigo-400"
|
||||
style="border-left: 6px solid {{ $queue->color }}">
|
||||
{{ $queue->name }}
|
||||
</button>
|
||||
@endforeach
|
||||
<body
|
||||
class="qms-kiosk font-sans text-slate-900"
|
||||
x-data="kioskFlow(@js([
|
||||
'issueUrl' => route('qms.kiosk.device.issue', $device->device_token),
|
||||
'csrf' => csrf_token(),
|
||||
'queues' => $queues->map(fn ($q) => [
|
||||
'id' => $q->id,
|
||||
'name' => $q->name,
|
||||
'prefix' => $q->prefix,
|
||||
'color' => $q->color,
|
||||
])->values()->all(),
|
||||
'collectName' => $settings['collect_name'],
|
||||
'collectPhone' => $settings['collect_phone'],
|
||||
'resetSeconds' => $settings['reset_seconds'],
|
||||
'welcomeMessage' => $settings['welcome_message'] ?? 'Tap a service below to get your ticket',
|
||||
]))"
|
||||
x-init="init()"
|
||||
>
|
||||
<div class="qms-kiosk__shell">
|
||||
<header class="shrink-0 border-b border-slate-200/80 bg-white/90 px-5 py-4 backdrop-blur-md lg:px-8">
|
||||
<div class="mx-auto flex max-w-3xl items-center justify-between gap-4">
|
||||
<img
|
||||
src="{{ $logoUrl }}"
|
||||
alt="{{ $logoAlt }}"
|
||||
class="h-8 w-auto max-w-[min(100%,220px)] object-contain object-left lg:h-10"
|
||||
>
|
||||
<p class="text-right text-sm font-medium text-slate-500">{{ $device->name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<template x-if="step === 'details'">
|
||||
<div class="rounded-2xl bg-slate-800 p-6 space-y-4">
|
||||
<input type="text" x-model="customerName" placeholder="Your name (optional)" class="w-full rounded-lg border-slate-600 bg-slate-900 px-4 py-3 text-white">
|
||||
<input type="tel" x-model="customerPhone" placeholder="Phone (optional)" class="w-full rounded-lg border-slate-600 bg-slate-900 px-4 py-3 text-white">
|
||||
<p x-show="error" class="text-red-400 text-sm" x-text="error"></p>
|
||||
<div class="flex gap-3">
|
||||
<button type="button" @click="reset()" class="flex-1 rounded-lg border border-slate-600 py-3">Back</button>
|
||||
<button type="button" @click="issue()" :disabled="loading" class="flex-1 rounded-lg bg-indigo-600 py-3 font-semibold">Get ticket</button>
|
||||
<main class="qms-kiosk__main px-5 py-6 lg:px-8">
|
||||
<div class="w-full max-w-2xl">
|
||||
{{-- Queue selection --}}
|
||||
<div x-show="step === 'select'" x-cloak>
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-slate-900 lg:text-3xl">Welcome</h1>
|
||||
<p class="mt-2 text-base text-slate-500" x-text="welcomeMessage"></p>
|
||||
</div>
|
||||
|
||||
<template x-if="queues.length === 1">
|
||||
<div class="mt-8">
|
||||
<button
|
||||
type="button"
|
||||
class="qms-kiosk__ticket-card w-full transition hover:ring-2 hover:ring-indigo-200"
|
||||
:disabled="loading"
|
||||
@click="selectQueue(queues[0].id)"
|
||||
>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">Get ticket for</p>
|
||||
<p class="mt-3 text-2xl font-semibold text-slate-900" x-text="queues[0].name"></p>
|
||||
<p class="mt-6 text-sm font-semibold text-indigo-600" x-show="!loading">Tap here to continue</p>
|
||||
<p class="mt-6 text-sm font-semibold text-indigo-600" x-show="loading">Issuing your ticket…</p>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="queues.length > 1">
|
||||
<div class="qms-kiosk__queue-grid mt-8">
|
||||
<template x-for="queue in queues" :key="queue.id">
|
||||
<button
|
||||
type="button"
|
||||
class="qms-kiosk__queue-btn"
|
||||
:disabled="loading"
|
||||
@click="selectQueue(queue.id)"
|
||||
>
|
||||
<span
|
||||
class="qms-kiosk__queue-mark"
|
||||
:style="'background-color:' + (queue.color || '#4f46e5')"
|
||||
x-text="queue.prefix"
|
||||
></span>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-lg font-semibold text-slate-900" x-text="queue.name"></span>
|
||||
<span class="mt-0.5 block text-sm text-slate-500">Tap to get ticket</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="queues.length === 0">
|
||||
<div class="qms-kiosk__ticket-card mt-8">
|
||||
<p class="text-lg font-semibold text-slate-900">No services available</p>
|
||||
<p class="mt-2 text-sm text-slate-500">Ask a staff member for assistance.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p x-show="error" class="mt-4 text-center text-sm text-rose-600" x-text="error"></p>
|
||||
</div>
|
||||
|
||||
{{-- Optional details --}}
|
||||
<div x-show="step === 'details'" x-cloak>
|
||||
<div class="qms-kiosk__ticket-card">
|
||||
<h2 class="text-xl font-semibold text-slate-900">Your details</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">Optional information to help staff assist you.</p>
|
||||
|
||||
<div class="mt-6 space-y-4 text-left">
|
||||
<div x-show="collectName">
|
||||
<label class="block text-sm font-medium text-slate-700">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="customerName"
|
||||
class="mt-1 w-full rounded-xl border-slate-300 text-base shadow-sm"
|
||||
placeholder="Your name"
|
||||
autocomplete="name"
|
||||
>
|
||||
</div>
|
||||
<div x-show="collectPhone">
|
||||
<label class="block text-sm font-medium text-slate-700">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
x-model="customerPhone"
|
||||
class="mt-1 w-full rounded-xl border-slate-300 text-base shadow-sm"
|
||||
placeholder="Phone number"
|
||||
autocomplete="tel"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p x-show="error" class="mt-4 text-sm text-rose-600" x-text="error"></p>
|
||||
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button type="button" class="btn-secondary flex-1" @click="reset()" :disabled="loading">Back</button>
|
||||
<button type="button" class="btn-primary flex-1" @click="issue()" :disabled="loading">
|
||||
<span x-show="!loading">Get ticket</span>
|
||||
<span x-show="loading">Please wait…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Ticket issued --}}
|
||||
<div x-show="step === 'ticket' && ticket" x-cloak>
|
||||
<div class="qms-kiosk__ticket-card">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">Your ticket number</p>
|
||||
<p class="qms-kiosk__ticket-number mt-4" x-text="ticket?.ticket_number"></p>
|
||||
|
||||
<div class="mt-6 space-y-2 text-sm text-slate-600">
|
||||
<p x-show="ticket?.position">
|
||||
<span class="font-medium text-slate-900" x-text="ticket?.position"></span>
|
||||
<span> ahead of you</span>
|
||||
</p>
|
||||
<p x-show="formatWait(ticket?.estimated_wait_seconds)" x-text="formatWait(ticket?.estimated_wait_seconds)"></p>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 text-base text-slate-600">Please take a seat. Your number will be called on the display.</p>
|
||||
|
||||
<button type="button" class="btn-primary mt-8 w-full" @click="reset()">
|
||||
Done
|
||||
<span x-show="resetCountdown" x-text="'(' + resetCountdown + 's)'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template x-if="step === 'ticket' && ticket">
|
||||
<div class="rounded-2xl bg-indigo-600 p-8 text-center">
|
||||
<p class="text-sm uppercase tracking-widest opacity-80">Your number</p>
|
||||
<p class="mt-2 text-6xl font-bold" x-text="ticket.ticket_number"></p>
|
||||
<p class="mt-4 text-xl" x-text="ticket.queue?.name"></p>
|
||||
<p class="mt-6 text-lg" x-text="ticket.estimated_wait_seconds ? 'Est. wait: ' + Math.ceil(ticket.estimated_wait_seconds / 60) + ' mins' : ''"></p>
|
||||
<button type="button" @click="reset()" class="mt-8 rounded-lg bg-white/20 px-6 py-3">Done</button>
|
||||
<footer class="shrink-0 border-t border-slate-200 bg-white/80 px-5 py-3 lg:px-8">
|
||||
<div class="mx-auto flex max-w-3xl items-center justify-center gap-2">
|
||||
<span class="text-[10px] font-medium uppercase tracking-wider text-slate-400">Powered by</span>
|
||||
<img src="{{ $poweredByLogoUrl }}" alt="Ladill Queue" class="h-3.5 w-auto opacity-80">
|
||||
</div>
|
||||
</template>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user