@extends('layouts.dashboard') @section('content') @php use App\Models\Reservation; /** @var iterable $list @var string $date @var string $q @var array $tableLabels @var array $labels */ $tableLabels = $tableLabels ?? []; $labels = $labels ?? []; // Occasion id => row (for list pills) and the active set (for the legend / dropdowns). $labelMap = []; $activeLabels = []; foreach ($labels as $lbl) { $labelMap[(int) $lbl['id']] = $lbl; if ((int) ($lbl['is_active'] ?? 1) === 1) { $activeLabels[] = $lbl; } } // Friendly date for the empty state / chips (e.g. "Jun 26, 2026"). $dateLabel = fmt_day($date); // Action button labels keyed by the target status. $actionLabels = ['confirmed' => t_raw('vendor.reservations.action_confirm'), 'seated' => t_raw('vendor.reservations.action_seat'), 'completed' => t_raw('vendor.reservations.action_complete'), 'cancelled' => t_raw('common.cancel'), 'no_show' => t_raw('vendor.reservations.action_no_show'), 'waitlisted' => t_raw('vendor.reservations.action_waitlist')]; // Status changes that should ask for confirmation first. $destructive = ['cancelled', 'no_show']; // Configuring occasions and closed dates is a management-level setting, not a host // task. The controller already blocks other roles (requireManagement); we also hide the // controls here so host staff aren't shown actions they cannot perform. $isManagement = \App\Support\Auth::can('reservations.manage'); // Is the day's list narrowed by anything? Drives the Reset link's state and the empty // state below, which otherwise announces "nothing is booked for this day" on a day // that has bookings sitting outside the narrowed set. $hasFilters = $q !== '' || ($filters['status'] ?? '') !== '' || ($filters['source'] ?? '') !== '' || (int) ($filters['occasion'] ?? 0) > 0; // Carry the active date + search + status / source / occasion onto the export link. $qs = http_build_query(array_filter([ 'date' => $date, 'status' => $filters['status'] ?? '', 'source' => $filters['source'] ?? '', 'occasion' => (int) ($filters['occasion'] ?? 0), 'q' => $q, ], static fn ($v) => (string) $v !== '' && (string) $v !== '0')); @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
@php // Page primary action → top-bar subheader slot (leaves the table card header for the // title + any table-scoped utility). ob_start captures the button HTML for the layout slot. ob_start(); @endphp {{ t_raw('vendor.reservations.new_btn') }} @php \App\Support\View::slot('subhead', ob_get_clean()); @endphp

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

{{ t_raw('vendor.reservations.subtitle') }}

{{ t_raw('common.export_csv') }} @if ($isManagement) @endif
@php $filters = $filters ?? ['status' => '', 'source' => '', 'occasion' => 0]; $sources = ['staff' => t_raw('status.reservation_source.staff'), 'website' => t_raw('status.reservation_source.website'), 'phone' => t_raw('status.reservation_source.phone'), 'walk_in' => t_raw('status.reservation_source.walk_in'), 'admin' => t_raw('status.reservation_source.admin')]; @endphp
@partial('filter-collapse')
@php /* type="search", as every other filter bar's search box is: qm-table-tools.js routes as-you-type filtering through `field.type === 'search'` only, so as a plain text box this one was invisible to that handler and re-queried on blur instead — via the change fallback written for the selects and date fields. */ @endphp
@if ($activeLabels)
@endif @php /* .qm-of-actions, not a bare flex row: this is the same actions slot every other filter bar uses, and it carries the margin that pushes Reset to the far end. */ @endphp
@php /* No status-count chip strip here: it duplicated the Status select in the filter row above as a second way to apply the same filter. */ @endphp
@if (count($list) === 0)
@php /* An empty $list means one of two different things and they must not read alike: the day is genuinely free, or the filters matched nothing on a day that is fully booked. Same two-branch shape as the admin twin (admin/reservations.php:100), and when the filters are what emptied it the useful action is clearing them, not adding a booking. */ @endphp @if ($hasFilters)

{{ t_raw('vendor.reservations.empty_filtered_title', [':date' => $dateLabel]) }}

{{ t_raw('vendor.reservations.empty_filtered') }}

{{ t_raw('common.reset') }} @else

{{ t_raw('vendor.reservations.empty_title', [':date' => $dateLabel]) }}

{{ t_raw('vendor.reservations.empty_text') }}

{{ t_raw('vendor.reservations.add_reservation_btn') }} @endif
@else

{{ t_raw('vendor.reservations.list_table_heading', [':date' => $dateLabel]) }}

@php /* Time renders via fmt_clock ("3:40 PM"); Date.parse() can't read that format, so that column carries no data-sort. */ @endphp @foreach ($list as $r) @php $rid = (int) $r['id']; $status = (string) $r['status']; $tableId = (int) ($r['table_id'] ?? 0); $transitions = Reservation::transitions()[$status] ?? []; $canEdit = !in_array($status, ['completed', 'cancelled', 'no_show'], true); // A walk-in seated without a name is stored EMPTY, not NULL (guest_name is NOT NULL, // and walkIn() writes ''), so `?? guest_fallback` was a guard that could never fire: // the monogram beside the name came out as "#" while the name beside it read "Guest". // Decided once here, on the trimmed value, and used by both — the same way the // dashboard's today-reservations card does it. $rName = trim((string) ($r['guest_name'] ?? '')) !== '' ? (string) $r['guest_name'] : t_raw('vendor.reservations.guest_fallback'); @endphp @php $srcKey = (string) ($r['source'] ?? 'staff'); @endphp @php $advance = []; foreach ($transitions as $to) { $isDestructive = in_array($to, $destructive, true); $advance[] = [ 'label' => $actionLabels[$to] ?? ucfirst($to), 'icon' => $isDestructive ? 'fa-ban' : 'fa-arrow-right', 'danger' => $isDestructive, 'form' => [ 'action' => '/vendor/reservations/status', 'fields' => ['id' => $rid, 'date' => $date, 'status' => $to], 'confirm' => $isDestructive ? t_raw('vendor.reservations.confirm_status_body', [':action' => $actionLabels[$to] ?? $to]) : '', ], ]; } @endphp @endforeach
{{ t_raw('vendor.reservations.col_booking') }}{{ t_raw('common.time') }}{{ t_raw('vendor.reservations.col_guest') }}{{ t_raw('vendor.reservations.party_label') }}{{ t_raw('vendor.reservations.table_label') }}{{ t_raw('vendor.reservations.col_source') }}{{ t_raw('common.note') }}{{ t_raw('common.status') }}{{ t_raw('common.actions') }}
@if ($canEdit)#{{ $rid }}@else#{{ $rid }}@endif {!! fmt_clock($r['reserved_at']) !!}
{!! brandmark_html($r['avatar'] ?? null, $rName, 'md', 'qm-brandmark--round') !!}
{{ $rName }} @php $lid = (int) ($r['label_id'] ?? 0); @endphp @if ($lid && isset($labelMap[$lid])) {{ $labelMap[$lid]['name'] }} @endif
@if (!empty($r['guest_phone'])){{ $r['guest_phone'] }}@endif @if (!empty($r['guest_email'])){{ $r['guest_email'] }}@endif @php /* M46. notified_at is the guard that stops a second "your table is ready"; showing it is how a host answers "was that guest told?" without opening the SMS log. Same line as the two above. */ @endphp @if (!empty($r['notified_at'])){!! t_raw('vendor.reservations.notified_at', [':time' => fmt_clock($r['notified_at'])]) !!}@endif
{{ (int) $r['party_size'] }} {!! or_dash($tableId ? ($tableLabels[$tableId] ?? null) : null) !!} {{ Reservation::sourceLabel($srcKey) }} @if (!empty($r['special_requests']))
{{ $r['special_requests'] }}
@endif @if (!empty($r['staff_notes']))
{{ $r['staff_notes'] }}
@endif @if (empty($r['special_requests']) && empty($r['staff_notes'])){!! dash() !!}@endif
{{ Reservation::statusLabel($status) }} @partial('row-actions', ['actions' => array_merge( [$canEdit ? ['label' => t_raw('vendor.reservations.edit_aria'), 'icon' => 'fa-pen', 'href' => '/vendor/reservations/' . $rid . '/edit', 'primary' => true] : null], $advance )])
@php if (!empty($pagination)) { echo partial('table-toolbar', ['pager' => $pagination]); } @endphp
@endif
@if ($activeLabels)
{{ t_raw('vendor.reservations.occasions_legend') }} @foreach ($activeLabels as $lbl) {{ $lbl['name'] }} @endforeach
@endif
@php /* /.qm-d2 */ @endphp @endsection