Deploy Ladill POS / deploy (push) Successful in 35s
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>
138 lines
4.8 KiB
PHP
138 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Pos;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Controllers\Pos\Concerns\ScopesToAccount;
|
|
use App\Models\PosCategory;
|
|
use App\Models\PosLocation;
|
|
use App\Models\PosModifierGroup;
|
|
use App\Models\PosProduct;
|
|
use App\Models\PosSaleLine;
|
|
use App\Models\PosStation;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
|
|
class ProductController extends Controller
|
|
{
|
|
use ScopesToAccount;
|
|
|
|
public function index(Request $request): View
|
|
{
|
|
$products = PosProduct::owned($this->ownerRef($request))
|
|
->with('category')
|
|
->orderBy('name')
|
|
->paginate(30)
|
|
->withQueryString();
|
|
|
|
return view('pos.products.index', compact('products'));
|
|
}
|
|
|
|
public function create(Request $request): View
|
|
{
|
|
return view('pos.products.create', [
|
|
'product' => new PosProduct([
|
|
'currency' => config('pos.default_currency', 'GHS'),
|
|
'is_active' => true,
|
|
]),
|
|
'selectedGroups' => [],
|
|
...$this->formOptions($request),
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$product = PosProduct::create([
|
|
...$this->validated($request),
|
|
'owner_ref' => $this->ownerRef($request),
|
|
]);
|
|
$product->modifierGroups()->sync($this->groupIds($request));
|
|
|
|
return redirect()->route('pos.products.index')->with('success', 'Product added.');
|
|
}
|
|
|
|
public function edit(Request $request, PosProduct $product): View
|
|
{
|
|
$this->authorizeOwner($request, $product);
|
|
|
|
return view('pos.products.edit', [
|
|
'product' => $product,
|
|
'selectedGroups' => $product->modifierGroups()->pluck('pos_modifier_groups.id')->all(),
|
|
...$this->formOptions($request),
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request, PosProduct $product): RedirectResponse
|
|
{
|
|
$this->authorizeOwner($request, $product);
|
|
$product->update($this->validated($request));
|
|
$product->modifierGroups()->sync($this->groupIds($request));
|
|
|
|
return redirect()->route('pos.products.index')->with('success', 'Product updated.');
|
|
}
|
|
|
|
public function destroy(Request $request, PosProduct $product): RedirectResponse
|
|
{
|
|
$this->authorizeOwner($request, $product);
|
|
$product->delete();
|
|
|
|
return redirect()->route('pos.products.index')->with('success', 'Product removed.');
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function formOptions(Request $request): array
|
|
{
|
|
$owner = $this->ownerRef($request);
|
|
|
|
return [
|
|
'restaurant' => PosLocation::owned($owner)->where('service_style', PosLocation::STYLE_RESTAURANT)->exists(),
|
|
'categories' => PosCategory::owned($owner)->orderBy('name')->get(),
|
|
'stations' => PosStation::owned($owner)->orderBy('name')->get(),
|
|
'modifierGroups' => PosModifierGroup::owned($owner)->orderBy('name')->get(),
|
|
];
|
|
}
|
|
|
|
/** @return list<int> */
|
|
private function groupIds(Request $request): array
|
|
{
|
|
$owner = $this->ownerRef($request);
|
|
$ids = array_map('intval', (array) $request->input('modifier_group_ids', []));
|
|
|
|
return PosModifierGroup::owned($owner)->whereIn('id', $ids)->pluck('id')->all();
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function validated(Request $request): array
|
|
{
|
|
$owner = $this->ownerRef($request);
|
|
|
|
$data = $request->validate([
|
|
'name' => ['required', 'string', 'max:200'],
|
|
'sku' => ['nullable', 'string', 'max:80'],
|
|
'price' => ['required', 'numeric', 'min:0.01'],
|
|
'currency' => ['nullable', 'string', 'size:3'],
|
|
'is_active' => ['sometimes', 'boolean'],
|
|
'category_id' => ['nullable', 'integer'],
|
|
'station_id' => ['nullable', 'integer'],
|
|
'course' => ['nullable', 'in:'.implode(',', array_keys(PosSaleLine::COURSES))],
|
|
]);
|
|
|
|
$categoryId = ($data['category_id'] ?? null) && PosCategory::owned($owner)->whereKey($data['category_id'])->exists()
|
|
? (int) $data['category_id'] : null;
|
|
$stationId = ($data['station_id'] ?? null) && PosStation::owned($owner)->whereKey($data['station_id'])->exists()
|
|
? (int) $data['station_id'] : null;
|
|
|
|
return [
|
|
'name' => $data['name'],
|
|
'sku' => $data['sku'] ?? null,
|
|
'price_minor' => (int) round(((float) $data['price']) * 100),
|
|
'currency' => strtoupper($data['currency'] ?? config('pos.default_currency', 'GHS')),
|
|
'is_active' => $request->boolean('is_active', true),
|
|
'category_id' => $categoryId,
|
|
'station_id' => $stationId,
|
|
'course' => $data['course'] ?? null,
|
|
];
|
|
}
|
|
}
|