@extends('layouts.dashboard') @section('content') @php /** * Vendor → Menu → Add / Edit item. The routed page form (own URL, full dashboard * chrome) that replaced the #itemModal Bootstrap modal. The field ids, names and * the populate/ingredient-builder JS are the modal's own — moved here verbatim — * so MenuController::saveItem() receives an identical POST and needs no change. * The layout is rebuilt to page standards: dashboard cards, qm-switch-row toggles * (right-aligned, one per row) and the canonical qm-tabs ingredient picker. * On edit, a hidden #item-form-seed carries the row's data-* attributes (exactly * the old edit button's) and the populate function runs once on load. * * @var array|null $item the menu item being edited, or null when adding * @var object|array $itemIngMap this item's ingredient map { id: {d,r,p} } (edit) * @var array $categories restaurant menu categories * @var array $ingredients grouped active ingredient library * @var array $schedules restaurant availability schedules * @var array $stations kitchen stations (empty when the kitchen is undivided) * @var array $allowance plan menu-item allowance */ use App\Services\MenuSchedule; $isEdit = $item !== null; $item = $item ?? null; $stations = $stations ?? []; $itemIngMap = $itemIngMap ?? new stdClass(); // The admin owns this list. It was hardcoded here AND again in Helpers, so adding one meant // editing two files and the two could disagree; the EU mandates 14 and this shipped 9. $allergenOptions = \App\Models\Dimension::options(\App\Models\Dimension::ALLERGENS); // Attribute toggles: [field id, POST name, label key, default-checked-on-add]. $attrToggles = [ ['item-veg', 'is_veg', 'vendor.menu.attr_vegetarian', false], ['item-available', 'is_available', 'vendor.menu.available_label', true], ['item-popular', 'is_popular', 'vendor.menu.popular_pill', false], ]; /* Those three keep their own columns because they are not descriptions of the dish: is_veg a real diet filter, drawn as the veg/non-veg dot and used across the storefront is_available the dish's state is_popular a merchandising flag The six that used to sit under them — eggs, chef's pick, zesty, sweet, fruity, baked — were six more boolean COLUMNS, so a buyer wanting "Halal" needed an ALTER TABLE. They are rows now. */ $attrRows = \App\Models\Dimension::attributeIcons(); $itemAttrs = array_filter(array_map('trim', explode(',', (string) ($item['attributes'] ?? '')))); $ingCatKeys = [ 'proteins' => 'common.ingredient_cat_proteins', 'dairy' => 'common.ingredient_cat_dairy', 'breads' => 'common.ingredient_cat_breads', 'vegetables' => 'common.ingredient_cat_vegetables', 'fruits' => 'common.ingredient_cat_fruits', 'sauces' => 'common.ingredient_cat_sauces', 'grains' => 'common.ingredient_cat_grains', 'herbs' => 'common.ingredient_cat_herbs', ]; @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
@php /* No heading above the form. The subheader already names the page ("Edit menu item") and the first card is headed "Item Details" — a third heading between the two said the same thing twice, 40px apart. The four card headings are a set and stay; this one was the odd one out. A plain block, NOT .qm-ops-head: that class exists to sit a heading beside an action, and its `> :first-child { flex: 1 1 220px }` is meant for the heading absorbing the free space. With the heading gone the button became the first child and inherited it, rendering a 220px Back button. The other form pages keep their heading, so they never hit this. */ @endphp @php /* Right-aligned with justify-content-end rather than text-end: flex-end follows the writing direction, so the button mirrors to the left in RTL on its own, while text-end resolves to a physical `text-align: right` that would strand it there. */ @endphp
{{ t_raw('common.back') }}
@php /* data-submit-once is the shared guard in app.js: it greys the Save button the moment the form is on its way, so an impatient double-click cannot fire a second submit and the vendor can see that the first one was taken. The booking form already carries it. It is feedback, not the guarantee — the create uid below is what stops a duplicate dish when the save arrives twice anyway (Back-then-Save, a replayed POST, a second tab). */ @endphp
@csrf @php /* Identity of THIS rendering of the add form, so the server can tell a repeat of one save from a second dish deliberately entered the same way. Absent on the edit form, which is idempotent already. See MenuController::createdByUid(). */ @endphp @if (($createUid ?? '') !== '') @endif
@php /* Every column the dish owns lives here, in one section. Vegetarian, Available, Popular, spice level and calories were in a second card on the right, and both cards were headed "Item Details" — the same words twice, side by side. They are the dish's diet flag, its state, a merchandising flag and two of its own measurements, so they belong with the rest of its columns rather than under a heading of their own. The right-hand column keeps what genuinely is a separate vocabulary: the admin-managed attributes and allergens. */ @endphp

@php /* The station override. Blank is the normal answer — the dish is cooked wherever its section is — so the default option says so rather than leaving an empty select that reads like a missing setting. Offered only once the kitchen has stations at all. */ @endphp @if ($stations)
@endif @php /* The code a till's barcode scanner rings this dish by — a packaged drink's printed EAN, or the restaurant's own SKU. Unique within this restaurant only, so two venues stocking the same bottle each keep their own item. */ @endphp
@php /* Calories were readable on three customer-facing surfaces — the storefront card, the printable sheet and the TV menu board — with no way for a vendor to set them. The column and the data were always there; only the input was missing. Blank means "not stated" and prints nothing, so a restaurant that does not count calories is unaffected. */ @endphp
@php /* The veg/non-veg dot the storefront card, the kiosk and the order builder all draw, shown here so the vendor sets the flag against the mark a customer actually sees. A direct child of the row, not of the label span: .qm-switch-row span is a COLUMN (title over subtitle), so an icon inside it stacks above the words. The row orders its span 1 and its input 2, leaving 0 — the leading edge — for this. And an , not a , because .qm-switch-row > span is the label cell (`flex: 1 1 auto`) and outranks the badge class: a span here stretched into a bar across the row. */ @endphp
@foreach ($attrToggles as [$fid, $fname, $lkey, $on]) @endforeach
{!! media_field_html('image', $isEdit ? ($item['image'] ?? null) : null, ['accept' => 'image', 'label' => t_raw('media.select_image')]) !!}
@php /* Folded sections rather than stacked cards: .qm-acc is the collapsible-section style the receipt, QR-card and TV-screen editors already use, so nothing new is introduced here. Independent items (no data-bs-parent) — a vendor tagging a dish reads allergens and attributes together, and an exclusive accordion would shut one to open the other. Both open on load: folding is there for a vendor who wants the page shorter, not a default that hides fields. */ @endphp
@php /* The admin-managed attributes, each shown with the artwork the card will draw — so the vendor picks by the badge a customer sees rather than by a word. The artwork is a direct child of the row for the same reason as the veg dot above: inside the label span it would stack over the words instead of leading them. */ @endphp

@foreach ($attrRows as $aSlug => $aRow) @endforeach
@php /* Allergens joins its two neighbours instead of holding a full-width card of its own below them: it is the same kind of list, set at the same moment, and the card cost 375px of page for nine switches. */ @endphp

@foreach ($allergenOptions as $val => $label) @endforeach
@php /* Choice groups — "Size: Small / Medium / Large". * * Ingredients answer "what can be taken out"; this answers the other half, a pick-one (or * pick-a-few) the customer must make. * * A group belongs to ONE dish. Picking an existing one from the menu COPIES its lines in as a * starting point, so what the vendor edits here can only ever affect the dish in front of them. * The alternative — one shared row pointed at by many dishes — needs a library screen to be * edited safely, and there isn't one: a shared group was read-only on every dish that held it, * so it ended up editable from nowhere at all, and making it editable inline instead meant a * price typed on one pizza silently changed eleven others and invalidated any live cart holding * its rows (Cart::forCart() resolves modifiers by id, and a rebuild renumbers them). * * ONE control, at the top where an action belongs: a native @foreach ($reusableGroups as $rg) @endforeach
@foreach ($choiceGroups as $gi => $g)
@php /* Name grows, the two numbers stay narrow, remove sits at the end — the same shape as the option lines below, so the card reads as one rhythm. It was a card-head over a two-column grid, which gave two single-digit fields half the card each and squeezed the name into a corner. */ @endphp
@php /* The two column names are stated once, on the first line, rather than repeated down every row or hidden from sight entirely. */ @endphp @foreach (($g['opts'] ?: [['name' => '', 'price' => 0]]) as $oi => $o)
@endforeach
@endforeach
@php /* Every offerable group's lines, so picking one fills its card without a round trip. */ @endphp @php /* THE blank group, and the only thing the script clones. It used to clone whichever card happened to be first, so deleting every card left it with nothing to copy and the control went dead until the page was reloaded. A