245 lines
15 KiB
PHP
245 lines
15 KiB
PHP
@php
|
|
$eur = fn (int $cents) => Illuminate\Support\Number::currency($cents / 100, in: $currency, locale: app()->getLocale());
|
|
@endphp
|
|
<div class="space-y-5">
|
|
<div class="animate-rise">
|
|
<a href="{{ route('admin.plans') }}" wire:navigate class="inline-flex items-center gap-1.5 text-xs font-medium text-muted hover:text-body">
|
|
<x-ui.icon name="arrow-left" class="size-3.5" />
|
|
{{ __('plans.back') }}
|
|
</a>
|
|
<h1 class="mt-2 text-2xl font-bold tracking-tight text-ink">{{ $family->name }}</h1>
|
|
<p class="mt-1 text-sm text-muted">{{ __('plans.versions_subtitle') }}</p>
|
|
</div>
|
|
|
|
@unless ($family->sales_enabled)
|
|
{{-- The kill switch overrides every window, so say so once at the top
|
|
rather than leaving each version to look live on its own. --}}
|
|
<div class="rounded-lg border border-warning-border bg-warning-bg p-4 text-sm text-body animate-rise">
|
|
{{ __('plans.family_withdrawn_note') }}
|
|
</div>
|
|
@endunless
|
|
|
|
<div class="grid grid-cols-1 gap-5 xl:grid-cols-[1fr_380px] xl:items-start">
|
|
{{-- Versions --}}
|
|
<div class="space-y-4 animate-rise [animation-delay:60ms]">
|
|
@error('availableFrom')
|
|
@if ($publishing === null)
|
|
{{-- A refused row action has no form of its own to report
|
|
into: with the publish form closed, the message would
|
|
otherwise land in an error bag nothing renders, and the
|
|
button would look like it had simply done nothing. --}}
|
|
<div class="rounded-lg border border-danger bg-danger-bg p-4 text-sm text-body">{{ $message }}</div>
|
|
@endif
|
|
@enderror
|
|
|
|
@forelse ($versions as $version)
|
|
@php
|
|
// The window being open is not the same as the plan being
|
|
// sold: the family's kill switch overrides every window.
|
|
$windowOpen = $version->isAvailableAt($now);
|
|
$live = $windowOpen && $family->sales_enabled;
|
|
$scheduled = $version->isPublished() && $version->available_from->greaterThan($now);
|
|
@endphp
|
|
<div wire:key="v-{{ $version->uuid }}" class="rounded-lg border border-line bg-surface p-5 shadow-xs
|
|
{{ $live ? 'border-success-border' : '' }}">
|
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<h2 class="text-base font-semibold text-ink">{{ __('plans.version', ['n' => $version->version]) }}</h2>
|
|
@if ($live)
|
|
<x-ui.badge status="active">{{ __('plans.badge_live') }}</x-ui.badge>
|
|
@elseif ($windowOpen)
|
|
<x-ui.badge status="suspended">{{ __('plans.badge_withdrawn') }}</x-ui.badge>
|
|
@elseif ($scheduled)
|
|
<x-ui.badge status="pending">{{ __('plans.badge_scheduled') }}</x-ui.badge>
|
|
@elseif ($version->isPublished())
|
|
<x-ui.badge status="closed">{{ __('plans.badge_closed') }}</x-ui.badge>
|
|
@else
|
|
<x-ui.badge status="warning">{{ __('plans.badge_draft') }}</x-ui.badge>
|
|
@endif
|
|
</div>
|
|
<p class="mt-1 text-xs text-muted">
|
|
@if ($version->isPublished())
|
|
{{ __('plans.window', [
|
|
'from' => $version->available_from->local()->isoFormat('L LT'),
|
|
'until' => $version->available_until?->local()->isoFormat('L LT') ?? __('plans.open_ended'),
|
|
]) }}
|
|
@else
|
|
{{ __('plans.draft_hint') }}
|
|
@endif
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
@if (! $version->isPublished())
|
|
<x-ui.button size="sm" wire:click="choose('{{ $version->uuid }}')">{{ __('plans.publish') }}</x-ui.button>
|
|
<x-ui.button variant="secondary" size="sm"
|
|
x-on:click="Livewire.dispatch('openModal', { component: 'admin.confirm-delete-plan-draft', arguments: { uuid: '{{ $version->uuid }}' } })">
|
|
{{ __('plans.discard') }}
|
|
</x-ui.button>
|
|
@else
|
|
@if ($windowOpen)
|
|
<x-ui.button variant="secondary" size="sm" wire:click="close('{{ $version->uuid }}')">
|
|
{{ __('plans.close_now') }}
|
|
</x-ui.button>
|
|
@endif
|
|
{{-- Every published version with an end date can
|
|
go back on open-ended sale, whether that end
|
|
is already past or still ahead. Closing used
|
|
to be one-way. --}}
|
|
@if ($version->available_until !== null)
|
|
<x-ui.button variant="secondary" size="sm" wire:click="reopen('{{ $version->uuid }}')">
|
|
{{ __('plans.reopen') }}
|
|
</x-ui.button>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Publish form, inline under the draft it applies to --}}
|
|
@if ($publishing === $version->uuid)
|
|
<div class="mt-4 rounded-lg border border-line-strong bg-surface-2 p-4">
|
|
<p class="text-sm text-body">{{ __('plans.publish_warning') }}</p>
|
|
@if ($handover !== null)
|
|
{{-- Publishing here ends another version's sale,
|
|
so say which one and when, before the button
|
|
is pressed rather than in the list after. --}}
|
|
<p class="mt-3 rounded border border-warning-border bg-warning-bg px-3 py-2 text-sm text-body">
|
|
{{ __('plans.handover_note', ['version' => $handover['version'], 'at' => $handover['at']]) }}
|
|
</p>
|
|
@endif
|
|
<div class="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<x-ui.input name="availableFrom" type="datetime-local"
|
|
:label="__('plans.available_from')" wire:model="availableFrom" />
|
|
<x-ui.input name="availableUntil" type="datetime-local"
|
|
:label="__('plans.available_until')" :hint="__('plans.available_until_hint')"
|
|
wire:model="availableUntil" />
|
|
</div>
|
|
<div class="mt-4 flex justify-end gap-2">
|
|
<x-ui.button variant="secondary" size="sm" wire:click="cancelPublish">{{ __('plans.cancel') }}</x-ui.button>
|
|
<x-ui.button size="sm" wire:click="publish" wire:loading.attr="disabled">{{ __('plans.publish_confirm') }}</x-ui.button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<dl class="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-sm sm:grid-cols-4">
|
|
@foreach ([
|
|
['plans.f_storage', $version->quota_gb.' GB'],
|
|
['plans.f_traffic', $version->traffic_gb.' GB'],
|
|
['plans.f_seats', $version->seats],
|
|
['plans.f_performance', __('billing.perf.'.($version->performance ?? 'standard'))],
|
|
['plans.f_ram', $version->ram_mb.' MB'],
|
|
['plans.f_cores', $version->cores],
|
|
['plans.f_disk', $version->disk_gb.' GB'],
|
|
['plans.f_template', $version->template_vmid ?? '—'],
|
|
] as [$label, $value])
|
|
<div>
|
|
<dt class="text-xs font-semibold text-muted">{{ __($label) }}</dt>
|
|
<dd class="text-body">{{ $value }}</dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
|
|
<div class="mt-4 flex flex-wrap gap-4 border-t border-line pt-3 text-sm">
|
|
@foreach ($version->prices->sortBy('term') as $price)
|
|
<span class="text-body">
|
|
<span class="text-xs font-semibold text-muted">{{ __('plans.term_'.$price->term) }}</span>
|
|
<span class="ml-1.5 font-semibold text-ink">{{ $eur($price->amount_cents) }}</span>
|
|
<span class="text-xs text-muted">{{ __('plans.net') }}</span>
|
|
</span>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($version->features)
|
|
<div class="mt-3 flex flex-wrap gap-1.5">
|
|
@foreach ($version->features as $feature)
|
|
<span class="rounded-pill border border-line-strong bg-surface-2 px-2 py-0.5 text-xs text-muted">
|
|
{{ __('billing.feature.'.$feature) }}
|
|
</span>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@empty
|
|
<p class="rounded-lg border border-line bg-surface p-8 text-center text-sm text-muted shadow-xs">
|
|
{{ __('plans.no_versions') }}
|
|
</p>
|
|
@endforelse
|
|
</div>
|
|
|
|
{{-- New draft --}}
|
|
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
|
<h2 class="text-base font-semibold text-ink">{{ __('plans.draft_title') }}</h2>
|
|
<p class="mt-1 text-sm text-muted">{{ __('plans.draft_body') }}</p>
|
|
|
|
<form wire:submit="draft" class="mt-4 space-y-4">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<x-ui.input name="quotaGb" type="number" min="1" max="1000000" :label="__('plans.f_storage')" wire:model="quotaGb" />
|
|
<x-ui.input name="trafficGb" type="number" min="0" max="10000000" :label="__('plans.f_traffic')" wire:model="trafficGb" />
|
|
<x-ui.input name="seats" type="number" min="1" max="100000" :label="__('plans.f_seats')" wire:model="seats" />
|
|
<div class="space-y-1.5">
|
|
<label for="performance" class="block text-sm font-medium text-body">{{ __('plans.f_performance') }}</label>
|
|
{{-- A list, not free text: a typo here would be published,
|
|
frozen, and shown to customers as a raw key. --}}
|
|
<select id="performance" wire:model="performance"
|
|
class="block w-full rounded border border-line bg-surface px-3 py-2 text-sm text-ink">
|
|
@foreach ($performanceClasses as $value => $label)
|
|
<option value="{{ $value }}">{{ $label }}</option>
|
|
@endforeach
|
|
</select>
|
|
@error('performance')<p class="text-xs text-danger">{{ $message }}</p>@enderror
|
|
</div>
|
|
</div>
|
|
|
|
<p class="pt-1 text-xs font-semibold text-muted">{{ __('plans.infra') }}</p>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<x-ui.input name="ramMb" type="number" min="512" max="4194304" :label="__('plans.f_ram')" wire:model="ramMb" />
|
|
<x-ui.input name="cores" type="number" min="1" max="512" :label="__('plans.f_cores')" wire:model="cores" />
|
|
<x-ui.input name="diskGb" type="number" min="1" max="1000000" :label="__('plans.f_disk')" :hint="__('plans.disk_hint')" wire:model="diskGb" />
|
|
<x-ui.input name="templateVmid" type="number" min="100" max="999999999" :label="__('plans.f_template')" wire:model="templateVmid" />
|
|
</div>
|
|
|
|
<p class="pt-1 text-xs font-semibold text-muted">{{ __('plans.pricing') }}</p>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<x-ui.input name="monthlyPrice" inputmode="decimal" :label="__('plans.term_monthly')" :hint="__('plans.euro_hint')"
|
|
wire:model.live.debounce.500ms="monthlyPrice" />
|
|
{{-- Free months, not a second price. The yearly total is
|
|
derived from the two, which is the only arrangement in
|
|
which "2 Monate gratis" on the website and the amount
|
|
Stripe charges cannot drift apart. --}}
|
|
<x-ui.input name="freeMonths" type="number" min="0" max="6" :label="__('plans.free_months')"
|
|
:hint="__('plans.free_months_hint')" wire:model.live="freeMonths" />
|
|
</div>
|
|
|
|
{{-- What that comes to, before it is written. Live, from the same
|
|
arithmetic the draft uses. --}}
|
|
@if ($yearlyPreview !== null)
|
|
<p class="text-xs text-muted">
|
|
{{ __('plans.yearly_preview', [
|
|
'total' => $eur($yearlyPreview),
|
|
'months' => 12 - max(0, min(12, $freeMonths)),
|
|
]) }}
|
|
@if ($freeMonths > 0)
|
|
· {{ trans_choice('plans.free_months_note', $freeMonths, ['count' => $freeMonths]) }}
|
|
@endif
|
|
</p>
|
|
@endif
|
|
|
|
<p class="pt-1 text-xs font-semibold text-muted">{{ __('plans.features') }}</p>
|
|
<div class="space-y-2">
|
|
@foreach ($featureKeys as $key)
|
|
<div wire:key="feat-{{ $key }}">
|
|
<x-ui.checkbox :name="'feature-'.$key" wire:model="features" value="{{ $key }}"
|
|
:label="__('billing.feature.'.$key)" />
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<x-ui.button type="submit" class="w-full" wire:loading.attr="disabled">
|
|
{{ __('plans.draft_create') }}
|
|
</x-ui.button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|