@extends('layouts.dashboard') @section('content') @php /** @var array $categories @var array $items @var array $ingredients @var array $itemIngredients @var array $schedules @var array $pager @var array|null $restaurant */ use App\Models\Ingredient; use App\Services\MenuSchedule; $catName = []; foreach ($categories as $c) { $catName[(int) $c['id']] = $c['name']; } $ingredients = $ingredients ?? []; $itemIngredients = $itemIngredients ?? []; $schedules = $schedules ?? []; $schedName = []; foreach ($schedules as $s) { $schedName[(int) $s['id']] = (string) $s['name']; } $summary = $summary ?? null; $allowance = $allowance ?? ['limit' => 0, 'used' => 0, 'unlimited' => true, 'remaining' => 0, 'reached' => false, 'plan_name' => '']; $filter = $filter ?? ''; $sort = $sort ?? 'menu'; $filterOpts = [ '' => t_raw('vendor.menu.filter_all'), 'available' => t_raw('vendor.menu.filter_available'), 'unavailable' => t_raw('vendor.menu.filter_unavailable'), 'popular' => t_raw('vendor.menu.filter_popular'), 'never_sold' => t_raw('vendor.menu.filter_never_sold'), 'no_cost' => t_raw('vendor.menu.filter_no_cost'), // The KPI links here, so the number is a job rather than a fact. 'uncategorised' => t_raw('vendor.menu.filter_uncategorised'), 'low_stock' => t_raw('vendor.menu.filter_low_stock'), ]; $sortOpts = [ 'menu' => t_raw('vendor.menu.sort_menu_order'), 'best' => t_raw('vendor.menu.sort_best_selling'), 'worst' => t_raw('vendor.menu.sort_worst_selling'), 'revenue' => t_raw('vendor.menu.sort_revenue'), 'margin' => t_raw('vendor.menu.sort_margin'), 'last_sold' => t_raw('vendor.menu.sort_last_sold'), // By the section's NAME, which is what a vendor recognises — "menu order" sorts by the id // the sections happened to be created in. 'category' => t_raw('vendor.menu.sort_category'), 'schedule' => t_raw('vendor.menu.sort_schedule'), 'name' => t_raw('vendor.menu.sort_name'), 'price' => t_raw('vendor.menu.sort_price'), // Where an import lands. Without it, "view the menu" after adding 200 dishes meant page 1 // of everything, with no way to see which dishes had just arrived. 'new' => t_raw('vendor.menu.sort_new'), ]; // Every control on the bar counts, not just the two that shipped first. The category and // schedule pickers narrow the list as well, and leaving them out of this flag told a vendor // whose chosen section simply holds no dishes "No menu items yet" — with an add-your-first-item // CTA — over a full menu, while Reset sat there looking as though nothing were applied. $hasMenuFilters = $filter !== '' || $sort !== 'menu' || (int) ($activeCategory ?? 0) > 0 || (int) ($activeSchedule ?? 0) > 0 || trim((string) ($search ?? '')) !== ''; // Quantities read as "3" rather than "3.00" — a kitchen counts in whole units far // more often than in fractions, and the decimals only appear when they are real. $fmtQty = static fn ($v): string => (rtrim(rtrim(number_format((float) $v, 2, '.', ''), '0'), '.') ?: '0'); $addBlocked = !$categories || $allowance['reached']; // Import is reached from the tab strip, not a button: it had one in the chrome AND one in the // page, which read as two different features. Adding a dish is an action ON this list, so it // lives in the list's own card head — the subheader carries no action at all here. $canImport = $canImport ?? (\App\Services\AiFeature::usable(\App\Services\AiFeature::MENU) || \App\Services\GoogleBusiness::enabled()); $catCounts = $catCounts ?? []; $draftCount = (int) ($draftCount ?? 0); $drafts = $drafts ?? []; $exportQs = (string) ($exportQs ?? ""); $sectionCount = count($categories); // Dragging writes the STORED order, so it is offered only while the list is showing it. Under // "best selling" or "price" the rows on screen are a reading of the menu, not its arrangement, // and a drop there would save that reading as the menu. A filter is fine — it narrows which rows // are on screen, and each one still keeps the slot it holds in the whole list. $orderIds = $orderIds ?? []; $reorderable = $sort === 'menu' && count($orderIds) > 1; $itemCols = $reorderable ? 7 : 6; @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
{{ t_raw('vendor.menu.allowance_reached_notice', [':plan' => (string) $allowance['plan_name'], ':limit' => number_format((int) $allowance['limit'])]) }}
| {{ t_raw('vendor.menu.reorder_label') }} | @endif{{ t_raw('vendor.menu.col_item') }} | {{ t_raw('common.price') }} | {{ t_raw('vendor.menu.col_sold') }} | {{ t_raw('vendor.menu.col_stock') }} | {{ t_raw('vendor.menu.available_label') }} | {{ t_raw('common.actions') }} |
|---|---|---|---|---|---|---|
| @endif |
{{ $it['name'] }} {!! !empty($it['is_popular']) ? '' . t('vendor.menu.popular_pill') . '' : '' !!}@if ($ingCount) {{ $ingCount }}@endif@if (!empty($catName[(int) $it['menu_category_id']])){{ $catName[(int) $it['menu_category_id']] }}@endif
|
@php /* Price and the margin it earns belong together: a price on its
own cannot tell an owner whether the dish is worth cooking. */
@endphp
@php $margin = $it['margin_pct'] ?? null;
@endphp
{{ money($it['price']) }}
@if ($margin !== null)
@php $mTone = (float) $margin >= 60 ? 'green' : ((float) $margin >= 35 ? 'amber' : 'red'); @endphp
{{ t_raw('vendor.menu.margin_pill', [':pct' => number_format((float) $margin, 1)]) }}
{{ t_raw('vendor.menu.cost_line', [':amount' => money($it['cost'])]) }}
@else
{{ t_raw('vendor.menu.cost_missing') }}
@endif
|
@php /* Units sold and when it last sold — the two facts that decide
whether a dish stays on the menu. */
@endphp
@php $units = (int) ($it['units_sold'] ?? 0);
@endphp
@if ($units > 0)
{{ t_raw('vendor.menu.units_value', [':n' => number_format($units)]) }}
{{ money($it['revenue']) }}
{!! t_raw('vendor.menu.last_sold_line', [':date' => fmt_day($it['last_sold'], true)]) !!}
@else
{{ t_raw('vendor.menu.never_sold_pill') }}
@endif
|
@php /* The countable stock behind the dish, linked straight to its
ledger — a dish that is "available" but out of stock is the single
most expensive thing a menu can be wrong about. */
@endphp
@php $stockId = (int) ($it['stock_id'] ?? 0);
@endphp
@if ($stockId === 0)
{!! dash() !!}
@else
@php
$onHand = (float) $it['stock_on_hand'];
$lowAt = (float) $it['stock_low'];
$sState = $onHand <= 0 ? 'red' : (($lowAt > 0 && $onHand <= $lowAt) ? 'amber' : 'green');
$sLabel = $onHand <= 0 ? t('vendor.inventory.state_out') : (($lowAt > 0 && $onHand <= $lowAt) ? t('vendor.inventory.state_low') : t('vendor.inventory.state_in_stock'));
@endphp
{!! $sLabel !!}
{!! $fmtQty($onHand) !!} {{ (string) $it['stock_unit'] }} · {{ (string) $it['stock_name'] }}
@if ((int) $it['stock_track'] !== 1){{ t_raw('vendor.menu.stock_not_tracked') }} @endif
@endif
|