@extends('layouts.admin') @section('title', 'Packages') @section('heading', 'License Packages') @section('subheading', 'Create and manage software license packages.') @php /* | The controller hands over the whole catalogue in one collection, so every | figure below is derived from it — no extra queries, and no filter here can | disagree with what the server sent. */ $totalLicenses = (int) $packages->sum('licenses_count'); // Labels a package earns from its own numbers rather than from a column // that does not exist. "Popular" is the one people actually buy; "Best // value" is the cheapest per day. Both carry a tooltip saying so. $popularId = $packages->where('licenses_count', '>', 0)->sortByDesc('licenses_count')->first()?->id; $valueId = $packages->filter(fn ($p) => $p->duration_days && $p->price > 0) ->sortBy(fn ($p) => $p->price / $p->duration_days)->first()?->id; $badgesFor = function ($package) use ($popularId, $valueId) { $badges = []; if ($package->id === $popularId) { $badges[] = [ 'label' => 'Popular', 'icon' => 'star', 'why' => 'Most licenses issued from this package', 'classes' => 'bg-amber-50 text-amber-700 ring-amber-500/25 dark:bg-amber-500/10 dark:text-amber-400 dark:ring-amber-500/30', ]; } if ($package->id === $valueId) { $badges[] = [ 'label' => 'Best value', 'icon' => 'sparkle', 'why' => 'Lowest price per day of any fixed-term package', 'classes' => 'bg-emerald-50 text-emerald-700 ring-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:ring-emerald-500/30', ]; } if ($package->duration_days === null) { $badges[] = [ 'label' => 'Flexible', 'icon' => 'sliders', 'why' => 'Duration is set on each license rather than by the package', 'classes' => 'bg-blue-50 text-blue-700 ring-blue-500/25 dark:bg-blue-500/10 dark:text-blue-400 dark:ring-blue-500/30', ]; } return $badges; }; // Handed to Alpine as the single source for the drawers, the editor and the // CSV export, so none of them has to scrape the table back out of the DOM. $rows = $packages->map(fn ($p) => [ 'id' => $p->id, 'name' => $p->name, 'slug' => $p->slug, 'duration' => $p->duration_days, 'price' => (float) $p->price, 'billing' => $p->billing_period, 'active' => (bool) $p->is_active, 'licenses' => (int) $p->licenses_count, 'sort' => (int) $p->sort_order, 'created' => $p->created_at?->toIso8601String(), 'createdLabel' => $p->created_at?->format('d M Y') ?? '—', ])->values(); // A failed save bounces back here with the input flashed. `_modal` rides // along unvalidated purely so the editor can be reopened where it was. $reopen = old('_modal') ? [ 'mode' => old('_modal'), 'values' => [ 'id' => old('_pid'), 'name' => old('name'), 'duration_days' => old('duration_days'), 'price' => old('price'), 'billing_period' => old('billing_period', 'custom'), 'sort_order' => old('sort_order', 0), 'is_active' => (bool) old('is_active'), ], ] : null; $blank = new \App\Models\LicensePackage(['is_active' => true]); @endphp @section('header-actions') {{-- The layout renders this inside the top bar, outside the page component, so the click cannot call openCreate() directly. It dispatches instead — the event bubbles to window, where the grid is listening. Still a real link: with JavaScript off it lands on the create page. --}} New @endsection @section('content')
{{-- ── Statistics ──────────────────────────────────────────────── --}} {{-- ── Toolbar ─────────────────────────────────────────────────── --}}
Export this view Export selected
{{-- Price range. Folded away because it is the one filter that needs two inputs and is rarely the one you reach for first. --}}
{{-- ── Quick filters ───────────────────────────────────────── --}}
@foreach (['monthly' => 'Monthly', 'quarterly' => 'Quarterly', 'semiannual' => 'Semiannual', 'annual' => 'Annual', 'custom' => 'Custom'] as $value => $label) @endforeach
{{-- ── Bulk actions ────────────────────────────────────────────── --}}
selected
{{-- Each package is its own PUT, so the count moving is the only honest way to show that something is happening. --}}
Saving… of
{{-- ── Table ───────────────────────────────────────────────────── --}}
@for ($i = 0; $i < 6; $i++)
@endfor
@if ($packages->isEmpty())
@else
@foreach ($packages as $package) @include('packages.partials.row', [ 'package' => $package, 'badges' => $badgesFor($package), 'totalLicenses' => $totalLicenses, ]) @endforeach
{{-- indeterminate exists only as a DOM property, so it has to be assigned rather than bound as an attribute. --}} Package Duration Billing Price Licenses Status Created Actions

No packages match these filters

Nothing in the catalogue fits. Widen the search, or clear the filters to see all {{ $packages->count() }}.

{{-- ── Footer ──────────────────────────────────────────── --}}
Rows Showing of filtered from {{ $packages->count() }} {{ Str::plural('package', $packages->count()) }}
@endif
@include('packages.partials.editor', ['package' => $blank]) @include('packages.partials.details') @include('packages.partials.compare') {{-- ── Toast ───────────────────────────────────────────────────── --}}
@endsection