@extends('layouts.dashboard')
@section('content')
@php
use App\Models\Order;
/** @var array $kpis @var array $recent @var array|null $restaurant */
/** @var array $revenueChart @var array $ordersChart @var array $statusChart @var array $topItemsChart @var array $chartMeta */
/** @var array $revenueRanges @var array $ordersRanges @var array $statusRanges @var array $kpiTrends */
$chartMeta = $chartMeta ?? [];
$chartMeta += ['revenue' => [], 'status' => [], 'top' => [], 'orders' => []];
$kpiTrends = $kpiTrends ?? ['orders' => ['pct' => 0, 'dir' => 'flat'], 'revenue' => ['pct' => 0, 'dir' => 'flat']];
$kpiSparks = $kpiSparks ?? [];
// Financial figures are owner-only: managers run operations without them.
$showMoney = $showMoney ?? true;
@endphp
@php /* .qm-d2 scopes the dashboard density layer (app.css §35) — the same
standard the admin dashboard set: same partials, same keys. */
@endphp
@if (!$restaurant)
{{ t_raw('vendor.dashboard.no_restaurant_msg') }}
@else
@if ($restaurant['status'] === 'pending')
{!! t_raw('vendor.dashboard.pending_review_banner', [':name' => '' . e($restaurant['name']) . ' ']) !!}
@endif
@php
$rightNow = $rightNow ?? null;
$stockAlerts = $stockAlerts ?? ['out' => 0, 'low' => 0];
@endphp
@if ($stockAlerts['out'] > 0 || $stockAlerts['low'] > 0)
@if ($stockAlerts['out'] > 0){!! t_raw($stockAlerts['out'] > 1 ? 'vendor.dashboard.stock_alert_out_many' : 'vendor.dashboard.stock_alert_out_one', [':n' => '' . (int) $stockAlerts['out'] . ' ']) !!}@endif
@if ($stockAlerts['out'] > 0 && $stockAlerts['low'] > 0) · @endif
@if ($stockAlerts['low'] > 0){!! t_raw('vendor.dashboard.stock_alert_low', [':n' => '' . (int) $stockAlerts['low'] . ' ']) !!}@endif
— {{ t_raw('vendor.dashboard.stock_alert_review_link') }} .
@endif
@if ($rightNow)
@php /* Triage semantics (same as the admin strip): a queue with work is
`is-hot`, an empty one `is-calm`; the drawer tile is a status, so it
carries neither. */
@endphp
@php $rnState = static fn (int $n): string => $n > 0 ? ' is-hot' : ' is-calm';
@endphp
{{ t_raw('admin.dashboard.right_now') }}
{{ t_raw('common.live') }}
@endif
{{ t_raw('dashboard.sec.snapshot') }}
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'blue', 'icon' => 'fa-receipt', 'label' => t_raw('vendor.dashboard.kpi_total_orders'), 'href' => route('vendor.orders'),
'value' => number_format((int) $kpis['orders']), 'spark' => $kpiSparks['orders'] ?? [],
'meta' => kpi_trend((int) $kpiTrends['orders']['pct'], (string) $kpiTrends['orders']['dir']) . '' . t('vendor.dashboard.kpi_vs_last_week') . ' '])
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'amber', 'icon' => 'fa-bolt', 'label' => t_raw('vendor.dashboard.kpi_active_orders'), 'href' => route('vendor.orders'),
'value' => number_format((int) $kpis['active']), 'spark' => $kpiSparks['active'] ?? [],
'meta' => '' . t('vendor.dashboard.kpi_in_progress_now') . ' '])
@if ($showMoney)
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'green', 'icon' => 'fa-coins', 'label' => t_raw('common.revenue'), 'href' => route('vendor.orders'),
'value' => money($kpis['revenue']), 'spark' => $kpiSparks['revenue'] ?? [],
'meta' => kpi_trend((int) $kpiTrends['revenue']['pct'], (string) $kpiTrends['revenue']['dir']) . '' . t('vendor.dashboard.kpi_vs_last_week') . ' '])
@endif
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'violet', 'icon' => 'fa-utensils', 'label' => t_raw('vendor.dashboard.kpi_menu_items'), 'href' => route('vendor.menu'),
'value' => number_format((int) $kpis['menu']), 'spark' => $kpiSparks['menu'] ?? [],
'meta' => '' . t('vendor.dashboard.kpi_on_your_menu') . ' '])
@if (!empty($loyaltyOn))
@partial('kpi-card', ['variant' => 'panel', 'accent' => 'rose', 'icon' => 'fa-gift', 'label' => t_raw('vendor.dashboard.kpi_loyalty_points'), 'href' => route('vendor.loyalty'),
'value' => number_format((int) ($loyaltyOutstanding ?? 0)), 'spark' => $kpiSparks['loyalty'] ?? [],
'meta' => '' . t('vendor.dashboard.kpi_points_outstanding') . ' '])
@endif
@php
$trendOptions = ['7' => t_raw('common.range_7d'), '14' => t_raw('common.range_14d'), '30' => t_raw('common.range_30d'), '90' => t_raw('common.range_qtr')];
@endphp
{{ t_raw('dashboard.sec.trends') }}
@if ($showMoney)
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_revenue_trend_title'),
'sub' => t_raw('vendor.dashboard.chart_revenue_caption'),
'cfg' => $revenueChart,
'meta' => $chartMeta['revenue'],
'icon' => 'fa-chart-line',
'accent' => 'green',
'opts' => ['id' => 'revChart', 'ranges' => $revenueRanges, 'selected' => '90', 'options' => $trendOptions],
])
@else
@endif
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_orders_per_day_title'),
'sub' => t_raw('vendor.dashboard.chart_orders_caption'),
'cfg' => $ordersChart,
'meta' => $chartMeta['orders'],
'icon' => 'fa-chart-bar',
'accent' => 'blue',
'opts' => ['id' => 'ordersChart', 'ranges' => $ordersRanges, 'selected' => '90', 'options' => $trendOptions],
])
{{ t_raw('dashboard.sec.breakdown') }}
@php /* The Breakdown at 8 columns with the live-delivery map riding the other 4:
the four analytic cards keep their own 2x2 grid, and the map stretches the
section's full height beside them. */
@endphp
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_orders_status_title'),
'sub' => t_raw('vendor.dashboard.chart_status_caption'),
'cfg' => $statusChart,
'meta' => $chartMeta['status'],
'icon' => 'fa-chart-pie',
'accent' => 'violet',
'opts' => ['id' => 'statusChart', 'ranges' => $statusRanges, 'selected' => '90', 'options' => $trendOptions],
])
@partial('chart-card', [
'heading' => t_raw('vendor.dashboard.chart_top_items_title'),
'sub' => t_raw('vendor.dashboard.chart_top_items_caption'),
'cfg' => $topItemsChart,
'meta' => $chartMeta['top'],
'icon' => 'fa-chart-bar',
'accent' => 'amber',
'opts' => ['id' => 'topItemsChart', 'ranges' => $topItemsRanges ?? [], 'selected' => '90', 'options' => $trendOptions],
])
@if ($showMoney)
@partial('chart-card', [
'heading' => t_raw('vendor.dashboard.chart_tender_title'),
'sub' => t_raw('vendor.dashboard.chart_tender_caption'),
'cfg' => $tenderChart ?? [],
'meta' => $tenderMeta ?? [],
'icon' => 'fa-chart-pie',
'accent' => 'blue',
'opts' => ['id' => 'tenderChart', 'ranges' => $tenderRanges ?? [], 'selected' => '90', 'options' => $trendOptions],
])
@else
@endif
@partial('chart-card', [
'heading' => t_raw('vendor.dashboard.chart_hours_title'),
'sub' => t_raw('vendor.dashboard.chart_hours_caption'),
'cfg' => $hoursChart ?? [],
'meta' => $hoursMeta ?? [],
'icon' => 'fa-chart-bar',
'accent' => 'violet',
'opts' => ['id' => 'hoursChart', 'ranges' => $hoursRanges ?? [], 'selected' => '90', 'options' => $trendOptions],
])
@if (!empty($mapCenter))
@php /* The live tally rides the title row's trailing side. It counts ORDERS
out of the door — one rider carrying two orders is 2 deliveries here,
1 rider in the foot, and one pin on the map whose card lists both. */
@endphp
{{ t_raw('status.order.out_for_delivery') }}
{{ count($liveDeliveries ?? []) }} {{ t_raw('common.active') }}
@else
{{ t_raw('status.order.out_for_delivery') }}
{{ count($liveDeliveries ?? []) }} {{ t_raw('common.active') }}
{{ t_raw('common.view_all') }}
@endif
@if (!empty($mapCenter))
@php
// The restaurant anchors the room; every live driver rides as a labelled pin —
// the same driver_locations positions the customer tracking map shows.
$dlvPoints = [[
'lat' => (float) $mapCenter['lat'],
'lng' => (float) $mapCenter['lng'],
'label' => (string) ($mapRestaurant['name'] ?? ''),
'iconHtml' => restaurant_logo_html($mapRestaurant ?? [], 'sm', '', ''),
]];
/* ONE pin per rider, not per order: a rider carrying two orders reports one
position, and two pins stacked there leave the lower card unreachable.
The rider's avatar is the pin; the card lists EVERY order they carry —
see partials/map-driver-popup.php. */
$byRider = [];
foreach (($liveDeliveries ?? []) as $dlv) {
$key = (int) ($dlv['driver_user_id'] ?? 0);
$byRider[$key]['rider'] ??= $dlv;
$byRider[$key]['orders'][] = $dlv;
}
foreach ($byRider as $grp) {
$dlvPoints[] = [
'lat' => (float) $grp['rider']['lat'],
'lng' => (float) $grp['rider']['lng'],
'iconHtml' => brandmark_html($grp['rider']['driver_avatar'] ?? null, (string) $grp['rider']['driver_name'], 'sm', 'qm-brandmark--round'),
'popupHtml' => partial('map-driver-popup', ['dlv' => $grp['rider'], 'orders' => $grp['orders']]),
];
}
@endphp
@php $dlvRiders = count(array_unique(array_filter(array_map(
static fn (array $d): string => trim((string) ($d['driver_name'] ?? '')),
$liveDeliveries ?? []
))));
@endphp
@php /* Same floating foot as the admin locations map — the live tally and
the View-all corner riding the map's lower edge. */
@endphp
@endif
@php /* Business position — reputation, money owed, sponsored reach. Each card
is an excerpt of its own screen (same figures, same keys) with a
View-all into it; a card with nothing to say is simply not rendered. */
@endphp
@if (!empty($reviewSummary) || !empty($payoutSummary) || !empty($adSummary))
@php // The row shares its width among the cards that actually render: roles that
// don't see Payouts (managers) get two half-width cards, not a hole.
$bizCards = (int) !empty($reviewSummary) + (int) !empty($payoutSummary) + (int) !empty($adSummary);
$bizCol = [1 => 'col-lg-12', 2 => 'col-lg-6', 3 => 'col-lg-4'][$bizCards];
@endphp
@if (!empty($reviewSummary))
@if ((int) $reviewSummary['count'] === 0)
{{ t_raw('vendor.dashboard.reviews_empty') }}
@else
{{ t_raw('vendor.customers.col_reviews') }}
@foreach ($reviewSummary['latest'] as $rvw)
{{ trim((string) $rvw['author_name']) !== '' ? $rvw['author_name'] : t_raw('vendor.reservations.guest_fallback') }}
{!! rating_stars_html((float) $rvw['rating']) !!}
{{ (string) $rvw['comment'] }}
{{ time_ago($rvw['created_at']) }}
@endforeach
@php /* The rating summary anchors the FOOT, sponsored-style: the average
with its stars, the review count, and View-all in the corner. */
@endphp
{{ t_raw('admin.restaurants.col_rating') }}
{{ number_format((float) $reviewSummary['avg'], 1) }}
{!! rating_stars_html((float) $reviewSummary['avg']) !!}
{{ t_raw('vendor.customers.col_reviews') }}
{{ number_format((int) $reviewSummary['count']) }}
{{ t_raw('common.view_all') }}
@endif
@endif
@if (!empty($payoutSummary))
{{ t_raw($payoutSummary['invoice'] ? 'vendor.royalty.title' : 'vendor.payouts.title') }}
@if (!empty($payoutSummary['latest']))
@php /* The last statements, the way the Reviews card quotes its latest
reviews: the period, what it settled at, where it stands — each
row an excerpt of the payouts page's own table. */
@endphp
@php $stPill = static fn (string $st): string => match ($st) {
'paid' => 'qm-pill-green', 'issued' => 'qm-pill-blue', 'disputed' => 'qm-pill-red', default => 'qm-pill-amber',
};
@endphp
@foreach ($payoutSummary['latest'] as $stmt)
{!! fmt_day($stmt['period_start']) !!} – {!! fmt_day($stmt['period_end']) !!}
{{ money((float) $stmt['net_payable']) }}
{{ status_label('payout', (string) $stmt['status']) }} @if (!empty($stmt['paid_at'])) {!! fmt_day($stmt['paid_at']) !!}@endif
@endforeach
@else
{{ t_raw('vendor.payouts.empty') }}
@endif
@php /* The same foot its siblings wear: the two figures the payouts page
leads with, and View-all docked in the card's own corner. */
@endphp
{{ t_raw($payoutSummary['invoice'] ? 'vendor.royalty.kpi_pending' : 'vendor.payouts.kpi_pending') }}
{{ money((float) $payoutSummary['pendingNet']) }}
{{ t_raw('vendor.payouts.kpi_this_month') }}
{{ money((float) $payoutSummary['paidThisMonth']) }}
{{ t_raw('common.view_all') }}
@endif
@if (!empty($adSummary))
@if ((int) $adSummary['live'] === 0)
@else
@php /* The SAME chart-card head as every chart on the page: title +
timeline pills on the first row, the live count as the Apex
caption row — View all moves to the card's corner CTA below. */
@endphp
@partial('chart-card', [
'heading' => t_raw('admin.campaigns.title'),
'sub' => (int) $adSummary['live'] . ' ' . t_raw('common.active'),
'cfg' => $adSummary['chart'],
'meta' => ['has' => true, 'label' => t_raw('vendor.campaigns.kpi_impressions')],
'icon' => 'fa-bullhorn',
'accent' => 'rose',
'opts' => ['id' => 'adReachChart', 'ranges' => $adSummary['ranges'], 'selected' => '14', 'options' => $trendOptions ?? []],
])
{{ t_raw('vendor.campaigns.kpi_impressions') }}
{{ number_format((int) $adSummary['impressions']) }}
{{ t_raw('vendor.campaigns.kpi_clicks') }}
{{ number_format((int) $adSummary['clicks']) }}
{{ t_raw('vendor.campaigns.kpi_ctr') }}
{!! $adSummary['ctr'] !== null ? number_format((float) $adSummary['ctr'], 2) . '%' : t('vendor.campaigns.active_no_data') !!}
@php /* View all docked in the card's own corner: red, square except
the bottom-right, riding the card's radius. */
@endphp
{{ t_raw('common.view_all') }}
@endif
@endif
@endif
@if (!empty($acceptsReservations))
@php /* Today's book beside its shape: the table (8 cols) with the covers-by-hour
bars (4 cols) — the same hour-bar form as Busiest hours, pointed at the
NEXT fortnight of live bookings, because a reservation book is a plan. */
@endphp
{{ t_raw('vendor.dashboard.todays_reservations_title') }}
@if (count($todayReservations) > 0)
{{ t_raw('vendor.dashboard.n_booked_today', [':n' => (int) ($reservationsPager['total'] ?? 0)]) }}
@endif
{{ t_raw('common.view_all') }}
{{ t_raw('common.time') }} {{ t_raw('vendor.reservations.col_guest') }} {{ t_raw('vendor.reservations.party_label') }} {{ t_raw('common.status') }}
@if (count($todayReservations) > 0)
@foreach ($todayReservations as $rv)
{!! fmt_clock($rv['reserved_at']) !!}
@php /* A walk-in seated without a name has none stored — the display
placeholder belongs here, not in the row. Same fallback the
reservations list uses. */
@endphp
@php $rvName = trim((string) ($rv['guest_name'] ?? '')) !== '' ? (string) $rv['guest_name'] : t_raw('vendor.reservations.guest_fallback');
@endphp
{!! brandmark_html($rv['avatar'] ?? null, $rvName, 'md', 'qm-brandmark--round') !!}
{{ $rvName }}
{{ (int) $rv['party_size'] }} {{ t_raw('vendor.dashboard.guests_suffix') }}
{{ \App\Models\Reservation::statusLabel($rv['status']) }}
@endforeach
@else
@partial('table-empty', ['colspan' => 4, 'icon' => 'fa-calendar-day', 'msg' => t_raw('vendor.dashboard.no_reservations_today')])
@endif
@partial('table-toolbar', ['pager' => $reservationsPager ?? []])
@partial('chart-card', [
'heading' => t_raw('vendor.dashboard.chart_resv_load_title'),
'sub' => t_raw('vendor.dashboard.chart_resv_load_sub'),
'cfg' => $resvLoadChart ?? [],
'meta' => $resvLoadMeta ?? [],
'icon' => 'fa-calendar-check',
'accent' => 'blue',
'opts' => ['id' => 'resvLoadChart', 'ranges' => $resvLoadRanges ?? [], 'selected' => '14', 'options' => $trendOptions],
])
@endif
@php /* Client locations beside Recent orders: the Customers analytics page's map
(same rows, same avatar pins - see analytics-customers.php, whose builder
this copies) wearing the delivery map's card anatomy. WHO the restaurant
feeds on the left; WHAT is happening on the right. */
@endphp
@php
$cliPoints = [];
foreach (($clientLocations ?? []) as $cl) {
if ($cl['delivery_lat'] === null || $cl['delivery_lng'] === null) {
continue;
}
$addr = trim((string) ($cl['delivery_address'] ?? ''));
$cliPoints[] = [
'lat' => (float) $cl['delivery_lat'],
'lng' => (float) $cl['delivery_lng'],
'label' => $cl['name'] . ($addr !== '' ? ' — ' . $addr : ''),
'iconHtml' => brandmark_html($cl['avatar'] ?? null, (string) $cl['name'], 'sm'),
];
}
$cliCenter = $cliPoints ? $cliPoints[0] : null;
@endphp
{{ t_raw('vendor.analytics.heading_client_locations') }}
{{ number_format(count($cliPoints)) }} {{ t_raw('common.customers') }}
@if ($cliCenter !== null)
@php /* Same floating foot as the delivery map - the tally and the View-all
corner riding the map's lower edge. */
@endphp
@else
{{ t_raw('vendor.analytics.empty_no_client_locations') }}
@endif
{{ t_raw('admin.dashboard.col_order') }} {{ t_raw('common.customer') }} {{ t_raw('common.total') }} {{ t_raw('admin.analytics_sales.col_mode') }} {{ t_raw('common.status') }} {{ t_raw('admin.dashboard.col_placed') }}
@if (count($recent))
@foreach ($recent as $o)
@php /* Same link the full orders list and the admin dashboard already use —
this card was the only place an order number was a dead end. */
@endphp
{{ $o['order_number'] }}
@if (!empty($o['first_item_name']))
{{ $o['first_item_name'] }}@if (($o['item_count'] ?? 0) > 1) {{ t_raw('admin.dashboard.items_more_suffix', [':n' => (int) $o['item_count'] - 1]) }}@endif @endif
{!! brandmark_html($o['customer_avatar'] ?? null, (string) $o['customer_name'], 'md', 'qm-brandmark--round') !!}
{{ $o['customer_name'] }}
{{ money($o['total']) }}
{{ status_label('service_mode', $o['service_mode']) }}
{{ Order::statusLabel($o['status'], $o['service_mode'] ?? 'delivery') }}
{!! fmt_datetime($o['placed_at']) !!}
@endforeach
@else
@partial('table-empty', ['colspan' => 6, 'icon' => 'fa-receipt', 'msg' => t_raw('admin.dashboard.empty_no_orders')])
@endif
@partial('table-toolbar', ['pager' => $recentPager ?? []])
@endif
@php /* /.qm-d2 */
@endphp
@endsection