Restaurant mode Phase 2: menu depth — categories, stations, modifiers, courses
Deploy Ladill POS / deploy (push) Successful in 35s
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d9e4b6e06e
commit
50b170b0af
@@ -4,7 +4,12 @@ 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;
|
||||
@@ -16,6 +21,7 @@ class ProductController extends Controller
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$products = PosProduct::owned($this->ownerRef($request))
|
||||
->with('category')
|
||||
->orderBy('name')
|
||||
->paginate(30)
|
||||
->withQueryString();
|
||||
@@ -23,20 +29,25 @@ class ProductController extends Controller
|
||||
return view('pos.products.index', compact('products'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('pos.products.create', ['product' => new PosProduct([
|
||||
'currency' => config('pos.default_currency', 'GHS'),
|
||||
'is_active' => true,
|
||||
])]);
|
||||
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
|
||||
{
|
||||
PosProduct::create([
|
||||
$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.');
|
||||
}
|
||||
@@ -45,13 +56,18 @@ class ProductController extends Controller
|
||||
{
|
||||
$this->authorizeOwner($request, $product);
|
||||
|
||||
return view('pos.products.edit', compact('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.');
|
||||
}
|
||||
@@ -64,23 +80,58 @@ class ProductController extends Controller
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user