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>
88 lines
5.5 KiB
PHP
88 lines
5.5 KiB
PHP
<x-app-layout title="Floor">
|
|
@php $cur = $location->currency; @endphp
|
|
<div class="space-y-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<h1 class="text-lg font-semibold text-slate-900">Floor</h1>
|
|
<p class="mt-1 text-sm text-slate-500">Open a tab on a table, or start a takeaway order.</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<form method="post" action="{{ route('pos.tickets.open') }}">
|
|
@csrf
|
|
<input type="hidden" name="order_type" value="takeaway">
|
|
<button class="rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">+ Takeaway</button>
|
|
</form>
|
|
<form method="post" action="{{ route('pos.tickets.open') }}">
|
|
@csrf
|
|
<input type="hidden" name="order_type" value="counter">
|
|
<button class="btn-primary">+ Counter order</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
@if(session('error'))
|
|
<div class="rounded-xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">{{ session('error') }}</div>
|
|
@endif
|
|
|
|
{{-- Open non-dine-in tickets --}}
|
|
@php $loose = $openTickets->whereNull('table_id'); @endphp
|
|
@if($loose->isNotEmpty())
|
|
<div>
|
|
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">Open orders</h2>
|
|
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
@foreach($loose as $t)
|
|
<a href="{{ route('pos.tickets.show', $t) }}" class="rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-indigo-200 hover:shadow-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium capitalize text-slate-600">{{ str_replace('_', ' ', $t->order_type) }}</span>
|
|
@if($t->kitchen_status === \App\Models\PosSale::KITCHEN_ACTIVE)
|
|
<span class="rounded-full bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-700">In kitchen</span>
|
|
@endif
|
|
</div>
|
|
<p class="mt-2 text-sm font-semibold text-slate-900">{{ $cur }} {{ number_format($t->total_minor / 100, 2) }}</p>
|
|
<p class="mt-0.5 text-xs text-slate-400">{{ $t->lines_count }} item(s) · {{ $t->opened_at?->diffForHumans() }}</p>
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Tables --}}
|
|
@if($tables->isEmpty())
|
|
<div class="rounded-2xl border border-dashed border-slate-200 bg-white px-6 py-12 text-center">
|
|
<p class="text-sm text-slate-500">No tables yet.</p>
|
|
<a href="{{ route('pos.settings') }}" class="mt-3 inline-block text-sm font-semibold text-indigo-600 hover:text-indigo-800">Add tables in Settings</a>
|
|
</div>
|
|
@else
|
|
@foreach($tablesByArea as $area => $areaTables)
|
|
<div>
|
|
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">{{ $area }}</h2>
|
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
|
@foreach($areaTables as $table)
|
|
@php $occupied = ! $table->isFree() && $table->currentSale; @endphp
|
|
@if($occupied)
|
|
<a href="{{ route('pos.tickets.show', $table->currentSale) }}"
|
|
class="flex flex-col rounded-2xl border-2 border-amber-300 bg-amber-50 p-4 text-left transition hover:border-amber-400">
|
|
<span class="text-sm font-semibold text-slate-900">{{ $table->label }}</span>
|
|
<span class="mt-1 text-xs text-amber-700">Occupied</span>
|
|
<span class="mt-2 text-sm font-semibold text-slate-900">{{ $cur }} {{ number_format($table->currentSale->total_minor / 100, 2) }}</span>
|
|
</a>
|
|
@else
|
|
<form method="post" action="{{ route('pos.tickets.open') }}" class="contents">
|
|
@csrf
|
|
<input type="hidden" name="order_type" value="dine_in">
|
|
<input type="hidden" name="table_id" value="{{ $table->id }}">
|
|
<button class="flex w-full flex-col rounded-2xl border-2 border-slate-200 bg-white p-4 text-left transition hover:border-indigo-300 hover:bg-indigo-50/40">
|
|
<span class="text-sm font-semibold text-slate-900">{{ $table->label }}</span>
|
|
<span class="mt-1 text-xs text-slate-400">{{ $table->seats }} seats · Free</span>
|
|
<span class="mt-2 text-xs font-semibold text-indigo-600">Open tab →</span>
|
|
</button>
|
|
</form>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
</x-app-layout>
|