firstOrFail(); $owner = (string) $table->owner_ref; $location = PosLocation::owned($owner)->first(); $products = PosProduct::owned($owner)->active() ->with(['category', 'modifierGroups.modifiers']) ->orderBy('name')->get(); return view('public.table-menu', [ 'table' => $table, 'storeName' => $location?->name ?: 'Menu', 'currency' => $location?->currency ?: config('pos.default_currency', 'GHS'), 'products' => $products->map(fn (PosProduct $p) => [ 'id' => $p->id, 'name' => $p->name, 'price_minor' => $p->price_minor, 'category' => $p->category?->name, 'modifier_groups' => $p->modifierGroups->map(fn ($g) => [ 'id' => $g->id, 'name' => $g->name, 'min' => $g->min_select, 'max' => $g->max_select, 'modifiers' => $g->modifiers->map(fn ($m) => [ 'id' => $m->id, 'name' => $m->name, 'price_delta_minor' => $m->price_delta_minor, ])->values()->all(), ])->values()->all(), ])->values()->all(), ]); } public function store(Request $request, string $code): JsonResponse { $table = PosTable::where('short_code', $code)->firstOrFail(); $owner = (string) $table->owner_ref; $data = $request->validate([ 'items' => ['required', 'array', 'min:1', 'max:50'], 'items.*.product_id' => ['required', 'integer'], 'items.*.quantity' => ['required', 'integer', 'min:1', 'max:50'], 'items.*.modifier_ids' => ['nullable', 'array'], 'items.*.modifier_ids.*' => ['integer'], 'items.*.notes' => ['nullable', 'string', 'max:200'], 'customer_name' => ['nullable', 'string', 'max:120'], ]); $sale = $this->resolveTab($table, $owner); if ($sale === null) { return response()->json(['message' => 'This table is not available for ordering right now.'], 422); } if (! empty($data['customer_name']) && ! $sale->customer_name) { $sale->forceFill(['customer_name' => $data['customer_name']])->save(); } $added = 0; foreach ($data['items'] as $item) { $payload = $this->sales->buildProductLine( $owner, (int) $item['product_id'], $item['modifier_ids'] ?? [], (int) $item['quantity'], $item['notes'] ?? null, null, 'guest', ); if ($payload !== null) { $this->sales->addLine($sale, $payload); $added++; } } if ($added === 0) { return response()->json(['message' => 'Those items are no longer available.'], 422); } // Guest orders go straight to the kitchen. $this->sales->sendToKitchen($sale); return response()->json(['redirect' => route('pos.table.confirmed', $code)]); } public function confirmed(string $code): View { $table = PosTable::where('short_code', $code)->firstOrFail(); $location = PosLocation::owned((string) $table->owner_ref)->first(); return view('public.table-confirmed', [ 'table' => $table, 'storeName' => $location?->name ?: 'Thanks', ]); } private function resolveTab(PosTable $table, string $owner): ?PosSale { if ($table->current_sale_id) { $existing = PosSale::owned($owner)->openTickets()->find($table->current_sale_id); if ($existing) { return $existing; } } $merchant = User::where('public_id', $owner)->first(); if (! $merchant) { return null; } $location = PosLocation::owned($owner)->first(); return $this->sales->openTicket($merchant, [ 'order_type' => PosSale::ORDER_DINE_IN, 'table_id' => $table->id, 'location_id' => $location?->id, 'currency' => $location?->currency, ]); } }