diff --git a/lang/de/dpa.php b/lang/de/dpa.php index 6f79b85..6d69a3d 100644 --- a/lang/de/dpa.php +++ b/lang/de/dpa.php @@ -15,4 +15,6 @@ return [ 'in_force_note' => 'Dieser Vertrag ist mit Ihrer Bestellung Bestandteil unserer Vereinbarung; eine gesonderte Unterschrift ist nicht nötig. Wenn Sie für Ihre Unterlagen eine ausdrückliche Bestätigung möchten, halten wir Fassung, Zeitpunkt und IP-Adresse fest.', 'download_agreement' => 'AV-Vertrag laden', 'download_measures' => 'TOM laden', + 'open' => 'Öffnen', + 'state_label' => 'Status', ]; diff --git a/lang/de/settings.php b/lang/de/settings.php index 098d932..c1136a4 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -11,6 +11,7 @@ return [ 'branding' => 'Erscheinungsbild', 'contract' => 'Vertrag', ], + 'signed_in_as' => 'Angemeldet als', 'title' => 'Einstellungen', 'subtitle' => 'Alles, was zu Ihrem Konto gehört — nach Themen getrennt.', 'save' => 'Speichern', diff --git a/lang/en/dpa.php b/lang/en/dpa.php index 4c0b3a5..1a95a8f 100644 --- a/lang/en/dpa.php +++ b/lang/en/dpa.php @@ -15,4 +15,6 @@ return [ 'in_force_note' => 'This agreement became part of our contract with your order; a separate signature is not required. If you would like an explicit confirmation for your records, we note the version, the moment and the IP address.', 'download_agreement' => 'Download agreement', 'download_measures' => 'Download measures', + 'open' => 'Open', + 'state_label' => 'State', ]; diff --git a/lang/en/settings.php b/lang/en/settings.php index e72e94f..25f2292 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -11,6 +11,7 @@ return [ 'branding' => 'Appearance', 'contract' => 'Contract', ], + 'signed_in_as' => 'Signed in as', 'title' => 'Settings', 'subtitle' => 'Everything that belongs to your account, grouped by subject.', 'save' => 'Save', diff --git a/resources/views/livewire/settings.blade.php b/resources/views/livewire/settings.blade.php index 2298e20..32fcf95 100644 --- a/resources/views/livewire/settings.blade.php +++ b/resources/views/livewire/settings.blade.php @@ -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 +
@@ -32,425 +51,495 @@

{{ __('settings.subtitle') }}

- {{-- 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. --}} -
- @foreach ($tabs as $name) - - @endforeach -
+
- {{-- ══ Ihre Daten ═══════════════════════════════════════════════════════ --}} - @if ($tab === 'profile') -
- - - - - - - - - - - - - + {{-- ── 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. --}} + - {{-- 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. --}} - - @if ($customer?->customer_type) -

- - {{ __('settings.customer_type_'.$customer->customer_type) }} + {{-- ── The content column ─────────────────────────────────────────── + 820px, not the whole shell. Nobody types a postcode into a field a + thousand pixels wide. --}} +

+ + {{-- ══ Ihre Daten ═══════════════════════════════════════════════ --}} + @if ($tab === 'profile') + +
+

{{ __('settings.company_title') }}

+

{{ __('settings.company_sub') }}

+ + + + + + + + + + + + + + + + {{-- 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. --}} + +
+ +
+ + +
+ +
+
+ + + + {{ __('settings.save') }} + + +
+
+ + {{-- 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. --}} +
+

{{ __('settings.customer_type') }}

+

+ {{ $customer?->customer_type ? __('settings.customer_type_locked') : __('settings.customer_type_once') }}

- @else -
- @foreach ($customerTypes as $type) - - @endforeach -
- @error('customerType')

{{ $message }}

@enderror - @endif - - - - {{ __('settings.save') }} - - - - - @endif + +
+ @if ($customer?->customer_type) +

+ + {{ __('settings.customer_type_'.$customer->customer_type) }} +

+ @else +
+ @foreach ($customerTypes as $type) + + @endforeach +
+ @error('customerType')

{{ $message }}

@enderror + @endif +
+
+
+ + @endif - {{-- ══ Sicherheit ═══════════════════════════════════════════════════════ --}} - @if ($tab === 'security') -
+ {{-- ══ Sicherheit ═══════════════════════════════════════════════ --}} + @if ($tab === 'security') +
- {{-- Own password. There was no way to change one at all — Fortify's - updatePasswords feature was switched off. --}} -
- - - - - -
- - -
-
+ {{-- Own password. There was no way to change one at all — + Fortify's updatePasswords feature was switched off. --}} +
+

{{ __('admin_settings.password_title') }}

+

{{ __('admin_settings.password_sub') }}

- - - {{ __('admin_settings.password_save') }} - - - - +
+ + + + + +
+ + +
+
- {{-- 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. --}} - - -
+ + + {{ __('admin_settings.password_save') }} + + + + +
+ + {{-- 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. --}} +
-

{{ __('settings.twofa_title') }}

+

{{ __('settings.twofa_title') }}

{{ $twoFactorOn ? __('settings.twofa_state_on') : __('settings.twofa_state_off') }}
-

{{ __('settings.twofa_sub') }}

-
- +

{{ __('settings.twofa_sub') }}

- @if (! $passwordConfirmed) - {{-- The gate. A signed-in session is not enough to change how - the account is protected — the risk is an unlocked - machine. --}} -
- -
-
- -
- - {{ __('settings.twofa_confirm_button') }} - -
-
-
- @elseif ($twoFactorOn) - -
- - {{ __('settings.twofa_new_codes') }} - - - {{ __('settings.twofa_disable') }} - -
-
- @elseif ($twoFactorPending) - -
-
{!! $twoFactorQr !!}
-
-
- -
- - {{ __('settings.twofa_activate') }} - -
-
-
- @else - - - {{ __('settings.twofa_enable') }} - - - @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. --}} - -
    - @foreach ($recoveryCodes as $code)
  • {{ $code }}
  • @endforeach -
-
- @endif - - - {{-- The devices. It answers the same question two-factor does — who - can get into this account — and it is a table of its own. --}} - @livewire('sessions') -
- @endif - - {{-- ══ Erscheinungsbild ═════════════════════════════════════════════════ --}} - @if ($tab === 'branding') -
- - - - - - -
- - -
- @error('brandPrimary')

{{ $message }}

@enderror -
- - -
- - -
- @error('brandAccent')

{{ $message }}

@enderror -
- - -
-
- @if ($logo) - - @elseif ($logoUrl) - + + @if (! $passwordConfirmed) + {{-- The gate. A signed-in session is not enough + to change how the account is protected — the + risk is an unlocked machine. --}} + + +
+
+ +
+ + {{ __('settings.twofa_confirm_button') }} + +
+
+ + @elseif ($twoFactorOn) + +
+ + {{ __('settings.twofa_new_codes') }} + + + {{ __('settings.twofa_disable') }} + +
+
+ @elseif ($twoFactorPending) + +
+
{!! $twoFactorQr !!}
+
+
+ +
+ + {{ __('settings.twofa_activate') }} + +
+
+
@else - {{ __('settings.brand_default') }} - @endif -
-
- - @if ($logoUrl) - - @endif - @error('logo')

{{ $message }}

@enderror -
-
-
- - - @if ($branding['is_default'] ?? false) -

{{ __('settings.brand_using_default') }}

- @endif - - {{ __('settings.save') }} - -
-
- - @endif - - {{-- ══ Vertrag ══════════════════════════════════════════════════════════ --}} - @if ($tab === 'contract') -
- - - - {{-- The package, and the way out of it. --}} - -
-

- @if ($cancellationScheduled) - {{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }} - @elseif ($hasActivePackage) - {{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }} - @else - {{ __('settings.no_package') }} - @endif -

- - @if ($hasActivePackage && ! $cancellationScheduled) - - {{ __('settings.cancel_cta') }} - - @elseif (! $hasActivePackage && ! $cancellationScheduled) - - {{ __('settings.to_packages') }} - - @endif -
-
- - {{-- ── 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. - - 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. - - 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. --}} - @if ($withdrawal->applies) - -
-

- @if ($withdrawal->open) - {{ __('withdrawal.card_sub', [ - 'date' => $withdrawal->endsAt->local()->isoFormat('LL'), - 'days' => $withdrawal->daysLeft(), - ]) }} - @else - {{ $withdrawal->refusal }} - @endif -

- @if ($withdrawal->open) - - {{ __('withdrawal.cta') }} - - @endif -
-
- @endif - - {{-- ── 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. - - Nothing renders until an operator has published a version. --}} - @if ($dpa !== null) - -
-
- - {{ __('dpa.read_agreement') }} - - - {{ __('dpa.download') }} - - @if ($dpa->measures_path) - - - {{ __('dpa.read_measures') }} + + + {{ __('settings.twofa_enable') }} - - {{ __('dpa.download') }} - - @endif - {{ __('dpa.version', ['version' => $dpa->version]) }} -
- - @if ($dpaAcceptance) - {{-- R19: stored in UTC, read on the wall clock. --}} -

- - {{ __('dpa.accepted_on', [ - 'version' => $dpa->version, - 'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'), - ]) }} -

- @else -
-

{{ __('dpa.in_force_note') }}

- - {{ __('dpa.accept_cta') }} - -
+ @endif -
-
- @endif - {{-- Closing the account. Last row of the panel, where the - irreversible things belong. --}} - -
- - {{ __('settings.close_cta') }} - -
-
-
+ @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. --}} + +
    + @foreach ($recoveryCodes as $code)
  • {{ $code }}
  • @endforeach +
+
+ @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. --}} - -
-
    -
  • {{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}
  • -
  • {{ __('settings.lifecycle_dormant', ['warn' => App\Console\Commands\PruneDormantAccounts::WARN_DAYS_BEFORE]) }}
  • -
  • {{ __('settings.lifecycle_kept') }}
  • -
- - {{ __('settings.lifecycle_terms') }} - + {{-- The devices. It answers the same question two-factor does + — who can get into this account. --}} +
+ @livewire('sessions') +
-
+ @endif + + {{-- ══ Erscheinungsbild ═════════════════════════════════════════ --}} + @if ($tab === 'branding') +
+
+

{{ __('settings.branding_title') }}

+

{{ __('settings.branding_sub') }}

+ + + + + + + +
+ + +
+ @error('brandPrimary')

{{ $message }}

@enderror +
+ + +
+ + +
+ @error('brandAccent')

{{ $message }}

@enderror +
+ + +
+
+ @if ($logo) + + @elseif ($logoUrl) + + @else + {{ __('settings.brand_default') }} + @endif +
+
+ + @if ($logoUrl) + + @endif + @error('logo')

{{ $message }}

@enderror +
+
+
+ + + @if ($branding['is_default'] ?? false) +

{{ __('settings.brand_using_default') }}

+ @endif + + {{ __('settings.save') }} + +
+
+
+
+ @endif + + {{-- ══ Vertrag ══════════════════════════════════════════════════ --}} + @if ($tab === 'contract') +
+ +
+

{{ __('settings.package_title') }}

+

{{ __('settings.contract_sub') }}

+ + + +
+

+ @if ($cancellationScheduled) + {{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }} + @elseif ($hasActivePackage) + {{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }} + @else + {{ __('settings.no_package') }} + @endif +

+ + @if ($hasActivePackage && ! $cancellationScheduled) + + {{ __('settings.cancel_cta') }} + + @elseif (! $hasActivePackage && ! $cancellationScheduled) + + {{ __('settings.to_packages') }} + + @endif +
+
+ + {{-- ── 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 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 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) + +
+

+ @if ($withdrawal->open) + {{ __('withdrawal.card_sub', [ + 'date' => $withdrawal->endsAt->local()->isoFormat('LL'), + 'days' => $withdrawal->daysLeft(), + ]) }} + @else + {{ $withdrawal->refusal }} + @endif +

+ @if ($withdrawal->open) + + {{ __('withdrawal.cta') }} + + @endif +
+
+ @endif +
+
+ + {{-- ── 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. + + Nothing renders until an operator has published a + version. --}} + @if ($dpa !== null) +
+
+

{{ __('dpa.title') }}

+ {{ __('dpa.version', ['version' => $dpa->version]) }} +
+

{{ __('dpa.sub') }}

+ + + +
+ + {{ __('dpa.open') }} + + + {{ __('dpa.download') }} + +
+
+ + @if ($dpa->measures_path) + +
+ + {{ __('dpa.open') }} + + + {{ __('dpa.download') }} + +
+
+ @endif + + + @if ($dpaAcceptance) + {{-- R19: stored in UTC, read on the wall clock. --}} +

+ + {{ __('dpa.accepted_on', [ + 'version' => $dpa->version, + 'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'), + ]) }} +

+ @else +
+

{{ __('dpa.in_force_note') }}

+ + {{ __('dpa.accept_cta') }} + +
+ @endif +
+
+
+ @endif + + {{-- ── 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. --}} +
+

{{ __('settings.close_account_title') }}

+

{{ __('settings.close_account_sub') }}

+ + + +
    +
  • {{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}
  • +
  • {{ __('settings.lifecycle_dormant', ['warn' => App\Console\Commands\PruneDormantAccounts::WARN_DAYS_BEFORE]) }}
  • +
  • {{ __('settings.lifecycle_kept') }}
  • +
+ + {{ __('settings.lifecycle_terms') }} + +
+ + +
+ + {{ __('settings.close_cta') }} + +
+
+
+
+
+ @endif
- @endif +