CluPilotCloud/resources/views/livewire/admin/customer-detail.blade.php

454 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

@php
$money = fn (?int $cents, string $currency = 'EUR') => $cents === null
? '—'
: Number::currency($cents / 100, in: $currency, locale: app()->getLocale());
$date = fn ($value) => $value === null ? '—' : $value->local()->isoFormat('D. MMM YYYY');
@endphp
<div class="mx-auto max-w-[1120px] space-y-6">
{{-- ── Who, at a glance ─────────────────────────────────────────────────
The four things somebody on the phone needs before they say anything:
who it is, whether the account is live, what they are on, and whether
the machine is running. --}}
<div class="animate-rise">
<a href="{{ route('admin.customers') }}" wire:navigate class="text-xs font-semibold text-muted hover:text-ink">
{{ __('customer_detail.back') }}
</a>
<div class="mt-1 flex flex-wrap items-center gap-x-4 gap-y-2">
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ $customer->name }}</h1>
<x-ui.badge :status="$customer->status === 'active' ? 'active' : 'warning'">
{{ __('admin.status.'.$customer->status) }}
</x-ui.badge>
@if ($subscription)
<x-ui.badge status="provisioning">{{ __('billing.plan.'.$subscription->plan) }}</x-ui.badge>
@endif
<a href="mailto:{{ $customer->email }}" class="ml-auto font-mono text-sm text-muted hover:text-ink">{{ $customer->email }}</a>
</div>
</div>
{{-- ── The tabs ─────────────────────────────────────────────────────────
Four different questions, and stacking them made a page nobody read to
the bottom. Writing is last, because it is what you do after reading.
The choice is in the query string, so a reload, a bookmark and the back
button all land where the operator was. --}}
<div class="flex flex-wrap items-center gap-x-1 gap-y-2 border-b border-line animate-rise [animation-delay:40ms]" role="tablist">
@foreach ($tabs as $name)
<button type="button" role="tab" wire:click="$set('tab', '{{ $name }}')"
aria-selected="{{ $tab === $name ? 'true' : 'false' }}"
@class([
'flex items-center gap-2 whitespace-nowrap border-b-2 px-4 py-2.5 text-sm font-medium transition-colors -mb-px',
'border-accent-active text-ink' => $tab === $name,
'border-transparent text-muted hover:text-ink' => $tab !== $name,
])>
<x-ui.icon :name="['overview' => 'users', 'package' => 'box', 'billing' => 'receipt', 'messages' => 'mail', 'compose' => 'send'][$name] ?? 'settings'"
class="size-4" />{{ __('customer_detail.tab.'.$name) }}
@if ($name === 'messages' && $openRequests > 0)
<span class="rounded-pill bg-warning-bg px-1.5 text-[10px] font-semibold text-warning">{{ $openRequests }}</span>
@endif
</button>
@endforeach
</div>
{{-- ══ Stammdaten ═══════════════════════════════════════════════════════ --}}
@if ($tab === 'overview')
<div class="grid gap-5 lg:grid-cols-2 lg:items-start">
{{-- `editing` is local: fetching the modal is a round trip, and a
click with nothing to show for it reads as a click that missed. --}}
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise"
x-data="{ editing: false }"
x-on:modal-opened.window="editing = false"
x-on:modalclosed.window="editing = false">
<div class="flex flex-wrap items-center gap-3">
<h2 class="font-semibold text-ink">{{ __('customer_detail.identity') }}</h2>
{{-- R20: editing happens in a modal, never in the row. --}}
<x-ui.button variant="secondary" size="sm" class="ml-auto"
x-bind:disabled="editing"
x-on:click="editing = true; setTimeout(() => editing = false, 6000); $dispatch('openModal', { component: 'admin.edit-customer', arguments: { uuid: '{{ $customer->uuid }}' } })">
<x-ui.icon name="pencil" class="size-4" x-show="!editing" />
<x-ui.icon name="refresh" class="size-4 animate-spin" x-show="editing" x-cloak />
{{ __('customer_detail.edit') }}
</x-ui.button>
</div>
<dl class="mt-4 space-y-3 text-sm">
@foreach ([
__('customer_detail.company') => $customer->name,
__('customer_detail.contact') => $customer->contact_name ?: '—',
__('customer_detail.email') => $customer->email,
__('customer_detail.phone') => $customer->phone ?: '—',
__('customer_detail.since') => $date($customer->created_at),
__('customer_detail.locale') => strtoupper((string) ($customer->locale ?: '—')),
] as $term => $value)
<div class="flex justify-between gap-4 border-b border-line pb-3 last:border-0 last:pb-0">
<dt class="text-muted">{{ $term }}</dt>
<dd class="text-right font-medium text-ink">{{ $value }}</dd>
</div>
@endforeach
</dl>
</div>
<div class="space-y-5">
{{-- Whose money it is, in the tax sense. Consumer or business is
not cosmetic: it decides whether a withdrawal right exists
at all, and "nobody asked" is treated as consumer. --}}
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
<h2 class="font-semibold text-ink">{{ __('customer_detail.tax') }}</h2>
<dl class="mt-4 space-y-3 text-sm">
<div class="flex justify-between gap-4">
<dt class="text-muted">{{ __('customer_detail.type') }}</dt>
<dd class="text-right font-medium text-ink">
@if ($customer->hasRecordedType())
{{ __('customer_detail.type_'.$customer->customer_type) }}
@else
<span class="text-warning">{{ __('customer_detail.type_unknown') }}</span>
@endif
</dd>
</div>
<div class="flex justify-between gap-4">
<dt class="text-muted">{{ __('customer_detail.vat_id') }}</dt>
<dd class="text-right font-mono text-ink">
{{ $customer->vat_id ?: '—' }}
@if ($customer->vat_id && $customer->hasVerifiedVatId())
<x-ui.badge status="active" class="ml-1">{{ __('customer_detail.verified') }}</x-ui.badge>
@elseif ($customer->vat_id)
<x-ui.badge status="warning" class="ml-1">{{ __('customer_detail.unverified') }}</x-ui.badge>
@endif
</dd>
</div>
<div class="flex justify-between gap-4">
<dt class="text-muted">{{ __('customer_detail.stripe') }}</dt>
<dd class="text-right font-mono text-xs text-muted">{{ $customer->stripe_customer_id ?: '—' }}</dd>
</div>
</dl>
</div>
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
<h2 class="font-semibold text-ink">{{ __('customer_detail.address') }}</h2>
<p class="mt-3 whitespace-pre-line text-sm leading-relaxed text-body">
{{ $customer->billing_address ?: __('customer_detail.no_address') }}
</p>
</div>
</div>
</div>
@endif
{{-- ══ Paket & Maschine ═════════════════════════════════════════════════ --}}
@if ($tab === 'package')
@if ($subscription === null)
<x-ui.alert variant="info">{{ __('customer_detail.no_contract') }}</x-ui.alert>
@else
<div class="grid gap-5 lg:grid-cols-2 lg:items-start">
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
<div class="flex flex-wrap items-center gap-3">
<h2 class="font-semibold text-ink">{{ __('billing.plan.'.$subscription->plan) }}</h2>
<x-ui.badge :status="$subscription->status === 'active' ? 'active' : 'warning'">
{{ $subscription->status }}
</x-ui.badge>
</div>
{{-- The contract's OWN frozen figures, not today's catalogue:
this is what the customer is owed, and a price rise does
not reach back into it. --}}
<dl class="mt-4 space-y-3 text-sm">
@foreach ([
__('customer_detail.price') => $money($subscription->price_cents, $subscription->currency).' '.__('customer_detail.net_per', ['term' => __('customer_detail.term_'.$subscription->term)]),
__('customer_detail.quota') => $subscription->quota_gb.' GB',
__('customer_detail.traffic') => $subscription->traffic_gb.' GB',
__('customer_detail.seats') => $subscription->seats,
__('customer_detail.started') => $date($subscription->started_at),
__('customer_detail.period_end') => $date($subscription->current_period_end),
] as $term => $value)
<div class="flex justify-between gap-4 border-b border-line pb-3 last:border-0 last:pb-0">
<dt class="text-muted">{{ $term }}</dt>
<dd class="text-right font-medium text-ink">{{ $value }}</dd>
</div>
@endforeach
</dl>
@if ($subscription->cancelled_at)
<x-ui.alert variant="warning" class="mt-4">
{{ __('customer_detail.cancelled', ['date' => $date($subscription->cancelled_at)]) }}
</x-ui.alert>
@endif
</div>
<div class="space-y-5">
@if ($subscription->addons->isNotEmpty())
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
<h2 class="font-semibold text-ink">{{ __('customer_detail.addons') }}</h2>
<ul class="mt-3 divide-y divide-line text-sm">
@foreach ($subscription->addons as $addon)
{{-- addon_key and price_cents are the column
names; the price is the one it was
BOOKED at, which is what the customer
agreed to and what the row freezes. --}}
<li class="flex flex-wrap items-baseline justify-between gap-3 py-2.5">
<span class="text-body">
{{ __('billing.addon.'.$addon->addon_key.'.name') }}
@if ($addon->cancels_at)
<span class="text-xs text-warning">
{{ __('customer_detail.addon_until', ['date' => $date($addon->cancels_at)]) }}
</span>
@endif
</span>
<span class="font-mono tabular-nums text-ink">
{{ $addon->quantity > 1 ? $addon->quantity.' × ' : '' }}{{ $money($addon->price_cents, $subscription->currency) }}
</span>
</li>
@endforeach
</ul>
</div>
@endif
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
<h2 class="font-semibold text-ink">{{ __('customer_detail.machine') }}</h2>
@if ($instance === null)
<p class="mt-3 text-sm text-muted">{{ __('customer_detail.no_instance') }}</p>
@else
<dl class="mt-4 space-y-3 text-sm">
@foreach ([
__('customer_detail.address_web') => $instance->subdomain ?: '—',
__('customer_detail.instance_status') => __('admin.status.'.$instance->status),
__('customer_detail.host') => $instance->host?->name ?? '—',
__('customer_detail.datacenter') => $instance->host?->datacenter ?? '—',
__('customer_detail.disk') => $instance->disk_gb.' GB',
] as $term => $value)
<div class="flex justify-between gap-4 border-b border-line pb-3 last:border-0 last:pb-0">
<dt class="text-muted">{{ $term }}</dt>
<dd class="text-right font-mono text-ink">{{ $value }}</dd>
</div>
@endforeach
</dl>
@endif
</div>
</div>
</div>
@endif
@endif
{{-- ══ Zahlungen ════════════════════════════════════════════════════════ --}}
@if ($tab === 'billing')
<div class="space-y-5">
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise">
<div class="flex flex-wrap items-center gap-3 border-b border-line px-6 py-4">
<h2 class="font-semibold text-ink">{{ __('customer_detail.invoices') }}</h2>
<x-ui.button :href="route('admin.invoices.new', ['customer' => $customer->uuid])"
variant="secondary" size="sm" class="ml-auto" wire:navigate>
<x-ui.icon name="receipt" class="size-4" />{{ __('customer_detail.write_invoice') }}
</x-ui.button>
</div>
@if ($invoices->isEmpty())
<p class="p-8 text-center text-sm text-muted">{{ __('customer_detail.no_invoices') }}</p>
@else
<table class="w-full text-sm">
<thead>
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
<th class="px-6 py-3 font-semibold">{{ __('customer_detail.number') }}</th>
<th class="px-6 py-3 font-semibold">{{ __('customer_detail.issued') }}</th>
<th class="px-6 py-3 text-right font-semibold">{{ __('customer_detail.net') }}</th>
<th class="px-6 py-3 text-right font-semibold">{{ __('customer_detail.gross') }}</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody>
@foreach ($invoices as $invoice)
<tr class="border-b border-line last:border-0">
<td class="px-6 py-3 font-mono text-ink">{{ $invoice->number }}</td>
<td class="px-6 py-3 text-muted">{{ $date($invoice->issued_on) }}</td>
<td class="px-6 py-3 text-right font-mono tabular-nums text-body">{{ $money($invoice->net_cents, $invoice->currency) }}</td>
<td class="px-6 py-3 text-right font-mono tabular-nums font-semibold text-ink">{{ $money($invoice->gross_cents, $invoice->currency) }}</td>
<td class="px-6 py-3 text-right">
<a href="{{ route('admin.invoices.pdf', $invoice->uuid) }}"
class="text-xs font-semibold text-accent-text hover:underline">PDF</a>
</td>
</tr>
@endforeach
</tbody>
</table>
@endif
</div>
{{-- What they actually bought, as opposed to what was invoiced. The
two answer different questions: an order is a purchase, an
invoice is a document about one. --}}
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
<h2 class="border-b border-line px-6 py-4 font-semibold text-ink">{{ __('customer_detail.orders') }}</h2>
@if ($orders->isEmpty())
<p class="p-8 text-center text-sm text-muted">{{ __('customer_detail.no_orders') }}</p>
@else
<ul class="divide-y divide-line">
@foreach ($orders as $order)
<li class="flex flex-wrap items-baseline gap-x-4 gap-y-1 px-6 py-3 text-sm">
<span class="font-medium text-ink">{{ $order->label() }}</span>
<x-ui.badge :status="$order->status === 'paid' ? 'active' : 'warning'">{{ $order->status }}</x-ui.badge>
<span class="font-mono text-xs text-muted">{{ $date($order->created_at) }}</span>
<span class="ml-auto font-mono tabular-nums text-ink">{{ $money($order->amount_cents, $order->currency) }}</span>
</li>
@endforeach
</ul>
@endif
</div>
</div>
@endif
{{-- ══ Nachrichten ══════════════════════════════════════════════════════ --}}
@if ($tab === 'messages')
<div class="space-y-5">
{{-- What they asked through the portal --}}
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
<h2 class="font-semibold text-ink">{{ __('customer_detail.requests') }}</h2>
@if ($requests->isEmpty())
<p class="mt-3 text-sm text-muted">{{ __('customer_detail.no_requests') }}</p>
@else
<ul class="mt-3 divide-y divide-line">
@foreach ($requests as $request)
<li class="py-4">
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<span class="font-medium text-ink">{{ $request->subject }}</span>
<x-ui.badge :status="$request->isOpen() ? 'warning' : 'active'">
{{ __('support.status_'.$request->status) }}
</x-ui.badge>
<span class="ml-auto text-xs text-muted">{{ $request->created_at->local()->isoFormat('D. MMM YYYY, HH:mm') }}</span>
</div>
<p class="mt-2 whitespace-pre-line text-sm leading-relaxed text-body">{{ $request->body }}</p>
@if ($request->isOpen())
<x-ui.button wire:click="answer('{{ $request->uuid }}')" variant="secondary" size="sm" class="mt-3">
{{ __('customer_detail.answer') }}
</x-ui.button>
@endif
</li>
@endforeach
</ul>
@endif
</div>
{{-- What they wrote by mail --}}
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
<div class="flex flex-wrap items-center gap-3">
<h2 class="font-semibold text-ink">{{ __('customer_detail.inbound') }}</h2>
<a href="{{ route('admin.inbox') }}" class="ml-auto text-xs font-semibold text-accent-text hover:underline">
{{ __('customer_detail.to_inbox') }}
</a>
</div>
@if ($inbound->isEmpty())
<p class="mt-3 text-sm text-muted">{{ __('customer_detail.no_inbound') }}</p>
@else
<ul class="mt-3 divide-y divide-line">
@foreach ($inbound as $mail)
<li class="py-3">
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm">
<span class="font-medium text-ink">{{ $mail->subject ?: __('inbox.no_subject') }}</span>
@if ($mail->hasAttachments())
<x-ui.badge status="warning">{{ __('inbox.attachments') }}: {{ count($mail->attachments) }}</x-ui.badge>
@endif
<span class="ml-auto font-mono text-xs text-muted">{{ $mail->received_at->local()->isoFormat('D. MMM YYYY, HH:mm') }}</span>
</div>
<p class="mt-1.5 whitespace-pre-line text-xs leading-relaxed text-body">{{ $mail->body }}</p>
</li>
@endforeach
</ul>
@endif
</div>
{{-- What we sent --}}
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
<div class="flex flex-wrap items-center gap-3">
<h2 class="font-semibold text-ink">{{ __('customer_detail.mails') }}</h2>
<a href="{{ route('admin.mail-log') }}" class="ml-auto text-xs font-semibold text-accent-text hover:underline">
{{ __('customer_detail.all_mails') }}
</a>
</div>
@if ($mails->isEmpty())
<p class="mt-3 text-sm text-muted">{{ __('customer_detail.no_mails') }}</p>
@else
<ul class="mt-3 divide-y divide-line">
@foreach ($mails as $mail)
<li class="py-3">
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm">
<span class="font-medium text-ink">{{ $mail->subject }}</span>
@if ($mail->from_operator)
<x-ui.badge status="provisioning">{{ __('customer_detail.by_hand') }}</x-ui.badge>
@endif
<span class="ml-auto font-mono text-xs text-muted">
{{ $mail->sent_at->local()->isoFormat('D. MMM YYYY, HH:mm') }}
</span>
</div>
@if ($mail->body)
<p class="mt-1.5 whitespace-pre-line text-xs leading-relaxed text-muted">{{ $mail->body }}</p>
@else
<p class="mt-1 font-mono text-xs text-faint">{{ $mail->kind() }}</p>
@endif
</li>
@endforeach
</ul>
@endif
</div>
</div>
@endif
{{-- ══ Schreiben ════════════════════════════════════════════════════════ --}}
@if ($tab === 'compose')
<div class="grid gap-5 lg:grid-cols-[1fr_300px] lg:items-start">
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
<div class="flex flex-wrap items-center gap-3">
<h2 class="font-semibold text-ink">{{ __('customer_message.title') }}</h2>
@if ($answering)
<x-ui.badge status="provisioning">{{ __('customer_message.answering') }}</x-ui.badge>
<button type="button" wire:click="cancelAnswer" class="text-xs font-semibold text-muted hover:text-ink">
{{ __('customer_message.cancel_answer') }}
</button>
@endif
</div>
<form wire:submit="send" class="space-y-4">
<x-ui.input name="subject" wire:model="subject" :label="__('customer_message.subject')" />
<div>
<label for="body" class="block text-sm font-medium text-body">{{ __('customer_message.body') }}</label>
<textarea id="body" rows="12" wire:model="body"
class="mt-1.5 block w-full rounded border border-line bg-surface px-3.5 py-2.5 text-sm text-ink placeholder:text-faint"
placeholder="{{ __('customer_message.body_hint') }}"></textarea>
@error('body')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</div>
<div class="flex flex-wrap items-center gap-3">
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="send">
<x-ui.icon name="send" class="size-4" />{{ __('customer_message.send') }}
</x-ui.button>
<p class="text-xs text-muted">{{ __('customer_message.note', ['email' => $customer->email]) }}</p>
</div>
</form>
</div>
{{-- The templates. Inserted with this customer's own details already
in them; the last two sentences stay the operator's to write. --}}
<div class="space-y-3 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms] lg:sticky lg:top-6">
<div class="flex flex-wrap items-center gap-2">
<h2 class="font-semibold text-ink">{{ __('customer_message.templates') }}</h2>
<a href="{{ route('admin.templates') }}" class="ml-auto text-xs font-semibold text-accent-text hover:underline">
{{ __('customer_message.manage_templates') }}
</a>
</div>
@if ($templates->isEmpty())
<p class="text-xs leading-relaxed text-muted">{{ __('customer_message.no_templates') }}</p>
@else
<p class="text-xs leading-relaxed text-muted">{{ __('customer_message.templates_note') }}</p>
<ul class="space-y-1.5">
@foreach ($templates as $template)
<li>
<button type="button" wire:click="useTemplate('{{ $template->uuid }}')"
class="w-full rounded-md border border-line bg-surface-2 px-3 py-2 text-left text-sm text-body transition-colors hover:border-accent-border hover:text-ink">
{{ $template->name }}
</button>
</li>
@endforeach
</ul>
@endif
</div>
</div>
@endif
</div>