Deploy Ladill POS / deploy (push) Successful in 30s
Gated by a per-location service_style (retail | restaurant). Restaurant mode adds: - Floor screen with tables (areas, seats, status) — tap a free table to open a dine-in tab; takeaway/counter orders open from the same screen. - Open tickets (tabs): a sale stays pending while items are added; lines persist as you go (posTicket Alpine component posts each change to the server). - Send-to-kitchen fires un-sent lines; a polling Kitchen Display (KDS) shows active tickets and bumps items queued → preparing → ready → served. - Settlement reuses the existing cash / Ladill Pay flow; paying closes the tab and frees the table (PosSaleService::closeTicket, wired into both pay paths). - Settings gains the mode toggle and a tables manager. Schema is additive (new pos_tables; service_style on locations; order/kitchen columns on sales + lines). Retail flow is untouched. Sidebar surfaces Floor + Kitchen only in restaurant mode. New PosRestaurantTest covers the dine-in lifecycle end to end; suite green (10 passed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
6.5 KiB
PHP
98 lines
6.5 KiB
PHP
<x-app-layout title="Ticket">
|
||
<div x-data="posTicket(@js([
|
||
'base' => route('pos.tickets.show', $sale),
|
||
'lines' => $lines,
|
||
'products' => $products->map(fn ($p) => ['id' => $p->id, 'name' => $p->name, 'price_minor' => $p->price_minor])->values()->all(),
|
||
'totalMinor' => $sale->total_minor,
|
||
'currency' => $sale->currency,
|
||
]))" class="space-y-4">
|
||
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<a href="{{ route('pos.floor') }}" 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>
|
||
Floor
|
||
</a>
|
||
<h1 class="mt-1 text-lg font-semibold text-slate-900">
|
||
{{ $sale->table?->label ?? ucfirst(str_replace('_', ' ', $sale->order_type)) }}
|
||
</h1>
|
||
<p class="text-sm text-slate-500 capitalize">{{ str_replace('_', ' ', $sale->order_type) }}{{ $sale->covers ? ' · '.$sale->covers.' covers' : '' }} · {{ $sale->reference }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<p x-show="flash" x-text="flash" x-cloak class="rounded-xl border border-indigo-100 bg-indigo-50 px-4 py-2 text-sm text-indigo-700"></p>
|
||
|
||
<div class="grid gap-4 lg:grid-cols-[1fr_22rem]">
|
||
{{-- Catalog --}}
|
||
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||
<input type="search" x-model="search" placeholder="Search items…"
|
||
class="mb-3 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||
<template x-for="p in filteredProducts" :key="p.id">
|
||
<button type="button" @click="addProduct(p)" :disabled="busy"
|
||
class="rounded-xl border border-slate-200 p-3 text-left transition hover:border-indigo-300 hover:bg-indigo-50/50 disabled:opacity-50">
|
||
<span class="block text-sm font-medium text-slate-900" x-text="p.name"></span>
|
||
<span class="mt-1 block text-xs text-slate-500" x-text="money(p.price_minor)"></span>
|
||
</button>
|
||
</template>
|
||
<template x-if="filteredProducts.length === 0">
|
||
<p class="col-span-full py-6 text-center text-sm text-slate-400">No products. Add some under Products.</p>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Ticket --}}
|
||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-white">
|
||
<div class="flex-1 space-y-2 p-4">
|
||
<template x-if="lines.length === 0">
|
||
<p class="py-8 text-center text-sm text-slate-400">No items yet. Tap a product to add it.</p>
|
||
</template>
|
||
<template x-for="line in lines" :key="line.id">
|
||
<div class="rounded-xl border border-slate-100 p-2.5">
|
||
<div class="flex items-start justify-between gap-2">
|
||
<div class="min-w-0">
|
||
<p class="truncate text-sm font-medium text-slate-900" x-text="line.name"></p>
|
||
<p class="text-xs text-slate-400" x-text="money(line.unit_price_minor)"></p>
|
||
<span x-show="line.fired" x-cloak class="mt-1 inline-block rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">In kitchen</span>
|
||
</div>
|
||
<div class="text-right">
|
||
<p class="text-sm font-semibold text-slate-900" x-text="money(line.line_total_minor)"></p>
|
||
<div class="mt-1 flex items-center justify-end gap-1.5">
|
||
<button type="button" @click="dec(line)" :disabled="busy" class="flex h-6 w-6 items-center justify-center rounded-md border border-slate-200 text-slate-600 hover:bg-slate-50">−</button>
|
||
<span class="w-5 text-center text-sm" x-text="line.quantity"></span>
|
||
<button type="button" @click="inc(line)" :disabled="busy" class="flex h-6 w-6 items-center justify-center rounded-md border border-slate-200 text-slate-600 hover:bg-slate-50">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="border-t border-slate-100 p-4">
|
||
<div class="mb-3 flex items-center justify-between text-base font-semibold text-slate-900">
|
||
<span>Total</span>
|
||
<span x-text="money(totalMinor)"></span>
|
||
</div>
|
||
|
||
<button type="button" @click="send()" :disabled="busy || ! hasNew"
|
||
class="mb-2 w-full rounded-xl border border-amber-300 bg-amber-50 px-4 py-2.5 text-sm font-semibold text-amber-800 transition hover:bg-amber-100 disabled:opacity-40">
|
||
Send to kitchen
|
||
</button>
|
||
|
||
<form method="post" action="{{ route('pos.tickets.settle', $sale) }}" class="space-y-2">
|
||
@csrf
|
||
<input type="text" name="customer_name" placeholder="Customer name (optional)"
|
||
class="w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<button name="payment_method" value="cash" :disabled="lines.length === 0"
|
||
class="rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:opacity-40">Cash</button>
|
||
<button name="payment_method" value="pay" :disabled="lines.length === 0"
|
||
class="btn-primary disabled:opacity-40">Ladill Pay</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</x-app-layout>
|