Restyle customer display to match Frontdesk kiosk UI.
Deploy Ladill POS / deploy (push) Successful in 41s

Use a light shell, soft indigo accents, and object-contain logos so
brand marks are not square-cropped on the dual-screen customer view.
This commit is contained in:
isaacclad
2026-07-15 16:31:23 +00:00
parent 600aedb59d
commit aed0722641
+100 -79
View File
@@ -14,7 +14,8 @@
html, body { overflow: hidden; }
</style>
</head>
<body class="h-full bg-slate-950 font-sans text-white antialiased"
{{-- Light kiosk shell aligned with Frontdesk reception kiosk. --}}
<body class="h-full bg-slate-100 font-sans text-slate-900 antialiased"
x-data="customerDisplay(@js([
'initial' => $state,
'stateUrl' => $stateUrl,
@@ -22,19 +23,29 @@
'actionUrl' => $actionUrl,
'csrf' => csrf_token(),
]))">
<div class="flex h-full min-h-dvh flex-col">
{{-- Brand bar --}}
<header class="flex items-center justify-between gap-4 border-b border-white/10 px-6 py-4 sm:px-10">
<div class="flex items-center gap-3 min-w-0">
<div class="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true">
<div class="absolute -left-24 top-0 h-72 w-72 rounded-full bg-indigo-200/40 blur-3xl"></div>
<div class="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-200/40 blur-3xl"></div>
</div>
<div class="fixed inset-x-0 top-0 z-20 h-1.5 bg-gradient-to-r from-indigo-600 to-violet-600"></div>
<div class="relative flex h-full min-h-dvh flex-col">
{{-- Brand bar logo uses object-contain (never square-crop) like Frontdesk kiosk-brand --}}
<header class="flex items-center justify-between gap-4 border-b border-slate-200/80 bg-white/80 px-6 py-4 backdrop-blur-sm sm:px-10">
<div class="flex min-w-0 items-center gap-3">
<template x-if="state.brand?.logo_url">
<img :src="state.brand.logo_url" alt="" class="h-10 w-10 rounded-xl object-cover bg-white/10">
<div class="flex shrink-0 items-center rounded-xl bg-white px-3 py-2 shadow-sm ring-1 ring-slate-200/80">
<img :src="state.brand.logo_url"
:alt="state.location_name || 'Store'"
class="h-9 w-auto max-w-[200px] object-contain object-left sm:h-10 sm:max-w-[240px]">
</div>
</template>
<div class="min-w-0">
<p class="truncate text-lg font-semibold tracking-tight" x-text="state.location_name"></p>
<p class="text-xs text-slate-400" x-show="state.brand?.header" x-text="state.brand.header"></p>
<p class="truncate text-lg font-semibold tracking-tight text-slate-900" x-text="state.location_name"></p>
<p class="truncate text-xs text-slate-500" x-show="state.brand?.header" x-text="state.brand.header"></p>
</div>
</div>
<div class="text-right text-xs text-slate-500">
<div class="shrink-0 text-right text-xs font-medium tabular-nums text-slate-500">
<span x-text="clock"></span>
</div>
</header>
@@ -44,74 +55,79 @@
<section x-show="isPhase('idle') || isPhase('promo')" x-cloak
class="flex flex-1 flex-col items-center justify-center gap-6 px-8 text-center">
<template x-if="state.promo?.image_url">
<img :src="state.promo.image_url" alt="" class="h-28 w-28 rounded-3xl object-cover bg-white/10 shadow-2xl">
<div class="flex items-center justify-center rounded-3xl bg-white px-8 py-6 shadow-sm ring-1 ring-slate-200/80">
<img :src="state.promo.image_url"
alt=""
class="h-24 w-auto max-h-28 max-w-[280px] object-contain sm:h-28 sm:max-w-[320px]">
</div>
</template>
<div>
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl" x-text="state.promo?.headline || state.location_name"></h1>
<p class="mx-auto mt-4 max-w-xl text-lg text-slate-300" x-text="state.promo?.body || 'Welcome'"></p>
<div class="max-w-xl">
<p class="text-sm font-semibold uppercase tracking-wider text-indigo-600">Welcome</p>
<h1 class="mt-3 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl"
x-text="state.promo?.headline || state.location_name"></h1>
<p class="mx-auto mt-4 text-lg text-slate-500" x-text="state.promo?.body || 'Thank you for shopping with us.'"></p>
</div>
<p class="text-sm text-slate-500">Your order will appear here</p>
<p class="text-sm text-slate-400">Your order will appear here</p>
</section>
{{-- CART + shared totals for payment / tip --}}
{{-- CART + payment / tip / signature --}}
<section x-show="isPhase('cart') || isPhase('payment') || isPhase('tip') || isPhase('signature')" x-cloak
class="flex flex-1 flex-col overflow-hidden lg:flex-row">
<div class="flex flex-1 flex-col overflow-hidden px-6 py-5 sm:px-10">
<div class="mb-3 flex items-end justify-between gap-3">
<div class="mb-4 flex items-end justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-300">Order summary</p>
<p class="text-sm text-slate-400" x-show="state.customer_name">
For <span class="text-white" x-text="state.customer_name"></span>
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Order summary</p>
<p class="mt-0.5 text-sm text-slate-500" x-show="state.customer_name">
For <span class="font-medium text-slate-800" x-text="state.customer_name"></span>
</p>
</div>
<p class="text-sm text-slate-500" x-show="state.sale_reference" x-text="state.sale_reference"></p>
<p class="text-sm tabular-nums text-slate-400" x-show="state.sale_reference" x-text="state.sale_reference"></p>
</div>
<ul class="flex-1 space-y-2 overflow-y-auto pr-1">
<template x-for="(line, i) in (state.lines || [])" :key="i">
<li class="flex items-start justify-between gap-4 rounded-2xl bg-white/5 px-4 py-3">
<li class="flex items-start justify-between gap-4 rounded-2xl border border-slate-200/80 bg-white px-4 py-3 shadow-sm">
<div class="min-w-0">
<p class="font-medium text-white">
<p class="font-medium text-slate-900">
<span class="text-slate-400" x-text="line.quantity + '×'"></span>
<span x-text="line.name"></span>
</p>
<p class="mt-0.5 text-xs text-slate-400" x-show="line.note" x-text="line.note"></p>
<p class="mt-0.5 text-xs text-emerald-300" x-show="line.discount_minor > 0"
<p class="mt-0.5 text-xs text-slate-500" x-show="line.note" x-text="line.note"></p>
<p class="mt-0.5 text-xs font-medium text-emerald-600" x-show="line.discount_minor > 0"
x-text="'Discount ' + money(line.discount_minor)"></p>
</div>
<p class="shrink-0 font-semibold tabular-nums" x-text="money(line.line_total_minor)"></p>
<p class="shrink-0 font-semibold tabular-nums text-slate-900" x-text="money(line.line_total_minor)"></p>
</li>
</template>
</ul>
<div class="mt-4 space-y-1.5 border-t border-white/10 pt-4 text-sm">
<div class="flex justify-between text-slate-300">
<div class="mt-4 space-y-1.5 rounded-2xl border border-slate-200/80 bg-white px-4 py-4 text-sm shadow-sm">
<div class="flex justify-between text-slate-600">
<span>Subtotal</span>
<span class="tabular-nums" x-text="money(state.subtotal_minor)"></span>
</div>
<div class="flex justify-between text-emerald-300" x-show="state.discount_minor > 0">
<div class="flex justify-between text-emerald-600" x-show="state.discount_minor > 0">
<span>Discounts</span>
<span class="tabular-nums" x-text="'' + money(state.discount_minor)"></span>
</div>
<div class="flex justify-between text-slate-300" x-show="state.tax_minor > 0">
<div class="flex justify-between text-slate-600" x-show="state.tax_minor > 0">
<span>Tax</span>
<span class="tabular-nums" x-text="money(state.tax_minor)"></span>
</div>
<div class="flex justify-between text-slate-300" x-show="state.tip?.selected_minor > 0">
<div class="flex justify-between text-slate-600" x-show="(state.tip?.selected_minor || state.tip_minor) > 0">
<span>Tip</span>
<span class="tabular-nums" x-text="money(state.tip.selected_minor)"></span>
<span class="tabular-nums" x-text="money(state.tip?.selected_minor || state.tip_minor)"></span>
</div>
<div class="flex justify-between text-2xl font-bold tracking-tight">
<div class="flex justify-between border-t border-slate-100 pt-3 text-2xl font-bold tracking-tight text-slate-900">
<span>Total</span>
<span class="tabular-nums text-indigo-300" x-text="money(grandTotal())"></span>
<span class="tabular-nums text-indigo-600" x-text="money(grandTotal())"></span>
</div>
</div>
{{-- Loyalty --}}
<div x-show="state.loyalty" x-cloak
class="mt-4 rounded-2xl border border-amber-400/30 bg-amber-400/10 px-4 py-3 text-sm">
<p class="font-semibold text-amber-200" x-text="state.loyalty?.label || 'Loyalty'"></p>
<div class="mt-1 flex flex-wrap gap-4 text-amber-100/90">
class="mt-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm">
<p class="font-semibold text-amber-900" x-text="state.loyalty?.label || 'Loyalty'"></p>
<div class="mt-1 flex flex-wrap gap-4 text-amber-800/90">
<span x-show="state.loyalty?.points_balance != null">
Balance: <strong x-text="state.loyalty.points_balance"></strong> pts
</span>
@@ -125,61 +141,62 @@
</div>
</div>
{{-- Right panel: payment / tip / signature --}}
<aside class="flex w-full flex-col justify-center gap-5 border-t border-white/10 bg-slate-900/60 px-6 py-6 lg:w-[420px] lg:border-l lg:border-t-0 sm:px-8">
<aside class="flex w-full flex-col justify-center gap-5 border-t border-slate-200 bg-white/90 px-6 py-6 lg:w-[420px] lg:border-l lg:border-t-0 sm:px-8">
{{-- Payment --}}
<div x-show="isPhase('payment')" class="space-y-4">
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-300">Payment</p>
<p class="text-xl font-semibold" x-text="state.payment?.message || 'Please complete payment'"></p>
<p class="text-sm text-slate-400 capitalize" x-show="state.payment?.status"
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Payment</p>
<p class="text-xl font-semibold text-slate-900" x-text="state.payment?.message || 'Please complete payment'"></p>
<p class="text-sm capitalize text-slate-500" x-show="state.payment?.status"
x-text="'Status: ' + (state.payment?.status || '')"></p>
<div class="flex flex-wrap gap-2" x-show="state.payment?.options?.length">
<template x-for="opt in (state.payment?.options || [])" :key="opt">
<button type="button" @click="choosePayment(opt)"
class="rounded-xl border border-white/15 bg-white/5 px-3 py-2 text-sm font-medium hover:bg-white/10"
class="rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 shadow-sm hover:border-indigo-300 hover:bg-indigo-50"
x-text="opt"></button>
</template>
</div>
<div x-show="qrData()" class="flex flex-col items-center gap-3 rounded-2xl bg-white p-4 text-slate-900">
<div x-show="qrData()" class="flex flex-col items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<img :src="qrImage(qrData())" alt="Payment QR" class="h-48 w-48">
<p class="text-center text-xs text-slate-600">Scan to pay with your phone</p>
<p class="text-center text-xs text-slate-500">Scan to pay with your phone</p>
</div>
</div>
{{-- Tip (restaurants) --}}
{{-- Tip --}}
<div x-show="isPhase('tip')" class="space-y-4">
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-300">Add a tip</p>
<p class="text-xl font-semibold">How much would you like to tip?</p>
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Add a tip</p>
<p class="text-xl font-semibold text-slate-900">How much would you like to tip?</p>
<div class="grid grid-cols-2 gap-2">
<template x-for="pct in (state.tip?.options_percent || [10,15,18,20])" :key="pct">
<button type="button" @click="selectTipPercent(pct)"
:class="state.tip?.selected_percent === pct ? 'bg-indigo-500 text-white ring-2 ring-indigo-300' : 'bg-white/5 hover:bg-white/10'"
class="rounded-2xl px-3 py-4 text-center transition">
:class="state.tip?.selected_percent === pct
? 'border-indigo-500 bg-indigo-600 text-white ring-2 ring-indigo-200'
: 'border-slate-200 bg-white text-slate-900 hover:border-indigo-300 hover:bg-indigo-50'"
class="rounded-2xl border px-3 py-4 text-center shadow-sm transition">
<span class="block text-2xl font-bold" x-text="pct + '%'"></span>
<span class="mt-1 block text-xs text-slate-300" x-text="money(tipAmount(pct))"></span>
<span class="mt-1 block text-xs opacity-80" x-text="money(tipAmount(pct))"></span>
</button>
</template>
</div>
<button type="button" @click="selectTipPercent(0)"
class="w-full rounded-xl border border-white/15 px-3 py-2.5 text-sm text-slate-300 hover:bg-white/5">
class="w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50">
No tip
</button>
<div x-show="state.tip?.custom_allowed" class="flex gap-2">
<input type="number" min="0" step="0.01" x-model="customTip"
placeholder="Custom amount"
class="flex-1 rounded-xl border-0 bg-white/10 px-3 py-2 text-sm text-white placeholder:text-slate-500 focus:ring-2 focus:ring-indigo-400">
class="flex-1 rounded-xl border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
<button type="button" @click="selectCustomTip()"
class="rounded-xl bg-indigo-500 px-4 py-2 text-sm font-semibold hover:bg-indigo-400">Apply</button>
class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500">Apply</button>
</div>
</div>
{{-- Signature --}}
<div x-show="isPhase('signature')" class="space-y-3">
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-300">Signature</p>
<p class="text-lg font-semibold" x-text="state.signature?.prompt || 'Please sign below'"></p>
<div class="overflow-hidden rounded-2xl bg-white">
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Signature</p>
<p class="text-lg font-semibold text-slate-900" x-text="state.signature?.prompt || 'Please sign below'"></p>
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<canvas x-ref="sigCanvas" class="h-48 w-full touch-none"
@pointerdown="sigStart($event)"
@pointermove="sigMove($event)"
@@ -188,17 +205,17 @@
</div>
<div class="flex gap-2">
<button type="button" @click="clearSignature()"
class="flex-1 rounded-xl border border-white/15 px-3 py-2 text-sm hover:bg-white/5">Clear</button>
class="flex-1 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Clear</button>
<button type="button" @click="submitSignature()"
class="flex-1 rounded-xl bg-indigo-500 px-3 py-2 text-sm font-semibold hover:bg-indigo-400">Done</button>
class="flex-1 rounded-xl bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500">Done</button>
</div>
<p x-show="sigSaved" class="text-sm text-emerald-300">Signature saved thank you.</p>
<p x-show="sigSaved" class="text-sm font-medium text-emerald-600">Signature saved thank you.</p>
</div>
{{-- Cart-only helper --}}
<div x-show="isPhase('cart')" class="text-center text-sm text-slate-400">
<p>Please review your order</p>
<p class="mt-1 text-xs">Prices shown include any applied discounts</p>
<div x-show="isPhase('cart')" class="rounded-2xl border border-dashed border-slate-200 bg-slate-50/80 px-5 py-8 text-center">
<p class="text-base font-semibold text-slate-800">Please review your order</p>
<p class="mt-1 text-sm text-slate-500">Prices shown include any applied discounts</p>
</div>
</aside>
</section>
@@ -206,33 +223,33 @@
{{-- RECEIPT --}}
<section x-show="isPhase('receipt')" x-cloak
class="flex flex-1 flex-col items-center justify-center gap-5 px-8 text-center">
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500/20 text-3xl text-emerald-300"></div>
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 text-3xl text-emerald-600 ring-1 ring-emerald-200"></div>
<div>
<h1 class="text-3xl font-bold sm:text-4xl" x-text="state.receipt?.message || 'Thank you!'"></h1>
<p class="mt-2 text-slate-400" x-show="state.receipt?.reference || state.sale_reference">
Receipt <span class="text-white" x-text="state.receipt?.reference || state.sale_reference"></span>
<h1 class="text-3xl font-bold text-slate-900 sm:text-4xl" x-text="state.receipt?.message || 'Thank you!'"></h1>
<p class="mt-2 text-slate-500" x-show="state.receipt?.reference || state.sale_reference">
Receipt <span class="font-medium text-slate-800" x-text="state.receipt?.reference || state.sale_reference"></span>
</p>
<p class="mt-4 text-4xl font-bold tabular-nums text-indigo-300" x-text="money(state.receipt?.total_minor ?? state.total_minor)"></p>
<p class="mt-4 text-4xl font-bold tabular-nums text-indigo-600" x-text="money(state.receipt?.total_minor ?? state.total_minor)"></p>
</div>
<div x-show="state.receipt?.digital_url" class="rounded-2xl bg-white p-4">
<div x-show="state.receipt?.digital_url" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<img :src="qrImage(state.receipt.digital_url)" alt="Digital receipt QR" class="h-40 w-40">
<p class="mt-2 text-xs text-slate-600">Scan for digital receipt</p>
<p class="mt-2 text-xs text-slate-500">Scan for digital receipt</p>
</div>
<div x-show="state.receipt?.email_prompt !== false" class="w-full max-w-md">
<p class="mb-2 text-sm text-slate-400">Email me a digital receipt</p>
<p class="mb-2 text-sm text-slate-500">Email me a digital receipt</p>
<div class="flex gap-2">
<input type="email" x-model="receiptEmail" placeholder="you@email.com"
class="flex-1 rounded-xl border-0 bg-white/10 px-3 py-2.5 text-sm text-white placeholder:text-slate-500 focus:ring-2 focus:ring-indigo-400">
class="flex-1 rounded-xl border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
<button type="button" @click="sendReceiptEmail()"
class="rounded-xl bg-indigo-500 px-4 py-2.5 text-sm font-semibold hover:bg-indigo-400">Send</button>
class="rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-500">Send</button>
</div>
<p x-show="emailSent" class="mt-2 text-sm text-emerald-300">Receipt emailed check your inbox.</p>
<p x-show="emailError" class="mt-2 text-sm text-rose-300" x-text="emailError"></p>
<p x-show="emailSent" class="mt-2 text-sm font-medium text-emerald-600">Receipt emailed check your inbox.</p>
<p x-show="emailError" class="mt-2 text-sm font-medium text-rose-600" x-text="emailError"></p>
</div>
<p class="text-xs text-slate-500" x-show="state.brand?.footer" x-text="state.brand.footer"></p>
<p class="text-xs text-slate-400" x-show="state.brand?.footer" x-text="state.brand.footer"></p>
</section>
</main>
</div>
@@ -263,7 +280,6 @@
this.pollTimer = setInterval(() => this.fetchState(), 4000);
this.$nextTick(() => this.resizeCanvas());
window.addEventListener('resize', () => this.resizeCanvas());
// Same-browser dual window: instant cart updates without waiting for SSE.
try {
this.bc = new BroadcastChannel('pos-customer-display');
this.bc.onmessage = (e) => {
@@ -297,8 +313,13 @@
grandTotal() {
const base = Number(this.state.total_minor) || 0;
const tip = Number(this.state.tip?.selected_minor) || 0;
return base + tip;
const tipInTotal = Number(this.state.tip_minor) || 0;
const tipSelected = Number(this.state.tip?.selected_minor) || 0;
// Avoid double-counting when the server already baked tip into total_minor.
if (tipInTotal > 0) {
return base;
}
return base + tipSelected;
},
tipAmount(pct) {