@extends('layouts.dashboard') @section('content') @php use App\Models\Order; /** @var iterable $orders @var array $statuses @var iterable $drivers @var array $driverNames @var array $itemCounts @var array $pager @var array $filters @var array $totals */ $drivers = $drivers ?? []; $driverNames = $driverNames ?? []; $driverAvatars = $driverAvatars ?? []; $itemCounts = $itemCounts ?? []; $pager = $pager ?? null; $filters = $filters ?? []; $totals = $totals ?? ['count' => 0, 'total' => 0.0, 'paid_total' => 0.0, 'by_tender' => []]; $f = static fn (string $k): string => (string) ($filters[$k] ?? ''); $hasFilters = trim(implode('', array_map($f, ['date_from', 'date_to', 'status', 'payment_method', 'payment_status', 'q']))) !== ''; // Preserve the active filters on the export link. $qs = http_build_query(array_filter([ 'date_from' => $f('date_from'), 'date_to' => $f('date_to'), 'status' => $f('status'), 'payment_method' => $f('payment_method'), 'payment_status' => $f('payment_status'), 'q' => $f('q'), ], static fn ($v) => $v !== '')); // Labels the payment-method filter offers (the per-order payment column was removed; // filtering by how an order was paid is still useful even when it isn't a column). // Stripe is named rather than folded into "Card": offline already carries that label, so the // select drew two options both reading "Card" and neither said which tender it would pick. // Same naming the tender strip below and the shift report use for these two keys. $methodLabel = ['cod' => t_raw('status.payment_method.cash'), 'offline' => t_raw('status.payment_method.card'), 'stripe' => 'Stripe', 'paypal' => 'PayPal']; // The totals strip reads the payment LEDGER, whose tender keys differ from // orders.payment_method (a split bill can be cash + card). $tenderLabel = ['cash' => t_raw('status.payment_method.cash'), 'card' => t_raw('status.payment_method.card'), 'offline' => t_raw('dashboard.status_offline'), 'stripe' => 'Stripe', 'paypal' => 'PayPal', 'other' => t_raw('status.payment_method.other')]; // Managers run the floor but do not see the owner's takings reconciliation // (the Collected/by-tender strip). $showMoney = $showMoney ?? true; $statusOpts = ['pending', 'confirmed', 'preparing', 'out_for_delivery', 'delivered', 'cancelled']; $kpiSparks = $kpiSparks ?? []; // Per-tender takings-card icon + accent, so this strip mirrors the analytics KPIs. $tenderIcon = ['cash' => 'fa-money-bill-wave', 'card' => 'fa-credit-card', 'offline' => 'fa-credit-card', 'stripe' => 'fa-credit-card', 'paypal' => 'fa-wallet', 'other' => 'fa-wallet']; $tenderAccent = ['cash' => 'teal', 'card' => 'violet', 'offline' => 'violet', 'stripe' => 'blue', 'paypal' => 'blue', 'other' => 'slate']; // Where the order was raised, and how it is being served — the two facts that decide // which part of the operation owns it. $sourceLabel = ['website' => t_raw('status.order_source.website'), 'kiosk' => t_raw('status.order_source.kiosk'), 'pos' => t_raw('status.order_source.pos'), 'phone' => t_raw('status.order_source.phone')]; $sourceIcon = ['website' => 'fa-globe', 'kiosk' => 'fa-tablet-alt', 'pos' => 'fa-cash-register', 'phone' => 'fa-phone']; $modeLabel = ['delivery' => t_raw('common.delivery'), 'pickup' => t_raw('common.pickup'), 'dine_in' => t_raw('common.dine_in')]; $refunds = $refunds ?? null; $canRefund = $canRefund ?? false; // THE CONTROL MUST ASK THE GUARD'S OWN QUESTION — the same mistake the refund control // carried. The Export CSV button below asked $showMoney (finance.view, the financial READ // privilege) while the endpoint it points at is guarded by 'orders.export'. Those are two // separately grantable privileges in the role grid, so a role composed with the export right // but not the figures was offered no button at all, and one composed the other way round was // offered a button the endpoint then refuses. $canExport = $canExport ?? \App\Support\Auth::can('orders.export'); // Fulfilment is judged against the kitchen's own target, so "42 min" reads as fast or // slow without the operator holding the target in their head. $prepTarget = max(1, (int) ($restaurant['avg_prep_time'] ?? 30)); @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
@php /* Takings KPIs — the reconciliation snapshot for the current filter set, lifted above the list as its own section. Owner sees the money cards; a manager sees the order count only (the same takings mask used elsewhere). */ @endphp
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'blue', 'icon' => 'fa-receipt', 'label' => t_raw('nav.orders'), 'value' => number_format((int) $totals['count']), 'spark' => $kpiSparks['orders'] ?? [], 'meta' => '' . t('vendor.orders.kpi_meta_view') . '']) @if ($showMoney) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'green', 'icon' => 'fa-coins', 'label' => t_raw('common.collected'), 'value' => money($totals['paid_total']), 'spark' => $kpiSparks['collected'] ?? [], 'meta' => '' . t('vendor.orders.kpi_meta_paid_takings') . '']) @php /* Refunds sit beside the takings, never buried in them: the by-tender cards are already net, so a day that took 1,000 and gave 200 back would otherwise be indistinguishable from a quiet 800 day. */ @endphp @if ($refunds !== null) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'rose', 'icon' => 'fa-undo', 'label' => t_raw('vendor.orders.kpi_refunded'), 'value' => money($refunds['total']), 'meta' => '' . t('vendor.orders.kpi_meta_refunded', [':n' => number_format((int) $refunds['count'])]) . '']) @endif @foreach (($totals['by_tender'] ?? []) as $m => $sum) @partial('kpi-card', ['variant' => 'panel', 'accent' => $tenderAccent[$m] ?? 'slate', 'icon' => $tenderIcon[$m] ?? 'fa-wallet', 'label' => (string) ($tenderLabel[$m] ?? ucfirst((string) $m)), 'value' => money($sum), 'spark' => $kpiSparks[$m] ?? [], 'meta' => '' . t('vendor.orders.kpi_meta_collected') . '']) @endforeach @endif

{{ t_raw('vendor.orders.list_heading') }}

@if ($canExport) {{ t_raw('common.export_csv') }} @endif
@php /* Reconciliation filter bar — total a day's takings by tender at close. */ @endphp
@partial('filter-collapse')
@php /* Placed renders via fmt_datetime ("Jul 19, 2026 · 3:40 PM"); Date.parse() can't read that format, so that column carries no data-sort. Rider is an inline assignment form, not sortable content, so it carries none either. */ @endphp @php /* Channel, item count and service mode describe the order rather than standing on their own, so they ride under the order number in the identity cell; fulfilment time rides under the placed time it is measured from. Three columns of one-word answers made the row wider than the information in it. */ @endphp @if (count($orders)) @foreach ($orders as $o) @php $isDelivery = ($o['service_mode'] ?? '') === 'delivery'; $noRider = $isDelivery && (int) ($o['driver_user_id'] ?? 0) === 0 && in_array($o['status'], ['confirmed', 'preparing', 'out_for_delivery'], true); @endphp @php /* Placed, then how long it took — the duration is measured from this very timestamp, so the two belong in one cell. Fulfilment is judged against the kitchen's own target so the number carries a verdict, not just a duration; an order still in flight has none yet. */ @endphp @php $fm = $o['fulfilment_minutes'] ?? null; @endphp @php /* Actions read the same here as on the admin orders table: the one thing done constantly stays inline, everything else folds behind the "⋮". Advancing an order used to be a select plus a Save press; each reachable status is now its own entry, so the common move costs one click and the states the machine forbids are simply absent rather than offered and rejected. */ @endphp @php $advance = []; foreach (Order::nextStatuses($o['status']) as $s) { if ($s === $o['status']) { continue; } // the status it already holds is not a move $advance[] = [ 'label' => Order::statusLabel($s, $o['service_mode'] ?? 'delivery'), 'icon' => $s === 'cancelled' ? 'fa-ban' : 'fa-arrow-right', 'danger' => $s === 'cancelled', 'form' => [ 'action' => '/vendor/orders/status', 'fields' => ['order_id' => (int) $o['id'], 'status' => $s], ], ]; } @endphp @endforeach @else @partial('table-empty', ['colspan' => 7, 'icon' => 'fa-receipt', 'msg' => $hasFilters ? t_raw('vendor.orders.empty_filtered') : t_raw('vendor.orders.empty_default')]) @endif
{{ t_raw('vendor.orders.col_order') }}{{ t_raw('common.customer') }}{{ t_raw('common.total') }}{{ t_raw('vendor.orders.col_placed') }}{{ t_raw('common.status') }}{{ t_raw('vendor.orders.col_rider') }}{{ t_raw('common.actions') }}
{{ $o['order_number'] }} @if (!empty($o['first_item_name'])) @php $more = (int) ($itemCounts[(int) $o['id']] ?? 0) - 1; @endphp{{ (string) $o['first_item_name'] }}{!! $more > 0 ? ' ' . t('vendor.orders.n_more', [':n' => $more]) : '' !!}@endif @php // Who raised the order and how it is being served — the two facts that // decide which part of the operation owns it — plus the line count. $src = (string) ($o['source'] ?? 'website'); $mode = (string) ($o['service_mode'] ?? 'delivery'); $ic = (int) ($itemCounts[(int) $o['id']] ?? 0); @endphp
{{ $sourceLabel[$src] ?? $src }} {{ $modeLabel[$mode] ?? $mode }} · {{ $ic }} {{ t_raw($ic === 1 ? 'vendor.orders.item_singular' : 'vendor.orders.item_plural') }}
@if (!empty($o['loyalty_reward_label'])){{ t_raw('vendor.orders.reward_prefix') }}{{ (string) $o['loyalty_reward_label'] }}@endif @if (!empty($o['scheduled_at'])) {!! fmt_datetime($o['scheduled_at']) !!}@endif
{!! brandmark_html($o['customer_avatar'] ?? null, (string) $o['customer_name'], 'md', 'qm-brandmark--round') !!}
{{ $o['customer_name'] }} @if (!empty($o['customer_phone'])){{ $o['customer_phone'] }}@endif
{{ money($o['total']) }} {!! fmt_datetime($o['placed_at']) !!} @if ($fm !== null) @php $tone = $fm <= $prepTarget ? 'green' : ($fm <= $prepTarget * 1.5 ? 'amber' : 'red'); @endphp
{{ t_raw('vendor.orders.minutes_value', [':n' => number_format($fm)]) }}
@endif
{{ Order::statusLabel($o['status'], $o['service_mode'] ?? 'delivery') }} @php $terminalOrder = in_array($o['status'], ['delivered', 'cancelled'], true); $assignedId = (int) ($o['driver_user_id'] ?? 0); @endphp @if (!$isDelivery) {!! dash() !!} @else @if (!$terminalOrder) @php /* ONE control, not three labels: the avatar dropdown IS the state. Its selection names the rider (or Unassigned), the rider's photo rides in the closed control and in the list (the same decoration the cuisine picker's flags use), and choosing submits by itself — an assignment made in a dropdown needs no Assign button. */ @endphp
@csrf
@elseif ($assignedId) @php /* Settled orders: the rider is a fact now, not a control. */ @endphp {{ $driverNames[$assignedId] ?? t_raw('vendor.orders.rider_fallback_prefix', [':id' => $assignedId]) }} @else {!! dash() !!} @endif @endif
{!! partial('row-actions', ['actions' => array_merge( [['label' => t_raw('common.view'), 'icon' => 'fa-eye', 'href' => '/vendor/orders/' . (int) $o['id'], 'primary' => true]], /* A delivered order's journey is OVER: its slot shows a green check — a state mark, not a control — where an active row shows its next-move arrow (dimmed until hovered; see the actions-cell CSS). */ $o['status'] === 'delivered' ? [[ 'label' => Order::statusLabel('delivered', $o['service_mode'] ?? 'delivery'), 'icon' => 'fa-check', 'disabled' => true, 'attrs' => ['data-status-done' => '1'], ]] : [], $advance, [ /* Refund without cancelling. A completed order that went wrong still has to be made good, and forcing the operator to cancel it to move the money would falsify the service record. Owner-only, and only where there is captured money to return. */ ($canRefund && (string) $o['payment_status'] === 'paid' && (float) $o['total'] > 0) ? [ 'label' => t_raw('vendor.orders.refund_btn'), 'icon' => 'fa-undo', 'form' => [ 'action' => '/vendor/orders/status', 'fields' => ['intent' => 'refund', 'order_id' => (int) $o['id']], 'confirm' => t_raw('vendor.orders.confirm_refund', [':amount' => money($o['total']), ':number' => $o['order_number']]), ], ] : null, ] )]) !!}
@if ($pager)@partial('table-toolbar', ['pager' => $pager, 'base' => '/vendor/orders'])@endif
@php /* /.qm-d2 */ @endphp @endsection