Files
ladill-pos/resources/views/pos/tickets/show.blade.php
T
isaaccladandClaude Opus 4.8 50b170b0af
Deploy Ladill POS / deploy (push) Successful in 35s
Restaurant mode Phase 2: menu depth — categories, stations, modifiers, courses
Builds on Phase 1 with the menu structure restaurants need:
- Menu setup page (restaurant mode): categories, kitchen stations, and modifier
  groups with options (price deltas).
- Products gain category, kitchen station, default course, and attached modifier
  groups (managed on the product form).
- Ordering: the ticket groups the catalog by category and, when a product has
  modifier groups, opens a picker (respects min/max select) for options + notes +
  course before adding. Effective price = base + modifier deltas (resolved
  server-side; client prices are never trusted).
- Fire-by-course: send the whole tab or just one course to the kitchen.
- KDS: filter by station; each item shows its station, course, modifiers and notes.

Schema additive (pos_categories, pos_stations, pos_modifier_groups, pos_modifiers,
product↔group pivot, pos_sale_line_modifiers; category/station/course columns on
products and lines). PosRestaurantTest covers modifiers, course and station routing;
suite green (11 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 19:58:57 +00:00

178 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout title="Ticket">
<div x-data="posTicket(@js([
'base' => route('pos.tickets.show', $sale),
'lines' => $lines,
'products' => $products,
'totalMinor' => $sale->total_minor,
'currency' => $sale->currency,
'courseLabels' => $courses,
]))" 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 x-show="categories.length" class="mb-3 flex flex-wrap gap-1.5">
<button type="button" @click="activeCategory = ''" :class="activeCategory === '' ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600'"
class="rounded-full px-3 py-1 text-xs font-medium">All</button>
<template x-for="c in categories" :key="c">
<button type="button" @click="activeCategory = c" :class="activeCategory === c ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600'"
class="rounded-full px-3 py-1 text-xs font-medium" x-text="c"></button>
</template>
</div>
<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="tap(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>
<span x-show="p.modifier_groups.length" class="mt-1 inline-block text-[10px] font-medium text-indigo-500">options</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 x-show="line.modifiers.length" x-cloak class="text-xs text-slate-500" x-text="line.modifiers.join(', ')"></p>
<p x-show="line.notes" x-cloak class="text-xs text-rose-600" x-text="line.notes"></p>
<div class="mt-1 flex items-center gap-1.5">
<span x-show="line.course" x-cloak class="rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-500" x-text="courseLabel(line.course)"></span>
<span x-show="line.fired" x-cloak class="rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">In kitchen</span>
</div>
</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>
{{-- Fire to kitchen (all, or by course when more than one course is waiting) --}}
<div x-show="hasNew" x-cloak class="mb-2 space-y-1.5">
<button type="button" @click="send()" :disabled="busy"
class="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 all to kitchen
</button>
<template x-if="newCourses.length > 1">
<div class="flex flex-wrap gap-1.5">
<template x-for="c in newCourses" :key="c">
<button type="button" @click="send(c)" :disabled="busy"
class="rounded-lg border border-amber-200 bg-white px-2.5 py-1 text-xs font-medium text-amber-700 hover:bg-amber-50"
x-text="'Fire ' + courseLabel(c)"></button>
</template>
</div>
</template>
</div>
<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>
{{-- Modifier picker --}}
<div x-show="picker.open" x-cloak class="fixed inset-0 z-50 flex items-end justify-center bg-slate-900/40 p-0 sm:items-center sm:p-4" @click.self="closePicker()">
<div class="w-full max-w-md overflow-hidden rounded-t-2xl bg-white shadow-xl sm:rounded-2xl">
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
<p class="text-sm font-semibold text-slate-900" x-text="picker.product?.name"></p>
<button @click="closePicker()" class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-100">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="max-h-[60vh] space-y-4 overflow-y-auto px-5 py-4">
<template x-for="g in picker.groups" :key="g.id">
<div>
<p class="text-xs font-semibold text-slate-600">
<span x-text="g.name"></span>
<span class="font-normal text-slate-400" x-text="'(choose ' + g.min + (g.max ? '' + g.max : '+') + ')'"></span>
</p>
<div class="mt-2 flex flex-wrap gap-2">
<template x-for="m in g.modifiers" :key="m.id">
<button type="button" @click="toggleModifier(g, m)"
:class="isSelected(g, m) ? 'border-indigo-500 bg-indigo-50 text-indigo-700' : 'border-slate-200 text-slate-600'"
class="rounded-lg border px-3 py-1.5 text-sm font-medium">
<span x-text="m.name"></span>
<span x-show="m.price_delta_minor" class="text-xs opacity-70"
x-text="(m.price_delta_minor > 0 ? ' +' : ' ') + money(m.price_delta_minor).replace(currency + ' ', '')"></span>
</button>
</template>
</div>
</div>
</template>
<div>
<label class="text-xs font-semibold text-slate-600">Course</label>
<select x-model="picker.course" class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="">No course</option>
@foreach($courses as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="text-xs font-semibold text-slate-600">Notes</label>
<input type="text" x-model="picker.notes" maxlength="200" placeholder="e.g. no onions"
class="mt-1 w-full rounded-xl border-slate-200 text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
</div>
<div class="flex items-center justify-between gap-3 border-t border-slate-100 px-5 py-3">
<span class="text-sm font-semibold text-slate-900" x-text="money(pickerPrice)"></span>
<button type="button" @click="confirmPicker()" :disabled="! pickerValid || busy" class="btn-primary disabled:opacity-40">Add to ticket</button>
</div>
</div>
</div>
</div>
</x-app-layout>