@extends('layouts.dashboard')
@section('content')
@php
use App\Models\Order;
/** @var array $kpis @var array $recentRestaurants @var array $recentOrders */
/** @var array $revenueChart @var array $ordersChart @var array $statusChart @var array $topRestChart */
/** @var array $revenueRanges @var array $ordersRanges @var array $statusRanges @var array $topRestRanges */
/** @var array $earningsSnap @var array $salesSnap @var array $restaurantsSnap @var array $customersSnap */
/**
* A chart has data when at least one plotted value is non-zero. Donut/pie use
* `values`; cartesian charts use series[].data. Drives the no-data empty state
* so a fresh zero-data install shows a friendly placeholder, not a blank frame.
*/
$chartHasData = static function (array $cfg): bool {
$nums = [];
if (isset($cfg['values']) && is_array($cfg['values'])) {
$nums = $cfg['values'];
} elseif (isset($cfg['series']) && is_array($cfg['series'])) {
foreach ($cfg['series'] as $s) {
if (isset($s['data']) && is_array($s['data'])) {
$nums = array_merge($nums, $s['data']);
}
}
}
foreach ($nums as $n) {
if ((float) $n != 0.0) {
return true;
}
}
return false;
};
$kpiSparks = $kpiSparks ?? [];
$kpiTrends = $kpiTrends ?? ['orders' => ['pct' => 0, 'dir' => 'flat'], 'revenue' => ['pct' => 0, 'dir' => 'flat']];
$rightNow = $rightNow ?? null;
$today = $today ?? '';
$churn = $churn ?? ['quiet' => [], 'lapsed' => [], 'quietCount' => 0, 'lapsedCount' => 0];
$pendingReviewCount = (int) ($pendingReviewCount ?? 0);
$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
@php /* .qm-d2 scopes the dashboard density layer (css/admin-dash.css): same
partials, same keys — restyled and choreographed. */
@endphp
@if ($rightNow)
@php /* The strip is a TRIAGE bar, so state does the talking: a tile whose queue
has work is `is-hot` (accent chip, live dot), an empty queue `is-calm`
(receded). Status-type tiles (venue count) carry 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('admin.dashboard.platform_snapshot') }}
@php // Platform take (subscriptions + ads + commission) — hidden in owned mode, where
// there is no such income and the operator's money is the order revenue (Sales).
@endphp
@if (!empty($earningsSnap))
@partial('kpi-card', [
'variant' => 'panel', 'accent' => 'green', 'icon' => 'fa-coins', 'label' => t_raw('nav.earnings'),
'value' => money($earningsSnap['total']), 'meta' => '' . t('admin.dashboard.last_30_days') . '',
'href' => route('admin.analytics.earnings'), 'linkLabel' => t_raw('common.full_report'),
'spark' => $kpiSparks['earnings'] ?? [],
'rows' => [
['label' => t_raw('common.subscriptions'), 'value' => money($earningsSnap['subs'])],
['label' => t_raw('common.sponsored_ads'), 'value' => money($earningsSnap['ads'])],
['label' => $earningsSnap['take_label'] ?? t_raw('common.commission'), 'value' => money($earningsSnap['commission'])],
],
'pills' => [
['label' => t_raw('admin.dashboard.pill_active_subscriptions', [':n' => $earningsSnap['activeSubs']]), 'tone' => 'blue'],
$earningsSnap['renewalsDue'] > 0
? ['label' => t_raw('admin.dashboard.pill_renewals_due', [':n' => $earningsSnap['renewalsDue']]), 'tone' => 'amber']
: ['label' => t_raw('admin.dashboard.pill_no_renewals_due'), 'tone' => 'grey'],
],
])
@endif
@partial('kpi-card', [
'variant' => 'panel', 'accent' => 'blue', 'icon' => 'fa-receipt', 'label' => t_raw('nav.sales'),
'value' => money($salesSnap['revenue']),
'meta' => kpi_trend((int) ($kpiTrends['revenue']['pct'] ?? 0), (string) ($kpiTrends['revenue']['dir'] ?? 'flat')) . '' . t('admin.dashboard.last_30_days') . '',
'href' => route('admin.analytics.sales'), 'linkLabel' => t_raw('common.full_report'),
'spark' => $kpiSparks['revenue'] ?? [],
'rows' => [
['label' => t_raw('common.paid_orders'), 'value' => number_format($salesSnap['paidOrders'])],
['label' => t_raw('common.average_order'), 'value' => money($salesSnap['aov'])],
['label' => t_raw('common.refunded'), 'value' => money($salesSnap['refunds'])],
],
'pills' => [
['label' => t_raw('admin.dashboard.pill_orders_all_time', [':n' => (int) $kpis['orders']]), 'tone' => 'grey'],
$salesSnap['refunds'] > 0
? ['label' => t_raw('admin.dashboard.pill_amount_refunded', [':amount' => money($salesSnap['refunds'])]), 'tone' => 'red']
: ['label' => t_raw('admin.dashboard.pill_no_refunds'), 'tone' => 'green'],
],
])
@partial('kpi-card', [
'variant' => 'panel', 'accent' => 'teal', 'icon' => 'fa-store', 'label' => t_raw('nav.restaurants'),
'value' => number_format($restaurantsSnap['total']), 'meta' => '' . t('admin.dashboard.on_the_platform') . '',
'href' => route('admin.analytics.restaurants'), 'linkLabel' => t_raw('common.full_report'),
'spark' => $kpiSparks['restaurants'] ?? [],
'rows' => [
['label' => t_raw('common.status_approved'), 'value' => number_format($restaurantsSnap['approved'])],
['label' => t_raw('admin.dashboard.row_new_this_month'), 'value' => number_format($restaurantsSnap['newCount'])],
['label' => t_raw('common.status_suspended'), 'value' => number_format($restaurantsSnap['suspended'])],
],
'pills' => [
$restaurantsSnap['pending'] > 0
? ['label' => t_raw('admin.dashboard.pill_pending_approval', [':n' => $restaurantsSnap['pending']]), 'tone' => 'amber']
: ['label' => t_raw('admin.dashboard.pill_no_pending_approvals'), 'tone' => 'green'],
$restaurantsSnap['suspended'] > 0
? ['label' => t_raw('admin.dashboard.pill_suspended_count', [':n' => $restaurantsSnap['suspended']]), 'tone' => 'red']
: ['label' => t_raw('admin.dashboard.pill_none_suspended'), 'tone' => 'grey'],
],
])
@partial('kpi-card', [
'variant' => 'panel', 'accent' => 'pink', 'icon' => 'fa-users', 'label' => t_raw('nav.customers'),
'value' => number_format($customersSnap['customers']), 'meta' => '' . t('admin.dashboard.last_30_days') . '',
'href' => route('admin.analytics.customers'), 'linkLabel' => t_raw('common.full_report'),
'spark' => $kpiSparks['users'] ?? [],
'rows' => [
['label' => t_raw('admin.dashboard.row_new'), 'value' => number_format($customersSnap['newCustomers'])],
['label' => t_raw('common.returning'), 'value' => number_format($customersSnap['returning'])],
['label' => t_raw('common.guest_orders'), 'value' => number_format($customersSnap['guestOrders'])],
],
'pills' => [
['label' => t_raw('admin.dashboard.pill_accounts_total', [':n' => (int) $kpis['users']]), 'tone' => 'grey'],
['label' => t_raw('admin.dashboard.pill_guest_checkout_pct', [':pct' => $customersSnap['guestPct']]), 'tone' => 'blue'],
],
])
@php
/* The highlight only earns its slot when somebody has actually sold something: a
"top" restaurant on a platform with no paid orders is just whichever row sorted
first at zero. When there is nothing to show the two trend charts reflow to the
full twelve columns rather than leaving a two-column gap beside them. */
$showTopRest = !empty($topRestaurant) && (float) ($topRestaurant['rev'] ?? 0) > 0;
$trendMainCol = $showTopRest ? 5 : 8;
@endphp
{{ t_raw('dashboard.sec.trends') }}
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_revenue_trend_title'),
'sub' => t_raw('admin.dashboard.chart_revenue_caption'),
'cfg' => $revenueChart,
'meta' => ['has' => $chartHasData($revenueChart), 'label' => t_raw('admin.dashboard.chart_revenue_trend_title')],
'opts' => ['id' => 'admRevChart', 'ranges' => $revenueRanges, 'selected' => '90', 'options' => $trendOptions],
'icon' => 'fa-chart-line',
'accent' => 'green',
])
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_orders_per_day_title'),
'sub' => t_raw('admin.dashboard.chart_orders_caption'),
'cfg' => $ordersChart,
'meta' => ['has' => $chartHasData($ordersChart), 'label' => t_raw('admin.dashboard.chart_orders_per_day_aria')],
'opts' => ['id' => 'admOrdersChart', 'ranges' => $ordersRanges, 'selected' => '90', 'options' => $trendOptions],
'icon' => 'fa-chart-bar',
'accent' => 'rose',
])
@php /* The platform's strongest venue, shown with the same card the Restaurants
list uses. Three columns rather than two: at two the cover, the name and
the three-up stat rail were all fighting for the same narrow measure, so
the card reads at its natural proportions here and the revenue trend gives
up the column (5 + 4 + 3), leaving orders-per-day untouched. */
@endphp
@if ($showTopRest)
@php $tr = $topRestaurant; @endphp
 ($tr['cover_image'] ?? ''), 'assets/client/covers/shop-1.png') }})
@php $trFlag = function_exists('cuisine_flag') ? cuisine_flag((string) ($tr['cuisine_slug'] ?? '')) : null;
@endphp
@if ($trFlag)
@endif
{!! restaurant_logo_html($tr, 'lg', 'qm-brandmark--round') !!}
{{ (string) $tr['name'] }}
{{ trim((string) ($tr['cuisine_name'] ?? '') . ' · ' . (string) ($tr['city'] ?? ''), ' ·') }}
{!! (int) ($tr['rating_count'] ?? 0) ? number_format((float) $tr['rating_cache'], 1) : dash() !!}
{{ t_raw('admin.restaurants.col_rating') }}
{{ (string) ($tr['plan_name'] ?? '—') }}
{{ t_raw('common.plan') }}
{{ money((float) ($tr['rev'] ?? 0)) }}
{{ t_raw('admin.dashboard.chart_revenue_trend_title') }}
{!! brandmark_html($tr['owner_avatar'] ?? null, (string) ($tr['owner_name'] ?? ''), 'sm', 'qm-brandmark--round') !!}
{{ (string) ($tr['owner_name'] ?? '') }}
{{ (string) ($tr['owner_email'] ?? '') }}
@endif
{{ t_raw('dashboard.sec.breakdown') }}
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_top_restaurants_title'),
'sub' => t_raw('admin.dashboard.chart_top_rest_caption'),
'cfg' => $topRestChart,
'meta' => ['has' => $chartHasData($topRestChart), 'label' => t_raw('admin.dashboard.chart_top_restaurants_aria')],
'opts' => ['id' => 'admTopRestChart', 'ranges' => $topRestRanges, 'selected' => '90', 'options' => $trendOptions],
'icon' => 'fa-store',
'accent' => 'teal',
])
@partial('chart-card', [
'heading' => t_raw('admin.dashboard.chart_orders_status_title'),
'sub' => t_raw('admin.dashboard.chart_status_caption'),
'cfg' => $statusChart,
'meta' => ['has' => $chartHasData($statusChart), 'label' => t_raw('admin.dashboard.chart_orders_status_aria')],
'opts' => ['id' => 'admStatusChart', 'ranges' => $statusRanges, 'selected' => '90', 'options' => $trendOptions],
'icon' => 'fa-chart-pie',
'accent' => 'violet',
])
@php /* Business position — recurring mix, payout liability, review stream.
Same excerpt-card pattern as the vendor dashboard: each card carries
its own screen's figures and a View-all into it. */
@endphp
@php /* The business row always renders: a module with nothing yet shows its
empty state (same convention as the vendor dashboard's modules). */
@endphp
@if (true)
@if (\App\Services\Capability::has(\App\Services\Capability::SUBSCRIPTIONS))
@php $mixTotal = array_sum(array_map(static fn ($p) => (int) $p['c'], $planMix));
@endphp
@if ($mixTotal === 0 && empty($planMixChart['categories']))
{{ t_raw('admin.subscriptions.empty') }}
@else
{{ t_raw('nav.subscriptions') }}
@php /* Venues per plan as a horizontal bar — restaurants.plan_id is the
one link the FREE tier has, so the whole marketplace charts here.
The foot spells out each bar's exact count (read off the same
payload, so they can never disagree) and closes with the billable
tally the subtitle headlines. */
@endphp
@if (!empty($planMixChart['categories']))
@endif
@foreach ($planMixChart['categories'] as $pmI => $pmName)
{{ (string) $pmName }}
{{ number_format((int) ($planMixChart['series'][0]['data'][$pmI] ?? 0)) }}
@endforeach
{{ t_raw('common.active') }}
{{ number_format($mixTotal) }}
{{ t_raw('common.view_all') }}
@endif
@endif
@if (!empty($owedSummary))
{{ t_raw('nav.payouts') }}
@if (!empty($owedSummary['latest']))
@php /* The latest statements platform-wide, exactly the way the vendor
Payouts card quotes its own: who, the period's worth, where it
stands - each row an excerpt of the payouts page's 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 ($owedSummary['latest'] as $stmt)
{{ (string) $stmt['restaurant_name'] }}
{{ money((float) $stmt['net_payable']) }}
{{ status_label('payout', (string) $stmt['status']) }} {!! fmt_day($stmt['period_start']) !!} – {!! fmt_day($stmt['period_end']) !!}
@endforeach
@endif
@php /* The same foot the vendor card and the siblings here wear: the page's
two lead figures, View-all docked in the card's own corner. */
@endphp
{{ t_raw('admin.payouts.kpi_owed') }}
{{ money((float) $owedSummary['totals']['net_owed']) }}
{{ t_raw('admin.payouts.kpi_in_flight') }}
{{ money((float) $owedSummary['inFlight']['net_payable']) }}
{{ t_raw('common.view_all') }}
@endif
@if (empty($latestReviews))
{{ t_raw('admin.reviews.empty_none') }}
@else
{{ t_raw('admin.reviews.title') }}
@foreach ($latestReviews as $rvw)
{{ (string) $rvw['restaurant_name'] }}
{!! rating_stars_html((float) $rvw['rating']) !!}
{{ (string) $rvw['comment'] }}
{{ (string) $rvw['author_name'] }} · {{ time_ago($rvw['created_at']) }}
@endforeach
@if (!empty($reviewAgg['count']))
{{ t_raw('admin.restaurants.col_rating') }}
{{ number_format((float) $reviewAgg['avg'], 1) }}
{!! rating_stars_html((float) $reviewAgg['avg']) !!}
{{ t_raw('vendor.customers.col_reviews') }}
{{ number_format((int) $reviewAgg['count']) }}
@endif
{{ t_raw('status.review.pending') }}
{{ number_format((int) ($rightNow['reviews'] ?? 0)) }}
{{ t_raw('common.view_all') }}
@endif
@endif
@php /* Partners beside the partner map — the same capped, status-ringed
brandmark markers the restaurant-analytics map draws. */
@endphp
| {{ t_raw('nav.restaurants') }} | {{ t_raw('admin.dashboard.col_owner') }} | {{ t_raw('admin.restaurants.col_rating') }} | {{ t_raw('admin.restaurants.col_featured') }} | {{ t_raw('common.status') }} | {{ t_raw('common.joined') }} |
@if ($recentRestaurants)
@foreach ($recentRestaurants as $r)
@php /* One identity cell, as /admin/restaurants draws it: the cuisine rides
the brandmark as a corner flag and the owner's address sits under their
name, so neither needs a column of its own. */
@endphp
{!! restaurant_brandmark($r, 'md') !!}
{{ $r['name'] }}
@if (!empty($r['cuisine_name']) || !empty($r['city'])) {{ trim(($r['cuisine_name'] ?? '') . (!empty($r['cuisine_name']) && !empty($r['city']) ? ' · ' : '') . ($r['city'] ?? '')) }}@endif
|
@if (!empty($r['owner_name']) || !empty($r['owner_email']))
{!! brandmark_html($r['owner_avatar'] ?? null, (string) ($r['owner_name'] ?? $r['owner_email']), 'sm', 'qm-brandmark--round') !!}
@if (!empty($r['owner_name'])){{ $r['owner_name'] }}@endif
{{ $r['owner_email'] ?? '' }}
@else{!! dash() !!}@endif
|
@php /* Rating + Featured mirror /admin/restaurants — the same fields already
loaded on each row, so no extra query. */
@endphp
@if ((int) ($r['rating_count'] ?? 0) > 0)
{{ number_format((float) $r['rating_cache'], 1) }}
{{ number_format((int) $r['rating_count']) }} {{ t_raw('common.ratings_count_suffix') }}
@else{!! dash() !!}@endif
|
{!! !empty($r['is_featured']) ? t('admin.restaurants.pill_featured') : t('admin.restaurants.pill_not_featured') !!} |
{{ status_label('restaurant', $r['status']) }} |
{!! fmt_day($r['created_at']) !!} |
@endforeach
@else
@partial('table-empty', ['colspan' => 6, 'icon' => 'fa-store', 'msg' => t('admin.restaurants.empty_none')])
@endif
@php
// Identical marker recipe to analytics-restaurants: brandmark ringed in its
// status colour, popup linking into the restaurants list.
$statusColor = static fn (string $s): string => $s === 'approved' ? 'var(--ok)' : ($s === 'pending' ? 'var(--warn)' : 'var(--danger)');
$mapPoints = [];
$mapCities = [];
foreach (($mapLocations ?? []) as $loc) {
if ($loc['lat'] === null || $loc['lng'] === null) { continue; }
if (!empty($loc['city'])) { $mapCities[mb_strtolower(trim((string) $loc['city']))] = true; }
$mapPoints[] = [
'lat' => (float) $loc['lat'],
'lng' => (float) $loc['lng'],
'label' => $loc['name'] . (!empty($loc['city']) ? ' — ' . $loc['city'] : ''),
'url' => url('/admin/restaurants?q=' . rawurlencode((string) $loc['name'])),
'iconHtml' => restaurant_logo_html($loc, 'sm', '', 'style="box-shadow:0 0 0 3px ' . e($statusColor((string) $loc['status'])) . '"'),
];
}
@endphp
@if ($mapPoints)
@php /* The module's numbers ride the map itself: a floating foot with the
supply tally and the View-all corner — the biz-row footer pattern. */
@endphp
@endif
| {{ t_raw('admin.dashboard.col_order') }} | {{ t_raw('nav.restaurants') }} | {{ t_raw('common.customer') }} | {{ t_raw('admin.orders.col_total') }} | {{ t_raw('common.status') }} | {{ t_raw('admin.dashboard.col_placed') }} |
@if (count($recentOrders))
@foreach ($recentOrders as $o)
{{ $o['order_number'] }}
@if (!empty($o['first_item_name']))
@php $more = (int) ($o['item_count'] ?? 0) - 1; @endphp {{ $o['first_item_name'] }}@if ($more > 0) {{ t_raw('admin.dashboard.items_more_suffix', [':n' => $more]) }}@endif@endif
|
{!! restaurant_logo_html(['logo' => $o['restaurant_logo'] ?? '', 'name' => $o['restaurant_name'] ?? ''], 'md') !!}
|
@php $custName = trim((string) ($o['customer_name'] ?? '')) ?: t_raw('common.customer');
@endphp
{!! brandmark_html($o['customer_avatar'] ?? null, $custName, 'md', 'qm-brandmark--round') !!}
{{ $custName }}
|
{{ money($o['total']) }} |
{{ Order::statusLabel($o['status'], $o['service_mode'] ?? 'delivery') }} |
{{ time_ago($o['placed_at']) }} |
@endforeach
@else
@partial('table-empty', ['colspan' => 6, 'icon' => 'fa-receipt', 'msg' => t('admin.dashboard.empty_no_orders')])
@endif
@php /* Churn watch. Two ways a partner leaves without saying so: it stops trading,
or its subscription term quietly runs out. Both are listed, newest money
first, so the biggest loss is the first name an operator reads. */
@endphp
| {{ t_raw('nav.restaurants') }} | {{ t_raw('admin.dashboard.churn_col_signal') }} | {{ t_raw('common.when') }} | {{ t_raw('common.revenue') }} | {{ t_raw('common.actions') }} |
@if ($churn['quiet'] || $churn['lapsed'])
@foreach ($churn['quiet'] as $c)
{!! restaurant_brandmark(['logo' => $c['logo'] ?? '', 'name' => $c['name'] ?? '', 'cuisine_slug' => $c['cuisine_slug'] ?? ''], 'md') !!}
{{ $c['name'] }}
@if (!empty($c['city'])) {{ $c['city'] }}@endif
|
{{ t_raw('admin.dashboard.churn_signal_quiet') }} |
{{ time_ago($c['last_order_at']) }} |
{{ money($c['prior_revenue']) }} |
@partial('row-actions', ['actions' => [
['label' => t_raw('common.view'), 'icon' => 'fa-eye', 'href' => '/admin/restaurants/' . (int) $c['id'], 'primary' => true],
['label' => t_raw('admin.dashboard.churn_see_orders'), 'icon' => 'fa-receipt', 'href' => '/admin/orders?restaurant_id=' . (int) $c['id']],
]])
|
@endforeach
@foreach ($churn['lapsed'] as $c)
{!! restaurant_brandmark(['logo' => $c['logo'] ?? '', 'name' => $c['name'] ?? '', 'cuisine_slug' => $c['cuisine_slug'] ?? ''], 'md') !!}
{{ $c['name'] }}
{{ (string) $c['plan_name'] }}
|
{{ t_raw('admin.dashboard.churn_signal_lapsed') }} |
{!! fmt_day($c['ends_at'], true) !!} |
{!! dash() !!} |
@partial('row-actions', ['actions' => [
['label' => t_raw('common.view'), 'icon' => 'fa-eye', 'href' => '/admin/restaurants/' . (int) $c['id'], 'primary' => true],
['label' => t_raw('common.subscriptions'), 'icon' => 'fa-file-invoice-dollar', 'href' => '/admin/subscriptions'],
]])
|
@endforeach
@else
@partial('table-empty', ['colspan' => 5, 'icon' => 'fa-smile', 'msg' => t('admin.dashboard.churn_empty')])
@endif
@php /* One table over two signals: its total is both counts added, and a page is cut
across the pair rather than capping each list separately. */
@endphp
@partial('table-toolbar', ['pager' => $churnPager ?? []])
@php /* /.qm-d2 */
@endphp
@endsection