|
|
|
|
@ -1,27 +1,46 @@
|
|
|
|
|
{{--
|
|
|
|
|
The customer's own settings.
|
|
|
|
|
|
|
|
|
|
## What was wrong, three times over
|
|
|
|
|
## Four attempts, and what was wrong with each
|
|
|
|
|
|
|
|
|
|
First it was one column 768px wide in a 1240px shell, holding everything a
|
|
|
|
|
customer might ever change, two thousand pixels deep. Then it became four
|
|
|
|
|
tabs — better — but the cards inside them were still boxes of different
|
|
|
|
|
heights in a grid, which on the contract tab produced a short one, a wide one
|
|
|
|
|
and a short one again: a staircase with no rhythm and half the width empty
|
|
|
|
|
beside it.
|
|
|
|
|
1. One column 768px wide in a 1240px shell, everything in it, two thousand
|
|
|
|
|
pixels deep.
|
|
|
|
|
2. Four tabs — better — but cards of different heights in a grid, which on
|
|
|
|
|
the contract tab read as a staircase.
|
|
|
|
|
3. Panels and rows: right idea, wrong proportions. Full-width slabs stacked
|
|
|
|
|
down the page, each with its title INSIDE it, so the page was a column of
|
|
|
|
|
long boxes and nothing said where one topic ended.
|
|
|
|
|
|
|
|
|
|
## What it is now
|
|
|
|
|
|
|
|
|
|
Panels and rows (x-ui.panel, x-ui.row). A group is a card with a header that
|
|
|
|
|
names it; a setting is a ROW inside that card — what it is on the left, the
|
|
|
|
|
control on the right, dividers between. That is the shape every settings page
|
|
|
|
|
worth copying uses, and it is the shape that uses the width: a label column
|
|
|
|
|
of 280px with the control beside it fills the line, where a stack of
|
|
|
|
|
full-width inputs leaves two thirds of it empty.
|
|
|
|
|
The shape serious settings pages actually use: a section list on the left,
|
|
|
|
|
a measured content column on the right, and the heading OUTSIDE the card it
|
|
|
|
|
belongs to.
|
|
|
|
|
|
|
|
|
|
Below `sm` the rows stack, because on a telephone a label belongs above its
|
|
|
|
|
field.
|
|
|
|
|
- The nav is the structure. It is sticky, so the sections stay reachable
|
|
|
|
|
however far down a form goes, and it says which one you are in without
|
|
|
|
|
making you read a row of tabs.
|
|
|
|
|
- 820px is the content column. Nobody types a phone number into a field
|
|
|
|
|
1100px wide; a settings page that lets them looks like a spreadsheet.
|
|
|
|
|
- A section is a heading, a sentence, and then a card of rows. Putting the
|
|
|
|
|
heading inside the card gave every group two frames — the title bar and
|
|
|
|
|
the border — and that is what made them read as boxes rather than as
|
|
|
|
|
topics.
|
|
|
|
|
- What cannot be undone lives at the end, in a card that says so with its
|
|
|
|
|
border.
|
|
|
|
|
|
|
|
|
|
Below `lg` the nav becomes a scrolling strip of chips above the content, and
|
|
|
|
|
the rows stack, because on a telephone a label belongs above its field.
|
|
|
|
|
--}}
|
|
|
|
|
@php
|
|
|
|
|
$sections = [
|
|
|
|
|
'profile' => 'users',
|
|
|
|
|
'security' => 'shield-check',
|
|
|
|
|
'branding' => 'pen',
|
|
|
|
|
'contract' => 'receipt',
|
|
|
|
|
];
|
|
|
|
|
@endphp
|
|
|
|
|
|
|
|
|
|
<div class="space-y-6">
|
|
|
|
|
|
|
|
|
|
<header class="animate-rise">
|
|
|
|
|
@ -32,28 +51,52 @@
|
|
|
|
|
<p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('settings.subtitle') }}</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{{-- The same tab bar the console uses, so one vocabulary holds on both sides
|
|
|
|
|
of the login. The choice lives in the query string — see the #[Url]
|
|
|
|
|
attribute on the component. --}}
|
|
|
|
|
<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)
|
|
|
|
|
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:gap-8">
|
|
|
|
|
|
|
|
|
|
{{-- ── The section list ────────────────────────────────────────────
|
|
|
|
|
Sticky on a wide screen: a long form must not scroll its own
|
|
|
|
|
navigation away. A strip of chips below `lg`, because a vertical
|
|
|
|
|
list on a telephone is four rows of nothing. --}}
|
|
|
|
|
<nav class="-mx-4 shrink-0 overflow-x-auto px-4 lg:mx-0 lg:w-[232px] lg:overflow-visible lg:px-0 lg:sticky lg:top-8 animate-rise [animation-delay:40ms]"
|
|
|
|
|
role="tablist" aria-label="{{ __('settings.title') }}">
|
|
|
|
|
<div class="flex gap-1.5 lg:flex-col">
|
|
|
|
|
@foreach ($sections as $name => $icon)
|
|
|
|
|
<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,
|
|
|
|
|
'flex shrink-0 items-center gap-2.5 whitespace-nowrap rounded px-3 py-2 text-sm font-medium transition-colors lg:w-full',
|
|
|
|
|
'bg-surface text-ink shadow-xs ring-1 ring-line' => $tab === $name,
|
|
|
|
|
'text-muted hover:bg-surface-hover hover:text-ink' => $tab !== $name,
|
|
|
|
|
])>
|
|
|
|
|
<x-ui.icon :name="['profile' => 'users', 'security' => 'shield-check', 'branding' => 'pen', 'contract' => 'receipt'][$name] ?? 'settings'"
|
|
|
|
|
class="size-4" />{{ __('settings.tab.'.$name) }}
|
|
|
|
|
<x-ui.icon :name="$icon" @class(['size-4', 'text-accent-text' => $tab === $name]) />
|
|
|
|
|
{{ __('settings.tab.'.$name) }}
|
|
|
|
|
</button>
|
|
|
|
|
@endforeach
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{-- ══ Ihre Daten ═══════════════════════════════════════════════════════ --}}
|
|
|
|
|
{{-- Who is signed in, at the foot of the list. A settings page is one
|
|
|
|
|
of the few where "whose settings are these" is a real question —
|
|
|
|
|
an office with a shared machine has it every day. --}}
|
|
|
|
|
<div class="mt-6 hidden rounded border border-line bg-surface-2 px-3 py-3 lg:block">
|
|
|
|
|
<p class="lbl">{{ __('settings.signed_in_as') }}</p>
|
|
|
|
|
<p class="mt-1.5 truncate text-sm font-medium text-ink">{{ auth()->user()->name }}</p>
|
|
|
|
|
<p class="truncate font-mono text-xs text-muted">{{ auth()->user()->email }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{{-- ── The content column ───────────────────────────────────────────
|
|
|
|
|
820px, not the whole shell. Nobody types a postcode into a field a
|
|
|
|
|
thousand pixels wide. --}}
|
|
|
|
|
<div class="min-w-0 flex-1 lg:max-w-[820px]">
|
|
|
|
|
|
|
|
|
|
{{-- ══ Ihre Daten ═══════════════════════════════════════════════ --}}
|
|
|
|
|
@if ($tab === 'profile')
|
|
|
|
|
<form wire:submit="saveProfile" class="space-y-5 animate-rise [animation-delay:60ms]">
|
|
|
|
|
<x-ui.panel :title="__('settings.company_title')" :subtitle="__('settings.company_sub')">
|
|
|
|
|
<form wire:submit="saveProfile" class="space-y-8 animate-rise [animation-delay:60ms]">
|
|
|
|
|
<section>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.company_title') }}</h2>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.company_sub') }}</p>
|
|
|
|
|
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
<x-ui.row :label="__('settings.company_name')" for="companyName">
|
|
|
|
|
<x-ui.input name="companyName" wire:model="companyName" />
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@ -67,15 +110,16 @@
|
|
|
|
|
<x-ui.input name="vatId" wire:model="vatId" />
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
|
|
|
|
|
{{-- Field by field, not a paragraph: what landed in the textarea
|
|
|
|
|
was whatever somebody typed — a postcode on the street line,
|
|
|
|
|
a town with no postcode, no country at all — and an invoice
|
|
|
|
|
has to name its recipient precisely enough to be a
|
|
|
|
|
{{-- Field by field, not a paragraph: what landed in
|
|
|
|
|
the textarea was whatever somebody typed — a
|
|
|
|
|
postcode on the street line, a town with no
|
|
|
|
|
postcode, no country at all — and an invoice has
|
|
|
|
|
to name its recipient precisely enough to be a
|
|
|
|
|
document. --}}
|
|
|
|
|
<x-ui.row :label="__('settings.billing_address')" for="billingStreet">
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<x-ui.input name="billingStreet" wire:model="billingStreet" :placeholder="__('settings.billing_street')" />
|
|
|
|
|
<div class="grid gap-3 sm:grid-cols-[140px_minmax(0,1fr)]">
|
|
|
|
|
<div class="grid gap-3 sm:grid-cols-[130px_minmax(0,1fr)]">
|
|
|
|
|
<x-ui.input name="billingPostcode" wire:model="billingPostcode" :placeholder="__('settings.billing_postcode')" />
|
|
|
|
|
<x-ui.input name="billingCity" wire:model="billingCity" :placeholder="__('settings.billing_city')" />
|
|
|
|
|
</div>
|
|
|
|
|
@ -83,17 +127,32 @@
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
|
|
|
|
|
{{-- A FACT for a record that has one. It decides the fourteen-day
|
|
|
|
|
right of withdrawal: a business able to set itself to
|
|
|
|
|
"Privatperson" here is a business able to withdraw from a
|
|
|
|
|
contract it may not withdraw from. Registration asks; this
|
|
|
|
|
page reports. The lock is in Settings::saveProfile, not here
|
|
|
|
|
— a form that only hides a control has never stopped anybody
|
|
|
|
|
who can post to /livewire/update. --}}
|
|
|
|
|
<x-ui.row :label="__('settings.customer_type')"
|
|
|
|
|
:hint="$customer?->customer_type ? __('settings.customer_type_locked') : __('settings.customer_type_once')">
|
|
|
|
|
<x-slot:footer>
|
|
|
|
|
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveProfile">
|
|
|
|
|
{{ __('settings.save') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</x-slot:footer>
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{{-- A FACT for a record that has one. It decides the
|
|
|
|
|
fourteen-day right of withdrawal: a business able to set
|
|
|
|
|
itself to "Privatperson" here is a business able to
|
|
|
|
|
withdraw from a contract it may not withdraw from.
|
|
|
|
|
Registration asks; this page reports. The lock is in
|
|
|
|
|
Settings::saveProfile, not here — a form that only hides
|
|
|
|
|
a control has never stopped anybody who can post to
|
|
|
|
|
/livewire/update. --}}
|
|
|
|
|
<section>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.customer_type') }}</h2>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">
|
|
|
|
|
{{ $customer?->customer_type ? __('settings.customer_type_locked') : __('settings.customer_type_once') }}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
<div class="px-6 py-4">
|
|
|
|
|
@if ($customer?->customer_type)
|
|
|
|
|
<p class="flex items-center gap-2 py-2 text-sm font-semibold text-ink">
|
|
|
|
|
<p class="flex items-center gap-2 text-sm font-semibold text-ink">
|
|
|
|
|
<x-ui.icon name="lock" class="size-4 text-muted" />
|
|
|
|
|
{{ __('settings.customer_type_'.$customer->customer_type) }}
|
|
|
|
|
</p>
|
|
|
|
|
@ -107,32 +166,31 @@
|
|
|
|
|
</label>
|
|
|
|
|
@endforeach
|
|
|
|
|
</div>
|
|
|
|
|
@error('customerType')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
|
|
|
|
|
@error('customerType')<p class="mt-2 text-xs text-danger">{{ $message }}</p>@enderror
|
|
|
|
|
@endif
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
|
|
|
|
|
<x-slot:footer>
|
|
|
|
|
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveProfile">
|
|
|
|
|
{{ __('settings.save') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</x-slot:footer>
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
</form>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- ══ Sicherheit ═══════════════════════════════════════════════════════ --}}
|
|
|
|
|
{{-- ══ Sicherheit ═══════════════════════════════════════════════ --}}
|
|
|
|
|
@if ($tab === 'security')
|
|
|
|
|
<div class="space-y-5 animate-rise [animation-delay:60ms]">
|
|
|
|
|
<div class="space-y-8 animate-rise [animation-delay:60ms]">
|
|
|
|
|
|
|
|
|
|
{{-- Own password. There was no way to change one at all —
|
|
|
|
|
Fortify's updatePasswords feature was switched off. --}}
|
|
|
|
|
<section>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('admin_settings.password_title') }}</h2>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('admin_settings.password_sub') }}</p>
|
|
|
|
|
|
|
|
|
|
{{-- Own password. There was no way to change one at all — Fortify's
|
|
|
|
|
updatePasswords feature was switched off. --}}
|
|
|
|
|
<form wire:submit="updateOwnPassword">
|
|
|
|
|
<x-ui.panel :title="__('admin_settings.password_title')" :subtitle="__('admin_settings.password_sub')">
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
<x-ui.row :label="__('admin_settings.password_current')" for="currentPassword">
|
|
|
|
|
<x-ui.input name="currentPassword" type="password" autocomplete="current-password" wire:model="currentPassword" />
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
<x-ui.row :label="__('admin_settings.password_new')" for="newPassword">
|
|
|
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<x-ui.input name="newPassword" type="password" autocomplete="new-password" wire:model="newPassword" />
|
|
|
|
|
<x-ui.input name="newPasswordConfirmation" type="password" autocomplete="new-password"
|
|
|
|
|
:placeholder="__('admin_settings.password_repeat')" wire:model="newPasswordConfirmation" />
|
|
|
|
|
@ -146,33 +204,31 @@
|
|
|
|
|
</x-slot:footer>
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{{-- Two-factor. Every action is re-checked server-side against a
|
|
|
|
|
recent password confirmation — hiding the buttons stops nobody
|
|
|
|
|
who can post to /livewire/update. --}}
|
|
|
|
|
<x-ui.panel>
|
|
|
|
|
<x-slot:header>
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
{{-- Two-factor. Every action is re-checked server-side
|
|
|
|
|
against a recent password confirmation — hiding the
|
|
|
|
|
buttons stops nobody who can post to /livewire/update. --}}
|
|
|
|
|
<section>
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
<h2 class="text-sm font-bold text-ink">{{ __('settings.twofa_title') }}</h2>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.twofa_title') }}</h2>
|
|
|
|
|
<span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium
|
|
|
|
|
{{ $twoFactorOn ? 'border-success-border bg-success-bg text-success' : 'border-line-strong bg-surface-2 text-muted' }}">
|
|
|
|
|
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
|
|
|
|
|
{{ $twoFactorOn ? __('settings.twofa_state_on') : __('settings.twofa_state_off') }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="mt-0.5 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('settings.twofa_sub') }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</x-slot:header>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.twofa_sub') }}</p>
|
|
|
|
|
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
@if (! $passwordConfirmed)
|
|
|
|
|
{{-- The gate. A signed-in session is not enough to change how
|
|
|
|
|
the account is protected — the risk is an unlocked
|
|
|
|
|
machine. --}}
|
|
|
|
|
{{-- The gate. A signed-in session is not enough
|
|
|
|
|
to change how the account is protected — the
|
|
|
|
|
risk is an unlocked machine. --}}
|
|
|
|
|
<form wire:submit="confirmPassword">
|
|
|
|
|
<x-ui.row :label="__('admin_settings.password_current')" :hint="__('settings.twofa_confirm_first')" for="confirmablePassword">
|
|
|
|
|
<div class="flex flex-wrap items-start gap-2">
|
|
|
|
|
<div class="min-w-56 flex-1">
|
|
|
|
|
<div class="min-w-48 flex-1">
|
|
|
|
|
<x-ui.input name="confirmablePassword" type="password" autocomplete="current-password" wire:model="confirmablePassword" />
|
|
|
|
|
</div>
|
|
|
|
|
<x-ui.button type="submit" variant="secondary" wire:loading.attr="disabled" wire:target="confirmPassword">
|
|
|
|
|
@ -182,7 +238,7 @@
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
</form>
|
|
|
|
|
@elseif ($twoFactorOn)
|
|
|
|
|
<x-ui.row :label="__('settings.twofa_state_on')" :hint="__('settings.twofa_codes_hint')">
|
|
|
|
|
<x-ui.row :label="__('settings.twofa_new_codes')" :hint="__('settings.twofa_codes_hint')">
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
<x-ui.button variant="secondary" wire:click="regenerateRecoveryCodes" wire:loading.attr="disabled" wire:target="regenerateRecoveryCodes">
|
|
|
|
|
{{ __('settings.twofa_new_codes') }}
|
|
|
|
|
@ -198,7 +254,7 @@
|
|
|
|
|
<div class="flex flex-wrap items-start gap-5">
|
|
|
|
|
<div class="rounded border border-line bg-white p-3">{!! $twoFactorQr !!}</div>
|
|
|
|
|
<form wire:submit="confirmTwoFactor" class="space-y-3">
|
|
|
|
|
<div class="max-w-48">
|
|
|
|
|
<div class="max-w-44">
|
|
|
|
|
<x-ui.input name="twoFactorCode" inputmode="numeric" autocomplete="one-time-code"
|
|
|
|
|
:label="__('settings.twofa_code')" wire:model="twoFactorCode" />
|
|
|
|
|
</div>
|
|
|
|
|
@ -209,7 +265,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@else
|
|
|
|
|
<x-ui.row :label="__('settings.twofa_state_off')">
|
|
|
|
|
<x-ui.row :label="__('settings.twofa_enable')" :hint="__('settings.twofa_sub')">
|
|
|
|
|
<x-ui.button variant="primary" wire:click="enableTwoFactor" wire:loading.attr="disabled" wire:target="enableTwoFactor">
|
|
|
|
|
{{ __('settings.twofa_enable') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
@ -217,27 +273,34 @@
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
@if ($recoveryCodes)
|
|
|
|
|
{{-- Shown once, on purpose: nobody writes down what they were
|
|
|
|
|
never shown, and these are the way back in when the phone
|
|
|
|
|
is gone. --}}
|
|
|
|
|
{{-- Shown once, on purpose: nobody writes down
|
|
|
|
|
what they were never shown, and these are the
|
|
|
|
|
way back in when the phone is gone. --}}
|
|
|
|
|
<x-ui.row :label="__('settings.twofa_codes_title')" :hint="__('settings.twofa_codes_hint')">
|
|
|
|
|
<ul class="grid grid-cols-2 gap-x-6 gap-y-1 rounded border border-warning-border bg-warning-bg p-4 font-mono text-sm text-ink sm:grid-cols-3">
|
|
|
|
|
<ul class="grid grid-cols-2 gap-x-6 gap-y-1 rounded border border-warning-border bg-warning-bg p-4 font-mono text-sm text-ink">
|
|
|
|
|
@foreach ($recoveryCodes as $code)<li class="select-all">{{ $code }}</li>@endforeach
|
|
|
|
|
</ul>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@endif
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{{-- The devices. It answers the same question two-factor does — who
|
|
|
|
|
can get into this account — and it is a table of its own. --}}
|
|
|
|
|
{{-- The devices. It answers the same question two-factor does
|
|
|
|
|
— who can get into this account. --}}
|
|
|
|
|
<section>
|
|
|
|
|
@livewire('sessions')
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- ══ Erscheinungsbild ═════════════════════════════════════════════════ --}}
|
|
|
|
|
{{-- ══ Erscheinungsbild ═════════════════════════════════════════ --}}
|
|
|
|
|
@if ($tab === 'branding')
|
|
|
|
|
<form wire:submit="saveBranding" class="space-y-5 animate-rise [animation-delay:60ms]">
|
|
|
|
|
<x-ui.panel :title="__('settings.branding_title')" :subtitle="__('settings.branding_sub')">
|
|
|
|
|
<form wire:submit="saveBranding" class="animate-rise [animation-delay:60ms]">
|
|
|
|
|
<section>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.branding_title') }}</h2>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.branding_sub') }}</p>
|
|
|
|
|
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
<x-ui.row :label="__('settings.brand_display_name')" for="brandDisplayName">
|
|
|
|
|
<x-ui.input name="brandDisplayName" wire:model="brandDisplayName" :placeholder="$branding['display_name'] ?? ''" />
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@ -264,13 +327,13 @@
|
|
|
|
|
|
|
|
|
|
<x-ui.row :label="__('settings.brand_logo')" :hint="__('settings.brand_logo_hint')">
|
|
|
|
|
<div class="flex flex-wrap items-center gap-4">
|
|
|
|
|
<div class="grid size-20 shrink-0 place-items-center overflow-hidden rounded border border-line bg-surface-2">
|
|
|
|
|
<div class="grid size-16 shrink-0 place-items-center overflow-hidden rounded border border-line bg-surface-2">
|
|
|
|
|
@if ($logo)
|
|
|
|
|
<img src="{{ $logo->temporaryUrl() }}" alt="" class="max-h-20 max-w-20 object-contain" />
|
|
|
|
|
<img src="{{ $logo->temporaryUrl() }}" alt="" class="max-h-16 max-w-16 object-contain" />
|
|
|
|
|
@elseif ($logoUrl)
|
|
|
|
|
<img src="{{ $logoUrl }}" alt="" class="max-h-20 max-w-20 object-contain" />
|
|
|
|
|
<img src="{{ $logoUrl }}" alt="" class="max-h-16 max-w-16 object-contain" />
|
|
|
|
|
@else
|
|
|
|
|
<span class="px-2 text-center text-xs text-muted">{{ __('settings.brand_default') }}</span>
|
|
|
|
|
<span class="px-2 text-center text-[10px] leading-tight text-muted">{{ __('settings.brand_default') }}</span>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
<div class="min-w-0 flex-1 space-y-2">
|
|
|
|
|
@ -293,16 +356,19 @@
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</x-slot:footer>
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
</form>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- ══ Vertrag ══════════════════════════════════════════════════════════ --}}
|
|
|
|
|
{{-- ══ Vertrag ══════════════════════════════════════════════════ --}}
|
|
|
|
|
@if ($tab === 'contract')
|
|
|
|
|
<div class="space-y-5 animate-rise [animation-delay:60ms]">
|
|
|
|
|
<div class="space-y-8 animate-rise [animation-delay:60ms]">
|
|
|
|
|
|
|
|
|
|
<x-ui.panel :title="__('settings.contract_title')" :subtitle="__('settings.contract_sub')">
|
|
|
|
|
<section>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.package_title') }}</h2>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.contract_sub') }}</p>
|
|
|
|
|
|
|
|
|
|
{{-- The package, and the way out of it. --}}
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
<x-ui.row :label="__('settings.package_title')">
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
|
|
<p class="min-w-0 text-sm text-body">
|
|
|
|
|
@ -328,26 +394,27 @@
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
|
|
|
|
|
{{-- ── Das Widerrufsrecht ────────────────────────────────────
|
|
|
|
|
Shown to every CONSUMER with a contract, open or not — not
|
|
|
|
|
only while it is open. It vanished entirely once the
|
|
|
|
|
fourteen days ran out, which is indistinguishable from a
|
|
|
|
|
feature that was never built: the owner went looking for it
|
|
|
|
|
and reported it missing from an account that has no contract
|
|
|
|
|
at all, so there was nothing for it to be about.
|
|
|
|
|
{{-- ── Das Widerrufsrecht ────────────────────────
|
|
|
|
|
Shown to every CONSUMER with a contract, open or
|
|
|
|
|
not. It used to render only while the window was
|
|
|
|
|
open, so once the fourteen days expired the block
|
|
|
|
|
vanished — indistinguishable from a feature
|
|
|
|
|
nobody built.
|
|
|
|
|
|
|
|
|
|
A business never sees it, and the server refuses them again
|
|
|
|
|
in Settings::withdraw() — a card that is not rendered has
|
|
|
|
|
never stopped anybody who can post to /livewire/update.
|
|
|
|
|
A business never sees it, and Settings::withdraw()
|
|
|
|
|
refuses them again on the server: a card that is
|
|
|
|
|
not rendered has never stopped anybody who can
|
|
|
|
|
post to /livewire/update.
|
|
|
|
|
|
|
|
|
|
Not a variant of the cancellation above: that one ends a
|
|
|
|
|
contract validly concluded and keeps the term already paid
|
|
|
|
|
for; this one unwinds the contract, ends the service the same
|
|
|
|
|
day and sends the whole amount back. --}}
|
|
|
|
|
Not a variant of the cancellation above it: that
|
|
|
|
|
one ends a contract validly concluded and keeps
|
|
|
|
|
the term already paid for; this one unwinds the
|
|
|
|
|
contract, ends the service the same day and sends
|
|
|
|
|
the whole amount back. --}}
|
|
|
|
|
@if ($withdrawal->applies)
|
|
|
|
|
<x-ui.row :label="__('withdrawal.card_title')">
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
|
|
<p class="min-w-0 max-w-[70ch] text-sm text-body">
|
|
|
|
|
<p class="min-w-0 max-w-[60ch] text-sm text-body">
|
|
|
|
|
@if ($withdrawal->open)
|
|
|
|
|
{{ __('withdrawal.card_sub', [
|
|
|
|
|
'date' => $withdrawal->endsAt->local()->isoFormat('LL'),
|
|
|
|
|
@ -366,80 +433,90 @@
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@endif
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{{-- ── AV-Vertrag & TOM ──────────────────────────────────────
|
|
|
|
|
{{-- ── AV-Vertrag & TOM ──────────────────────────────────
|
|
|
|
|
Art. 28 DSGVO wants a contract wherever personal data is
|
|
|
|
|
processed on somebody else's behalf. It is concluded WITH the
|
|
|
|
|
terms at checkout (see the AGB), so this is not a second
|
|
|
|
|
signing ceremony: the document has to be available, current
|
|
|
|
|
and retrievable. The extra confirmation is offered because a
|
|
|
|
|
practice or a firm that gets audited often wants one on file.
|
|
|
|
|
processed on somebody else's behalf. It is concluded WITH
|
|
|
|
|
the terms at checkout (see the AGB), so this is not a
|
|
|
|
|
second signing ceremony: the document has to be
|
|
|
|
|
available, current and retrievable. The extra
|
|
|
|
|
confirmation is offered because a practice or a firm that
|
|
|
|
|
gets audited often wants one on file.
|
|
|
|
|
|
|
|
|
|
Nothing renders until an operator has published a version. --}}
|
|
|
|
|
Nothing renders until an operator has published a
|
|
|
|
|
version. --}}
|
|
|
|
|
@if ($dpa !== null)
|
|
|
|
|
<x-ui.row :label="__('dpa.title')" :hint="__('dpa.sub')">
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
<section>
|
|
|
|
|
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('dpa.title') }}</h2>
|
|
|
|
|
<span class="font-mono text-xs text-muted">{{ __('dpa.version', ['version' => $dpa->version]) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('dpa.sub') }}</p>
|
|
|
|
|
|
|
|
|
|
<x-ui.panel class="mt-4">
|
|
|
|
|
<x-ui.row :label="__('dpa.read_agreement')">
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
<x-ui.button :href="route('dpa.file', 'agreement')" variant="secondary" size="sm" target="_blank">
|
|
|
|
|
<x-ui.icon name="file-text" class="size-4" />{{ __('dpa.read_agreement') }}
|
|
|
|
|
<x-ui.icon name="file-text" class="size-4" />{{ __('dpa.open') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
<x-ui.button :href="route('dpa.file', ['which' => 'agreement', 'download' => 1])" variant="ghost" size="sm" download>
|
|
|
|
|
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
|
|
|
|
|
@if ($dpa->measures_path)
|
|
|
|
|
<span class="mx-1 h-4 w-px bg-line" aria-hidden="true"></span>
|
|
|
|
|
<x-ui.row :label="__('dpa.read_measures')">
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
<x-ui.button :href="route('dpa.file', 'measures')" variant="secondary" size="sm" target="_blank">
|
|
|
|
|
<x-ui.icon name="shield" class="size-4" />{{ __('dpa.read_measures') }}
|
|
|
|
|
<x-ui.icon name="shield" class="size-4" />{{ __('dpa.open') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
<x-ui.button :href="route('dpa.file', ['which' => 'measures', 'download' => 1])" variant="ghost" size="sm" download>
|
|
|
|
|
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
@endif
|
|
|
|
|
<span class="ml-auto font-mono text-xs text-muted">{{ __('dpa.version', ['version' => $dpa->version]) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
<x-ui.row :label="__('dpa.state_label')">
|
|
|
|
|
@if ($dpaAcceptance)
|
|
|
|
|
{{-- R19: stored in UTC, read on the wall clock. --}}
|
|
|
|
|
<p class="flex items-start gap-2 text-xs leading-relaxed text-muted">
|
|
|
|
|
<x-ui.icon name="check" class="mt-0.5 size-3.5 shrink-0 text-success" />
|
|
|
|
|
<p class="flex items-start gap-2 text-sm leading-relaxed text-body">
|
|
|
|
|
<x-ui.icon name="check" class="mt-0.5 size-4 shrink-0 text-success" />
|
|
|
|
|
{{ __('dpa.accepted_on', [
|
|
|
|
|
'version' => $dpa->version,
|
|
|
|
|
'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'),
|
|
|
|
|
]) }}
|
|
|
|
|
</p>
|
|
|
|
|
@else
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3 rounded border border-line bg-surface-2 px-4 py-3">
|
|
|
|
|
<p class="min-w-0 max-w-[70ch] text-xs leading-relaxed text-muted">{{ __('dpa.in_force_note') }}</p>
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<p class="max-w-[60ch] text-sm leading-relaxed text-body">{{ __('dpa.in_force_note') }}</p>
|
|
|
|
|
<x-ui.button variant="secondary" size="sm"
|
|
|
|
|
wire:click="acceptProcessingAgreement" wire:loading.attr="disabled" wire:target="acceptProcessingAgreement">
|
|
|
|
|
{{ __('dpa.accept_cta') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- Closing the account. Last row of the panel, where the
|
|
|
|
|
irreversible things belong. --}}
|
|
|
|
|
<x-ui.row :label="__('settings.close_account_title')" :hint="__('settings.close_account_sub')">
|
|
|
|
|
<div class="flex justify-start sm:justify-end">
|
|
|
|
|
<x-ui.button variant="secondary" size="sm"
|
|
|
|
|
x-on:click="$dispatch('openModal', { component: 'confirm-close-account' })">
|
|
|
|
|
{{ __('settings.close_cta') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
@endif
|
|
|
|
|
|
|
|
|
|
{{-- The deadlines, said where somebody asks the question. "Nach fünf
|
|
|
|
|
Tagen gelöscht" on the verification page reads as if it applied
|
|
|
|
|
to every account; it applies to a registration nobody confirmed.
|
|
|
|
|
The other rule — a year without a package — was nowhere at all.
|
|
|
|
|
Both come from the commands that enforce them, so the page cannot
|
|
|
|
|
drift from what actually happens. --}}
|
|
|
|
|
<x-ui.panel :title="__('settings.lifecycle_title')">
|
|
|
|
|
<div class="px-6 py-4">
|
|
|
|
|
{{-- ── What ends an account ──────────────────────────────
|
|
|
|
|
The deadlines and the button that closes it, together and
|
|
|
|
|
last, in a card whose border says what kind of thing this
|
|
|
|
|
is. Everything irreversible on this page lives here.
|
|
|
|
|
|
|
|
|
|
The deadlines come from the commands that enforce them,
|
|
|
|
|
so the page cannot drift from what actually happens. --}}
|
|
|
|
|
<section>
|
|
|
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.close_account_title') }}</h2>
|
|
|
|
|
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.close_account_sub') }}</p>
|
|
|
|
|
|
|
|
|
|
<x-ui.panel class="mt-4 border-danger-border">
|
|
|
|
|
<x-ui.row :label="__('settings.lifecycle_title')">
|
|
|
|
|
<ul class="space-y-1.5 text-sm leading-relaxed text-muted">
|
|
|
|
|
<li>{{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}</li>
|
|
|
|
|
<li>{{ __('settings.lifecycle_dormant', ['warn' => App\Console\Commands\PruneDormantAccounts::WARN_DAYS_BEFORE]) }}</li>
|
|
|
|
|
@ -449,8 +526,20 @@
|
|
|
|
|
class="mt-3 inline-block text-sm font-medium text-accent-text underline-offset-4 hover:underline">
|
|
|
|
|
{{ __('settings.lifecycle_terms') }}
|
|
|
|
|
</a>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
|
|
|
|
|
<x-ui.row :label="__('settings.close_cta')" :hint="__('settings.close_account_sub')">
|
|
|
|
|
<div class="flex justify-start sm:justify-end">
|
|
|
|
|
<x-ui.button variant="danger" size="sm"
|
|
|
|
|
x-on:click="$dispatch('openModal', { component: 'confirm-close-account' })">
|
|
|
|
|
{{ __('settings.close_cta') }}
|
|
|
|
|
</x-ui.button>
|
|
|
|
|
</div>
|
|
|
|
|
</x-ui.row>
|
|
|
|
|
</x-ui.panel>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
@endif
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|