@extends('layouts.dashboard') @section('content') @php /** * Super-admin subscription management. * * Lists every restaurant's current subscription standing and what can be done to a term: * record a payment taken outside the product, refund one, or end it early. Recording by hand is * the ONLY way a priced plan can be paid on an install with no gateway keyed — the action existed, * was deleted, and left this page's own tooltip promising it. Mirrors admin/campaigns.php: same * .qm-table pattern, shared component classes, no new CSS. * * @var array $rows one row per restaurant (latest subscription joined to plan + owner) * @var array $pager pagination metadata from paginate * @var array $platformTax the platform's own tax config for subscription invoices */ $statusPill = static function (string $s): string { return match ($s) { 'active' => 'qm-pill-green', 'pending' => 'qm-pill-amber', 'expired', 'cancelled' => 'qm-pill-red', default => 'qm-pill-grey', }; }; $today = date('Y-m-d'); $filter = $filter ?? ''; $counts = $counts ?? []; $allCount = $allCount ?? 0; // The export walks the same door the list shows: the active tab travels with it. $qs = $filter !== '' ? http_build_query(['status' => $filter]) : ''; @endphp @php /* .qm-d2: the dashboard density standard (app.css §35) */ @endphp

{{ t_raw('admin.subscriptions.list_heading') }}

{{ t_raw('common.export_csv') }}
@php /* The same status tabs every other queue wears; counts read the latest real term per restaurant, exactly what each row's pill shows. "All" includes venues that never subscribed, so its count is the venue count, not the tab sum. */ @endphp
@php /* Period is a date range (two values), not a single Date.parse-able cell, so that carries no data-sort. */ @endphp @if ($rows) @foreach ($rows as $s) @php $status = (string) ($s['status'] ?? ''); $planName = (string) ($s['plan_name'] ?? $s['restaurant_plan_name'] ?? ''); $planSlug = (string) ($s['plan_slug'] ?? $s['restaurant_plan_slug'] ?? ''); $isExpired = $status === 'expired' || ($status === 'active' && !empty($s['ends_at']) && $s['ends_at'] < $today); @endphp @php // Ending a term is rare and consequential, so both lifecycle states sit in the // overflow menu, and only while a term is still running — there is nothing to end // on a subscription that has already lapsed. $canEnd = !empty($s['subscription_id']) && in_array($status, ['active', 'pending'], true); // The two lifecycle actions END a term rather than sell one, so they stay behind // the overflow menu; recording a payment and refunding one sit inline, because those // are the actions an operator comes to this page to perform. $rowActions = []; /* The paper a paid term produced. Same invoice the vendor prints, reached through the admin route (no vendor ownership scope on this side). */ if (!empty($s['subscription_id']) && !empty($s['invoice_no'])) { $rowActions[] = [ 'label' => t_raw('vendor.billing.view_invoice'), 'icon' => 'fa-file-invoice', 'href' => '/admin/subscriptions/invoice/' . (int) $s['subscription_id'], 'primary' => true, ]; } if ($canEnd) { $rowActions[] = ['label' => t_raw('admin.subscriptions.mark_expired_btn'), 'icon' => 'fa-hourglass-end', 'form' => [ 'action' => '/admin/subscriptions/lifecycle', 'fields' => ['subscription_id' => (int) $s['subscription_id'], 'status' => 'expired'], 'confirm' => t_raw('admin.subscriptions.confirm_expire', [':restaurant' => (string) $s['restaurant_name']]), ]]; $rowActions[] = ['label' => t_raw('admin.subscriptions.cancel_btn'), 'icon' => 'fa-ban', 'danger' => true, 'form' => [ 'action' => '/admin/subscriptions/lifecycle', 'fields' => ['subscription_id' => (int) $s['subscription_id'], 'status' => 'cancelled'], 'confirm' => t_raw('admin.subscriptions.confirm_cancel', [':restaurant' => (string) $s['restaurant_name']]), ]]; } /* Cancelling stops the NEXT charge; it never returns the money already taken. Until PaymentGateway::refund() existed there was nothing that could — so a term paid by mistake had no remedy inside the product at all. Offered only where there is money to return and a gateway reference to return it against. */ if (!empty($s['subscription_id']) && (float) ($s['amount_paid'] ?? 0) > 0 && $status !== 'refunded' && in_array((string) ($s['payment_method'] ?? ''), ['stripe', 'paypal'], true)) { $rowActions[] = ['label' => t_raw('vendor.orders.refund_btn'), 'icon' => 'fa-undo', 'danger' => true, 'form' => [ 'action' => '/admin/subscriptions/refund', 'fields' => ['subscription_id' => (int) $s['subscription_id']], 'confirm' => t_raw('admin.subscriptions.confirm_refund', [ ':amount' => money((float) $s['amount_paid']), ':restaurant' => (string) $s['restaurant_name'], ':method' => status_label('payment_method', (string) $s['payment_method']), ]), ]]; } @endphp @endforeach @else @partial('table-empty', ['colspan' => 7, 'icon' => 'fa-receipt', 'msg' => t('admin.subscriptions.empty')]) @endif
{{ t_raw('admin.subscriptions.list_heading') }}
{{ t_raw('common.restaurant') }}{{ t_raw('admin.subscriptions.col_plan') }}{{ t_raw('common.status') }}{{ t_raw('common.period') }}{{ t_raw('admin.subscriptions.col_amount') }}{{ t_raw('admin.subscriptions.col_reference') }}{{ t_raw('common.actions') }}
{!! restaurant_logo_html(['logo' => $s['logo'] ?? '', 'name' => $s['restaurant_name'] ?? ''], 'md') !!}
{{ (string) $s['restaurant_name'] }}
{{ (string) ($s['owner_email'] ?? '-') }}
@if ($planName !== '') {!! plan_tag($planSlug, $planName) !!} @php /* The TERM's cadence first — a monthly plan sold annually must say yearly here; only rows from before annual billing fall back to the plan's own cycle. */ @endphp {{ status_label('billing_cycle', (string) (($s['term_cycle'] ?? '') ?: ($s['plan_cycle'] ?? $s['restaurant_plan_cycle'] ?? ''))) }} @else {{ t_raw('admin.subscriptions.no_plan') }} @endif @if ($status !== '') {{ status_label('subscription', $isExpired && $status === 'active' ? 'expired' : $status) }} @else {{ status_label('subscription', 'none') }} @endif @if (!empty($s['starts_at'])) {!! fmt_day($s['starts_at']) !!} – {!! !empty($s['ends_at']) ? fmt_day($s['ends_at']) : '' . t('admin.subscriptions.lifetime') . '' !!} @else {!! dash() !!} @endif {!! isset($s['amount_paid']) && $s['subscription_id'] ? money($s['amount_paid']) : dash() !!} @if (!empty($s['payment_ref'])) {{ (string) $s['payment_ref'] }} @if (!empty($s['payment_method'])) @php $m = (string) $s['payment_method']; $ico = payment_method_icon($m); @endphp · {{ $ico !== '' ? $ico : status_label('payment_method', $m) }} @endif @else {!! dash() !!} @endif @php /* Record payment: money taken OUTSIDE the product — a bank transfer, a cheque, cash — or a gateway payment whose confirmation never landed. Gateway payments record themselves, so this rides in the actions menu with the other exception levers rather than fronting every row as if a settled PayPal term still needed recording. On an install with no gateway keyed it is still the only way a priced plan gets paid. */ @endphp @php $rowActions[] = [ 'label' => t_raw('admin.subscriptions.record_payment_btn'), 'icon' => 'fa-receipt', 'attrs' => [ 'data-bs-toggle' => 'modal', 'data-bs-target' => '#subPayModal', 'data-sub-pay' => '', 'data-restaurant-id' => (string) (int) $s['restaurant_id'], 'data-restaurant-name' => (string) $s['restaurant_name'], 'data-plan-id' => (string) (int) ($s['plan_id'] ?: $s['restaurant_plan_id']), ], ]; @endphp @partial('row-actions', ['inline' => 1, 'actions' => $rowActions, 'menuLabel' => t_raw('admin.subscriptions.lifecycle_menu_label')])
@partial('table-toolbar', ['pager' => $pager, 'base' => '/admin/subscriptions'])
@php /* The platform's own subscription-invoice tax moved to Settings → Subscription plans, beside the catalogue whose prices it changes. This page is the ledger of who has paid; what they are charged is set where the plans are priced. Saved by the same SubscriptionController::saveTax(), which now returns to that tab. */ @endphp @php /* RECORD OFFLINE PAYMENT. The form this page's own tooltip has been describing while the action behind it did not exist. Every string here already lived in lang/ — the action was deleted and its whole vocabulary left behind, which is how we knew exactly what it used to ask for. */ @endphp
@php /* /.qm-d2 */ @endphp @endsection