CluPilotCloud/resources/views/livewire/admin/plan-versions.blade.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>