@extends('layouts.dashboard') @section('content') @php /** * Super-admin payouts — the settlement ledger. * * Three readings of the same money: what is still owed to restaurants, what has been gathered * onto a statement but not transferred, and what has actually been paid out this month. Every * figure comes from App\Models\Payout, which reads the commission stamped on each order rather * than the restaurant's current plan rate. * * Mirrors admin/subscriptions.php and admin/analytics-earnings.php — same KPI band, .qm-table * and modal idiom, shared component classes, no new CSS. * * @var array $runs payout statements, newest first * @var array $pager pagination metadata for $runs * @var array $owed restaurants with an unsettled balance, largest first * @var array $owedPager pagination metadata for $owed (its own ?owed_page cursor) * @var array $unpayable restaurants whose balance no run can send, with the reason code * @var array $outstanding platform-wide unsettled totals * @var array $inFlight statements prepared but not transferred * @var array $settled statements transferred since $monthStart * @var string $monthStart first day of the current month * @var string $status active status filter ('' = all) * @var array $trendChart settled value over the opening period * @var array $trendOpts period switcher: every window's series, swapped without a reload * @var array $trendMeta chart data flag + accessible summary */ $statusPill = static function (string $s): string { return match ($s) { 'paid' => 'qm-pill-green', 'issued' => 'qm-pill-blue', 'disputed' => 'qm-pill-red', default => 'qm-pill-amber', }; }; $filters = ['' => t_raw('common.all'), 'draft' => status_label('payout', 'draft'), 'issued' => status_label('payout', 'issued'), 'paid' => status_label('payout', 'paid'), 'disputed' => status_label('payout', 'disputed')]; // The money model decides what this page is. Owned installs settle with nobody, so the page // says so and points at consolidated revenue instead of pretending there are payouts to run. // Split payments overlays every model with the payout flow (the platform collects and disburses // each restaurant's share), so while it is on the page never shows the invoice or owned variants. $businessModel = $businessModel ?? 'marketplace'; $splitPayments = $splitPayments ?? false; $producesInvoices = ($producesInvoices ?? false) && !$splitPayments; $isOwned = $businessModel === 'owned' && !$splitPayments; $unpayable = $unpayable ?? []; // Two reasons a balance cannot be sent, and they have different owners: the restaurant fills in // its own payee, only the operator can turn a payout rail back on. The pill says whose move it is // — amber is waiting on the restaurant, red is waiting on us — reusing the status pill palette. /* rail_unfunded is nobody's mistake — the payee is right and the money came in elsewhere — so it takes the neutral grey rather than a colour that reads as a fault to be chased. */ $reasonPill = static fn (string $r): string => $r === 'gateway_disabled' ? 'qm-pill-red' : ($r === 'rail_unfunded' ? 'qm-pill-grey' : 'qm-pill-amber'); // The statements ledger below carries the active status filter onto its export link. $qs = http_build_query(array_filter(['status' => $status], static fn ($v) => (string) $v !== '')); @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp
{{ t_raw('admin.payouts.owned_title') }}
{{ t_raw('admin.payouts.owned_sub') }}
{{ t_raw('nav.sales') }}{{ t_raw($producesInvoices ? 'admin.payouts.intro_invoice' : 'admin.payouts.intro') }}
@if ($splitPayments){{ t_raw('admin.payouts.split_note') }}
@endif{{ t_raw('admin.payouts.owed_sub') }}
| {{ t_raw('common.restaurant') }} | {{ t_raw('common.orders') }} | {{ t_raw('admin.payouts.col_gross') }} | {{ t_raw('common.commission') }} | @php /* The figure under this head is Payout::netExpr() — takings minus commission in marketplace (owed TO the restaurant), the royalty itself in franchise (owed BY it, to the platform). "Net owed" leaves the direction to the reader and the reader assumes the payout one, so the franchise head names the money instead. */ @endphp{{ t_raw($producesInvoices ? 'admin.payouts.col_net_owed_invoice' : 'admin.payouts.col_net_owed') }} | {{ t_raw('admin.payouts.col_oldest_order') }} | {{ t_raw('common.actions') }} |
|---|---|---|---|---|---|---|
|
{!! restaurant_brandmark($row, 'md') !!}
{{ (string) $row['restaurant_name'] }}
@if (!empty($row['plan_name'])){!! plan_tag((string) ($row['plan_slug'] ?? ''), (string) $row['plan_name']) !!}@else{{ t_raw('admin.subscriptions.no_plan') }}@endif
@if (!empty($row['open_payout_id']))
{{ t_raw('admin.payouts.draft_open_link') }}
@endif
|
{{ number_format((int) $row['orders_count']) }} | {{ money($row['gross_sales']) }} | {{ money($row['commission_total']) }} | {{ money($row['net_owed']) }} | {!! fmt_day($row['oldest_order_at']) !!} | @php // Under split payments the primary action sends the restaurant its share now // (prepare + disburse); Prepare stays available to gather without sending. $rowActions = []; if ($splitPayments) { $rowActions[] = ['label' => t_raw('admin.payouts.pay_now'), 'icon' => 'fa-paper-plane', 'primary' => true, 'form' => [ 'action' => '/admin/payouts/dispatch', 'fields' => ['restaurant_id' => (int) $row['restaurant_id']], 'confirm' => t_raw('admin.payouts.confirm_pay_now'), ]]; } $rowActions[] = ['label' => t_raw('admin.payouts.prepare_btn'), 'icon' => 'fa-layer-group', 'primary' => !$splitPayments, 'form' => [ 'action' => '/admin/payouts/prepare', 'fields' => ['restaurant_id' => (int) $row['restaurant_id']], ]]; @endphp@partial('row-actions', ['inline' => 1, 'actions' => $rowActions]) |
{{ t_raw('admin.payouts.unpayable_sub') }}
| {{ t_raw('common.restaurant') }} | {{ t_raw('admin.payouts.col_reason') }} | {{ t_raw('common.amount') }} | {{ t_raw('common.actions') }} |
|---|---|---|---|
|
{{ (string) $row['restaurant_name'] }}
@php // What is actually on file, so the operator can see the difference between "nothing set" and "set to a rail we cannot send on".
@endphp
{!! (string) $row['payout_account'] !== '' ? e((string) $row['payout_account']) : t('admin.restaurantdetail.payee_missing') !!}
|
{{ t_raw('admin.payouts.unpayable_' . $reason) }}
{{ t_raw('admin.payouts.unpayable_' . $reason . '_hint') }}
|
{{ money($row['amount']) }} | @php // The payee lives on the restaurant's own record, which is where this is chased from. @endphp @partial('row-actions', ['inline' => 1, 'actions' => [ ['label' => t_raw('common.view'), 'icon' => 'fa-store', 'href' => '/admin/restaurants/' . (int) $row['restaurant_id'], 'primary' => true], ]]) |
{{ t_raw($producesInvoices ? 'admin.payouts.runs_sub_invoice' : 'admin.payouts.runs_sub') }}
| {{ t_raw('admin.payouts.col_run') }} | {{ t_raw('common.restaurant') }} | {{ t_raw('common.period') }} | {{ t_raw('common.orders') }} | {{ t_raw('admin.payouts.col_gross') }} | {{ t_raw('common.commission') }} | {{ t_raw('admin.payouts.col_net_payable') }} | {{ t_raw('common.status') }} | {{ t_raw('common.actions') }} |
|---|---|---|---|---|---|---|---|---|
| #{{ (int) $run['id'] }} |
{!! restaurant_brandmark($run, 'md') !!}
{{ (string) $run['restaurant_name'] }}
{!! or_dash($run['payment_ref'] ?? null) !!}
|
{!! fmt_day($run['period_start']) !!} – {!! fmt_day($run['period_end']) !!} | {{ number_format((int) $run['orders_count']) }} | {{ money($run['gross_sales']) }} | {{ money($run['commission_total']) }} | {{ money($run['net_payable']) }} | {{ status_label('payout', $st) }} @if ($st === 'paid' && !empty($run['paid_at'])){!! fmt_day($run['paid_at']) !!}@endif | @partial('row-actions', ['actions' => [ ['label' => t_raw('admin.payouts.view_run'), 'icon' => 'fa-eye', 'href' => '/admin/payouts/' . (int) $run['id'], 'primary' => true], $st === 'draft' ? ['label' => t_raw('admin.payouts.issue_btn'), 'icon' => 'fa-paper-plane', 'form' => [ 'action' => '/admin/payouts/settle', 'fields' => ['payout_id' => (int) $run['id'], 'status' => 'issued'], 'confirm' => t_raw('admin.payouts.confirm_issue'), ]] : null, ]]) |