@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. --}}
| {{-- 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() }}.