@extends('layouts.dashboard') @section('content') @php /** * Super-admin payouts — one settlement statement and the orders it covers. * * Every line is snapshotted from the order it settles: the takings, the commission the order * was actually charged, and the balance owed to the restaurant. The header totals are re-derived * from these lines, so the summary and the rows can never disagree. * * @var array $payout statement joined to its restaurant and owner * @var array $lines one row per settled order * @var bool $producesInvoices this statement bills the restaurant instead of paying it */ $status = (string) $payout['status']; /* The direction this statement runs, from its OWN stamped kind (see PayoutController::show). A payout is money the platform sends TO the restaurant; a royalty invoice is money the restaurant owes the platform — so , "Record the transfer", "reached the restaurant", TRANSFERRED, RECIPIENT, "who is being paid", "due to the restaurant" and the TO RESTAURANT column all state the inverse of the truth when the statement is an invoice. Each gets an *_invoice sibling branched on this one flag, exactly as the KPI band on admin/payouts.php does. Phrases that hold either way — Takings, Commission, Adjustments, Net payable, Statement details, the order/date/rate columns — keep their single key. Defaulted from the row so the view is still correct if it is ever rendered without the flag. */ $producesInvoices = $producesInvoices ?? ((string) ($payout['kind'] ?? 'payout') === 'invoice'); $statusPill = match ($status) { 'paid' => 'qm-pill-green', 'issued' => 'qm-pill-blue', 'disputed' => 'qm-pill-red', default => 'qm-pill-amber', }; $isPaid = $status === 'paid'; $adjustments = (float) $payout['adjustments']; // A refund taken after settlement is clawed back as a negative adjustment on the next statement, // so a statement can be worth less than nothing: the restaurant owes the platform more than this // statement carries. The transfer is refused while that is true (PayoutController::settle()), and // the admin should read that here — with the amount outstanding — instead of discovering it as an // error only after filling the form in. $netPayable = (float) $payout['net_payable']; $outstanding = $netPayable < 0 ? abs($netPayable) : 0.0; $linesNet = 0.0; $linesGross = 0.0; $linesCommission = 0.0; foreach ($lines as $line) { $linesGross += (float) $line['order_total']; $linesCommission += (float) $line['commission_amount']; $linesNet += (float) $line['net']; } // The statement totals above are struck over every line, so the summary still reconciles // against the whole statement while the table shows a page of it. $linesPager = paginate(count($lines), 20); $linesPage = array_slice($lines, $linesPager['offset'], $linesPager['perPage']); @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp <div class="qm-d2"> @php // KPIs lead the page; the section banner introduces the detail below them. @endphp <div class="qm-kpi-grid"> @partial('kpi-card', ['variant' => 'panel', 'accent' => 'blue', 'icon' => 'fa-receipt', 'label' => t_raw('admin.payouts.col_gross'), 'value' => money($payout['gross_sales']), 'meta' => '<span class="qm-kpi-cap">' . t('admin.payouts.kpi_gross_cap', [':orders' => number_format((int) $payout['orders_count'])]) . '</span>']) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'violet', 'icon' => 'fa-percentage', 'label' => t_raw('common.commission'), 'value' => money($payout['commission_total']), 'meta' => '<span class="qm-kpi-cap">' . t('admin.payouts.kpi_commission_cap') . '</span>']) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'amber', 'icon' => 'fa-sliders-h', 'label' => t_raw('admin.payouts.kpi_adjustments'), 'value' => money($adjustments), 'meta' => '<span class="qm-kpi-cap">' . ($payout['adjustment_note'] !== null && $payout['adjustment_note'] !== '' ? e((string) $payout['adjustment_note']) : t('admin.payouts.kpi_adjustments_cap')) . '</span>']) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'green', 'icon' => 'fa-money-check-alt', 'label' => t_raw('admin.payouts.col_net_payable'), 'value' => money($payout['net_payable']), 'meta' => '<span class="qm-kpi-cap">' . ($isPaid ? t($producesInvoices ? 'admin.payouts.kpi_net_paid_cap_invoice' : 'admin.payouts.kpi_net_paid_cap', [':date' => fmt_day($payout['paid_at'], true)]) : t($producesInvoices ? 'admin.payouts.kpi_net_cap_invoice' : 'admin.payouts.kpi_net_cap')) . '</span>']) @partial('kpi-card', ['variant' => 'panel', 'accent' => 'teal', 'icon' => 'fa-receipt', 'label' => t_raw('admin.payouts.kpi_avg_order'), 'value' => money((int) $payout['orders_count'] > 0 ? (float) $payout['gross_sales'] / (int) $payout['orders_count'] : 0), 'meta' => '<span class="qm-kpi-cap">' . t('admin.payouts.kpi_avg_order_cap', [':orders' => number_format((int) $payout['orders_count'])]) . '</span>']) </div> <div class="qm-ops-head"> <div> <h2 class="qm-snap-section-title mb-1"><span class="qm-snap-title-bar" aria-hidden="true"></span>{{ t_raw('admin.payouts.detail_heading') }}</h2> <p class="qm-ops-sub mb-0">{!! t_raw('admin.payouts.detail_sub', [':restaurant' => (string) $payout['restaurant_name'], ':from' => fmt_day($payout['period_start'], true), ':to' => fmt_day($payout['period_end'], true)]) !!}</p> </div> <div class="d-flex align-items-center gap-2"> <span class="qm-pill {{ $statusPill }}">{{ status_label('payout', $status) }}</span> <a href="{{ route('admin.payouts') }}" class="qm-btn qm-btn-sm qm-btn-outline"><i class="fas fa-arrow-left me-1" aria-hidden="true"></i>{{ t_raw('admin.payouts.back_link') }}</a> </div> </div> <div class="qm-card"> <div class="qm-ops-head"> <div> <h2 class="mb-0">{{ t_raw('admin.payouts.statement_title') }}</h2> <p class="qm-ops-sub">{{ t_raw($producesInvoices ? 'admin.payouts.statement_sub_invoice' : 'admin.payouts.statement_sub') }}</p> </div> </div> <dl class="qm-billing-meta"> <div><dt>{{ t_raw('common.restaurant') }}</dt><dd>{{ (string) $payout['restaurant_name'] }}</dd></div> <div><dt>{{ t_raw($producesInvoices ? 'admin.payouts.field_recipient_invoice' : 'admin.payouts.field_recipient') }}</dt><dd>{!! or_dash($payout['owner_name'] ?? null) !!} @if (!empty($payout['owner_email']))<span class="qm-cell-sub">{{ (string) $payout['owner_email'] }}</span>@endif</dd></div> <div><dt>{{ t_raw('common.period') }}</dt><dd>{!! fmt_day($payout['period_start']) !!} – {!! fmt_day($payout['period_end']) !!}</dd></div> <div><dt>{{ t_raw('admin.payouts.field_prepared') }}</dt><dd>{!! fmt_datetime($payout['created_at']) !!} @if (!empty($payout['created_by_name']))<span class="qm-cell-sub">{{ (string) $payout['created_by_name'] }}</span>@endif</dd></div> <div><dt>{{ t_raw('admin.shared.payment_reference') }}</dt><dd>{!! or_dash($payout['payment_ref'] ?? null) !!}</dd></div> <div><dt>{{ t_raw($producesInvoices ? 'admin.payouts.field_transferred_invoice' : 'admin.payouts.field_transferred') }}</dt><dd>{!! !empty($payout['paid_at']) ? fmt_datetime($payout['paid_at']) : dash() !!}</dd></div> </dl> </div> @php /* SENT BY US, AND WAITING ON THE PROVIDER. A statement the product disbursed itself carries the gateway's own reference and sits at 'issued' until reconcile_payouts confirms the batch. Asking the operator to "mark as paid" in that window would have them hand-confirm a payment the product made — asserting an outcome only the provider knows yet. So the manual form is withheld while the machine's answer is pending; Dispute stays, because a batch that fails needs flagging, and settling out-of-band stays available on any statement the product did NOT send. */ @endphp @php $awaitingGateway = $status === 'issued' && !empty($payout['payment_ref']); @endphp @if (!$isPaid) <div class="qm-card"> <div class="qm-ops-head"> <div> <h2 class="mb-0">{{ t_raw($producesInvoices ? 'admin.payouts.settle_title_invoice' : 'admin.payouts.settle_title') }}</h2> <p class="qm-ops-sub mb-0">{{ t_raw($producesInvoices ? 'admin.payouts.settle_sub_invoice' : 'admin.payouts.settle_sub', [':amount' => money($payout['net_payable'])]) }}</p> </div> @if ($status !== 'disputed') <form method="post" action="{{ route('admin.payouts.settle') }}" data-confirm="{{ t_raw('admin.payouts.confirm_dispute') }}"> @csrf <input type="hidden" name="payout_id" value="{{ (int) $payout['id'] }}"> <input type="hidden" name="status" value="disputed"> <button type="submit" class="qm-btn qm-btn-sm qm-btn-outline"><i class="fas fa-flag me-1" aria-hidden="true"></i>{{ t_raw('admin.payouts.dispute_btn') }}</button> </form> @endif </div> @php /* Stated before the form, not after the attempt: the transfer is blocked, this is how much is owed back, and the two ways it clears. The form stays usable — the adjustment that clears the debt is submitted through it, and settle() re-reads the balance with that adjustment applied. */ @endphp @if ($outstanding > 0) <div class="alert alert-danger mb-3" role="status"> <span class="qm-alert-ico"><i class="fas fa-exclamation-triangle" aria-hidden="true"></i></span> <div class="qm-alert-body"> <strong>{{ t_raw('admin.payouts.negative_title', [':amount' => money($outstanding)]) }}</strong> <p class="mb-0">{{ t_raw('admin.payouts.negative_text') }}</p> </div> </div> @endif @php /* SEND IT, rather than only record that somebody else did. Every restaurant on this install stores a payee, and the platform is holding the money — so the operator can disburse from here instead of paying out-of-band and typing a reference back in. $sendable is the model's own dispatchable() answer, the SAME rule the scheduled sweep applies, so this appears only when pressing it would genuinely pay: an unpaid payout-kind statement, a positive balance, and a payee the rail can reach. The amount is the one on this page and is sent without re-gathering, so what is approved here is what leaves. Marking as paid stays below it, because settling outside the product is still valid — a bank transfer has no rail here and always will not. */ @endphp @if (!empty($sendable)) <form method="post" action="{{ route('admin.payouts.dispatch') }}" class="mb-3" data-confirm="{{ t_raw('admin.payouts.confirm_send', [':amount' => money($payout['net_payable']), ':payee' => e((string) ($sendable['payout_account'] ?? '')), ':rail' => e(ucfirst((string) $sendable['payout_method']))]) }}"> @csrf <input type="hidden" name="payout_id" value="{{ (int) $payout['id'] }}"> <button type="submit" class="qm-btn"> <i class="fas fa-paper-plane" aria-hidden="true"></i> {{ t_raw('admin.payouts.send_now_btn', [':amount' => money($payout['net_payable'])]) }} </button> </form> @endif @if ($awaitingGateway) @php /* The PROVIDER'S own answer, not a paraphrase of it. PayPal Payouts is asynchronous: the batch is accepted at once and each item settles after, so there is a window where the product holds a reference and not an outcome. "Unclaimed" is called out separately because it is not a transfer in flight — PayPal has the money and cannot deliver it, and it returns to the platform after 30 days unless the payee is corrected. */ @endphp @php $ps = $providerStatus ?? null; @endphp <div class="alert {{ $ps === 'unclaimed' ? 'alert-warning' : 'alert-info' }} mb-0" role="status"> <span class="qm-alert-ico"><i class="fas fa-{{ $ps === 'unclaimed' ? 'exclamation-triangle' : 'paper-plane' }}" aria-hidden="true"></i></span> <div class="qm-alert-body"> @if ($ps === 'unclaimed') {{ t_raw('admin.payouts.provider_unclaimed', [':ref' => e((string) $payout['payment_ref'])]) }} @elseif ($ps === 'pending') {{ t_raw('admin.payouts.provider_pending', [':ref' => e((string) $payout['payment_ref'])]) }} @else {{ t_raw('admin.payouts.awaiting_gateway', [':ref' => e((string) $payout['payment_ref'])]) }} @endif </div> </div> @else <form method="post" action="{{ route('admin.payouts.settle') }}" data-confirm="{{ t_raw($producesInvoices ? 'admin.payouts.confirm_settle_invoice' : 'admin.payouts.confirm_settle', [':amount' => money($payout['net_payable'])]) }}"> @csrf <input type="hidden" name="payout_id" value="{{ (int) $payout['id'] }}"> <input type="hidden" name="status" value="paid"> <div class="qm-form-grid"> <div class="mb-3"> <label class="form-label" for="payout-ref">{{ t_raw('admin.shared.payment_reference') }}</label> <input type="text" id="payout-ref" name="payment_ref" class="form-control" placeholder="{{ t_raw('admin.shared.payment_ref_placeholder') }}" required> </div> <div class="mb-3"> <label class="form-label" for="payout-adj">{{ t_raw('admin.payouts.field_adjustment') }}</label> <input type="number" step="0.01" id="payout-adj" name="adjustment" class="form-control" value="{!! $adjustments != 0.0 ? e((string) $adjustments) : '' !!}" placeholder="{{ t_raw('common.placeholder_amount') }}"> <div class="qm-cell-sub mt-1">{{ t_raw('admin.payouts.adjustment_hint') }}</div> </div> <div class="mb-3"> <label class="form-label" for="payout-adj-note">{{ t_raw('admin.payouts.field_adjustment_note') }}</label> <input type="text" id="payout-adj-note" name="adjustment_note" class="form-control" value="{{ (string) ($payout['adjustment_note'] ?? '') }}" placeholder="{{ t_raw('admin.payouts.adjustment_note_placeholder') }}"> </div> </div> <div class="d-flex justify-content-end"> <button type="submit" class="qm-btn"><i class="fas fa-money-check-alt me-1" aria-hidden="true"></i>{{ t_raw('admin.payouts.settle_btn') }}</button> </div> </form> @endif </div> @endif <div class="qm-card"> <div class="qm-ops-head"> <div> <h2 class="mb-0">{{ t_raw('admin.payouts.lines_title') }}</h2> <p class="qm-ops-sub mb-0">{{ t_raw('admin.payouts.lines_sub') }}</p> </div> </div> <div class="table-responsive"> <table class="qm-table" data-sortable> <caption class="visually-hidden">{{ t_raw('admin.payouts.lines_title') }}</caption> <thead><tr> <th data-sort="text">{{ t_raw('vendor.shifts.col_order') }}</th> <th data-sort="date">{{ t_raw('common.date') }}</th> <th data-sort="text">{{ t_raw('admin.payouts.col_method') }}</th> <th data-sort="number">{{ t_raw('admin.payouts.col_order_total') }}</th> <th data-sort="number">{{ t_raw('admin.payouts.col_rate') }}</th> <th data-sort="number">{{ t_raw('common.commission') }}</th> @php /* The line's `net` is takings−commission on a payout (what the platform owes back) and the commission itself on an invoice (the royalty the restaurant owes), so the head names the party the column runs to. */ @endphp <th data-sort="number" class="text-end">{{ t_raw($producesInvoices ? 'admin.payouts.col_net_invoice' : 'admin.payouts.col_net') }}</th> </tr></thead> <tbody> @if ($linesPage) @foreach ($linesPage as $line) <tr> <td class="qm-cell-body qm-nowrap"><strong>{!! or_dash($line['order_number'] ?? null) !!}</strong></td> <td class="qm-cell-body qm-nowrap" data-sort-value="{{ (string) ($line['placed_at'] ?? '') }}">{!! !empty($line['placed_at']) ? fmt_day($line['placed_at']) : dash() !!}</td> <td>@php $m = (string) ($line['payment_method'] ?? ''); $ico = $m !== '' ? payment_method_icon($m) : ''; echo $ico !== '' ? $ico : ($m !== '' ? status_label('payment_method', $m) : dash()); @endphp</td> <td data-sort-value="{{ (string) $line['order_total'] }}">{{ money($line['order_total']) }}</td> <td data-sort-value="{{ (string) ($line['commission_rate'] ?? 0) }}">{!! $line['commission_rate'] !== null ? e((string) (float) $line['commission_rate']) . '%' : dash() !!}</td> <td data-sort-value="{{ (string) $line['commission_amount'] }}">{{ money($line['commission_amount']) }}</td> <td class="text-end" data-sort-value="{{ (string) $line['net'] }}"><strong>{{ money($line['net']) }}</strong></td> </tr> @endforeach @else @partial('table-empty', ['colspan' => 7, 'icon' => 'fa-receipt', 'msg' => t('admin.payouts.lines_empty')]) @endif </tbody> @if ($lines) <tfoot> <tr> <td colspan="3"><strong>{{ t_raw('admin.payouts.lines_total', [':count' => number_format(count($lines))]) }}</strong></td> <td><strong>{{ money($linesGross) }}</strong></td> <td>{!! dash() !!}</td> <td><strong>{{ money($linesCommission) }}</strong></td> <td class="text-end"><strong>{{ money($linesNet) }}</strong></td> </tr> </tfoot> @endif </table> </div> @partial('table-toolbar', ['pager' => $linesPager, 'base' => '/admin/payouts/' . (int) $payout['id']]) </div> </div>@php /* /.qm-d2 */ @endphp @endsection