@extends('layouts.dashboard') @section('content') @php /** * Vendor → Cash drawer: till sessions with a BLIND cash-up. * * While a session is open and uncounted the controller does not compute expected cash and * does not send the components it could be derived from, so there is nothing on this page * for an operator to copy into the count field. Counting is the first step, not the last. * * @var array|null $open the open shift row (or null) * @var array|null $openTotals ledger totals — present ONLY once the drawer is counted * @var float|null $expected expected cash — present ONLY once the drawer is counted * @var float|null $declared what the operator counted, this cash-up * @var float|null $variance counted − expected, once both exist * @var array $movementCounts how many movements of each kind this session * @var array $openMovements recent movements of the open shift * @var array|null $session non-cash trading figures for the open session * @var array $closed closed sessions (newest first) * @var array $trendChart @var array $trendMeta @var array $trendStats */ $typeLabels = ['sale' => t_raw('vendor.shifts.type_sale'), 'refund' => t_raw('vendor.shifts.type_refund'), 'payin' => t_raw('vendor.shifts.type_payin'), 'payout' => t_raw('vendor.shifts.type_payout'), 'drop' => t_raw('vendor.shifts.type_drop')]; $typePills = ['sale' => 'qm-pill-green', 'refund' => 'qm-pill-red', 'payin' => 'qm-pill-blue', 'payout' => 'qm-pill-amber', 'drop' => 'qm-pill-grey']; $declared = $declared ?? null; $counted = $declared !== null; // the one switch this whole screen turns on $movementCounts = $movementCounts ?? []; $session = $session ?? null; $trendStats = $trendStats ?? ['short' => 0, 'over' => 0, 'balanced' => 0, 'worst' => 0.0, 'sessions' => 0]; // The shift history takes no filters beyond the restaurant scope, so the export needs no query string. $qs = ''; @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
@if ($open)

{{ t_raw('vendor.shifts.drawer_open_title') }}

{!! t_raw('vendor.shifts.drawer_open_sub', [':time' => fmt_datetime($open['opened_at'])]) !!}

{{ t_raw('vendor.shifts.xreport_link') }}
@php /* Non-cash trading figures. None of these is money in the drawer, so none of them can be added up into the expected total the count has to be blind to. */ @endphp @if ($session)
{{ t_raw('vendor.shifts.stat_orders_placed') }}{{ number_format((int) $session['orders_c']) }} {{ t_raw('common.cancelled') }}{{ number_format((int) $session['cancelled_c']) }} {{ t_raw('vendor.shifts.stat_tips') }}{{ money($session['tips_s']) }} {{ t_raw('vendor.shifts.stat_movements') }}{{ number_format(array_sum($movementCounts)) }}
@endif
@php /* THE CASH-UP. Before a count exists this section offers exactly one thing to do — count the drawer — and shows no cash figure of any kind. Afterwards the same section reveals what was expected, the variance it implies, and the confirmation that seals the session. */ @endphp

{{ t_raw('vendor.shifts.cashup_title') }}

@if (!$counted)

{{ t_raw('vendor.shifts.cashup_blind_hint') }}

@csrf
@else
{{ t_raw('vendor.shifts.col_counted') }}{{ money($declared) }} {{ t_raw('vendor.shifts.col_expected') }}{{ money($expected) }} {{ t_raw('vendor.shifts.col_variance') }}@php if ($variance == 0.0) { echo '' . t('vendor.shifts.balanced_pill') . ''; } elseif ($variance > 0) { echo '' . money($variance) . ' ' . t('vendor.shifts.variance_over_suffix') . ''; } else { echo '' . money(abs($variance)) . ' ' . t('vendor.shifts.variance_short_suffix') . ''; } @endphp {{ t_raw('vendor.shifts.stat_opening_float') }}{{ money($open['opening_float']) }} {{ t_raw('vendor.shifts.stat_cash_sales') }}{{ money($openTotals['sale']) }} {{ t_raw('vendor.shifts.stat_payins') }}{{ money($openTotals['payin']) }} {{ t_raw('vendor.shifts.stat_payouts') }}{{ money($openTotals['payout']) }} {{ t_raw('vendor.shifts.stat_safe_drops') }}{{ money($openTotals['drop']) }} {{ t_raw('vendor.shifts.stat_cash_refunds') }}{{ money($openTotals['refund']) }}
@csrf
@csrf @if (($variance ?? 0.0) != 0.0) @endif
@endif

{{ t_raw('vendor.shifts.section_movements') }}

@if ($openMovements) @foreach ($openMovements as $m) @php /* The audit trail stays fully readable while blind — who did what, when and why — because none of it is the number under control. The amounts are the number under control, so they wait for the count. */ @endphp @endforeach @else @partial('table-empty', ['colspan' => 6, 'icon' => 'fa-wallet', 'msg' => t_raw('vendor.shifts.empty_open_movements')]) @endif
{{ t_raw('vendor.shifts.movements_caption') }}
{{ t_raw('common.when') }}{{ t_raw('common.type') }}{{ t_raw('common.amount') }}{{ t_raw('vendor.shifts.col_order') }}{{ t_raw('vendor.shifts.col_by') }}{{ t_raw('common.note') }}
{!! fmt_clock($m['created_at']) !!} {{ $typeLabels[$m['type']] ?? ucfirst((string) $m['type']) }}@if ($counted){{ in_array($m['type'], ['refund', 'payout', 'drop'], true) ? '−' : '+' }}{{ money($m['amount']) }}@else{{ t_raw('vendor.shifts.hidden_until_counted') }}@endif {!! or_dash($m['order_number'] ?? null) !!} {!! or_dash($m['user_name'] ?? null) !!} {!! or_dash($m['note'] ?? null) !!}
@else

{{ t_raw('vendor.shifts.drawer_closed_title') }}

{{ t_raw('vendor.shifts.drawer_closed_sub') }}

@csrf
@endif @php /* One session being a few cents out is noise; the same drawer being short every Friday is a finding. Only a run of closed sessions can tell those apart. */ @endphp @if ((int) $trendStats['sessions'] > 0)
@partial('chart-card', [ 'heading' => t_raw('vendor.shifts.trend_title'), 'sub' => t_raw('vendor.shifts.trend_sub'), 'cfg' => $trendChart, 'meta' => $trendMeta, 'icon' => 'fa-chart-bar', 'accent' => 'blue', ])

{{ t_raw('vendor.shifts.trend_summary_title') }}

{{ t_raw('vendor.shifts.trend_sessions') }}{{ number_format((int) $trendStats['sessions']) }} {{ t_raw('vendor.shifts.balanced_pill') }}{{ number_format((int) $trendStats['balanced']) }} {{ t_raw('vendor.shifts.trend_short') }}{{ number_format((int) $trendStats['short']) }} {{ t_raw('vendor.shifts.trend_over') }}{{ number_format((int) $trendStats['over']) }} {{ t_raw('vendor.shifts.trend_worst') }}{!! (float) $trendStats['worst'] == 0.0 ? t('vendor.shifts.balanced_pill') : ((float) $trendStats['worst'] > 0 ? '+' : '−') . money(abs((float) $trendStats['worst'])) !!}
@endif

{{ t_raw('vendor.shifts.past_sessions_title') }}

{{ t_raw('common.export_csv') }}
@php /* Opened/Closed render via fmt_datetime ("Jul 19, 2026 · 3:40 PM"); Date.parse() can't read that format, so those two columns sort on the raw stamp instead. */ @endphp
@if ($closed) @foreach ($closed as $s) @php $v = $s['variance'] !== null ? (float) $s['variance'] : 0.0; @endphp @endforeach @else @partial('table-empty', ['colspan' => 7, 'icon' => 'fa-file-alt', 'msg' => t_raw('vendor.shifts.empty_closed_sessions')]) @endif
{{ t_raw('vendor.shifts.col_opened') }}{{ t_raw('vendor.shifts.col_closed') }}{{ t_raw('vendor.shifts.col_float') }}{{ t_raw('vendor.shifts.col_expected') }}{{ t_raw('vendor.shifts.col_counted') }}{{ t_raw('vendor.shifts.col_variance') }}{{ t_raw('common.actions') }}
{!! fmt_datetime($s['opened_at']) !!}
{{ (string) ($s['opened_by'] ?? '') }}
{!! fmt_datetime($s['closed_at']) !!}
{{ (string) ($s['closed_by'] ?? '') }}
{{ money($s['opening_float']) }} {!! $s['expected_cash'] !== null ? money($s['expected_cash']) : dash() !!} {!! $s['counted_cash'] !== null ? money($s['counted_cash']) : dash() !!} @if ($v == 0.0){{ t_raw('vendor.shifts.balanced_pill') }} @elseif ($v > 0){{ money($v) }} {{ t_raw('vendor.shifts.variance_over_suffix') }} @else{{ money(abs($v)) }} {{ t_raw('vendor.shifts.variance_short_suffix') }}@endif @if (!empty($s['variance_reason']))
{{ (string) $s['variance_reason'] }}
@endif
{{ t_raw('vendor.shifts.zreport_link') }}
@partial('table-toolbar', ['pager' => $pager, 'base' => '/vendor/shifts'])
@php /* /.qm-d2 */ @endphp @endsection