257 lines
16 KiB
PHP
257 lines
16 KiB
PHP
<div class="mx-auto max-w-[1120px] space-y-6">
|
|
<div class="animate-rise">
|
|
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('mail_settings.title') }}</h1>
|
|
<p class="mt-1 max-w-[70ch] text-sm text-muted">{{ __('mail_settings.subtitle') }}</p>
|
|
</div>
|
|
|
|
{{-- ── Die Reiter ────────────────────────────────────────────────────
|
|
Das waren vier Karten untereinander in einer schmalen Spalte, und die
|
|
letzten beiden allein zweiundzwanzig gleich aussehende Zeilen mit zwei
|
|
gleich aussehenden Speichern-Knöpfen darunter. Geteilt nach der Frage,
|
|
die man an die Seite mitbringt: WOMIT wird gesendet, WER sendet, und
|
|
WAS geht von wo raus — dieselbe Achse, nach der Admin\Integrations aus
|
|
demselben Grund umgebaut wurde.
|
|
|
|
Der offene Reiter steht in der Adresszeile, damit ein Neuladen oder
|
|
ein Lesezeichen dort landet, wo der Betreiber war. Die Bestätigung des
|
|
Passworts überlebt einen Wechsel: sie gehört zur Sitzung, nicht zum
|
|
Reiter. --}}
|
|
<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 (\App\Livewire\Admin\Mail::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="['versand' => 'server', 'postfaecher' => 'mail', 'zuordnung' => 'send'][$name] ?? 'settings'"
|
|
class="size-4" />{{ __('mail_settings.tab.'.$name) }}
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Beide Hinweise stehen ÜBER den Reitern, nicht in einem davon: sie
|
|
gelten für die ganze Seite. Der erste ist das eine, was jedes Postfach
|
|
hier zur Zierde macht — und er gehört genau dorthin, wo jemand
|
|
nachsieht, warum keine Mail ankam; der Postfach-Test darunter meldet
|
|
nämlich trotzdem Erfolg, weil MailboxTester absichtlich seinen eigenen
|
|
Weg baut. --}}
|
|
@if (! \App\Support\MailDelivery::delivers())
|
|
<x-ui.alert variant="warning">
|
|
{{ __('admin.notice.mail_not_delivering', ['mailer' => (string) (\App\Support\MailDelivery::transport() ?? 'null')]) }}
|
|
</x-ui.alert>
|
|
@endif
|
|
|
|
@if (! $usable)
|
|
<x-ui.alert variant="warning">{{ __('mail_settings.no_key') }}</x-ui.alert>
|
|
@endif
|
|
|
|
{{-- ── 1. Womit gesendet wird ───────────────────────────────────────
|
|
Ein Server, eine Karte. Der Hauptschalter steht ÜBER den Serverfeldern
|
|
und in derselben Karte: er entscheidet, ob die überhaupt benutzt
|
|
werden, und derselbe Knopf speichert beides. --}}
|
|
@if ($tab === 'versand')
|
|
<x-ui.card class="animate-rise [animation-delay:80ms]">
|
|
<h2 class="text-lg font-semibold text-ink">{{ __('mail_settings.server_title') }}</h2>
|
|
<p class="mt-1 max-w-[70ch] text-sm text-muted">{{ __('mail_settings.server_hint') }}</p>
|
|
|
|
<div class="mt-5 rounded-lg border border-line bg-surface-2 px-4 py-3.5">
|
|
<x-ui.switch name="deliver" wire:model.live="deliver"
|
|
:label="__('mail_settings.deliver')"
|
|
:hint="__('mail_settings.deliver_hint')"
|
|
:on="__('mail_settings.deliver_on')"
|
|
:off="__('mail_settings.deliver_off')" />
|
|
</div>
|
|
|
|
<div class="mt-5 grid gap-4 sm:grid-cols-3">
|
|
<x-ui.input name="host" wire:model="host" :label="__('mail_settings.host')" />
|
|
<x-ui.input name="port" type="number" wire:model="port" :label="__('mail_settings.port')" />
|
|
<div>
|
|
<label for="encryption" class="text-sm font-medium text-ink">{{ __('mail_settings.encryption') }}</label>
|
|
<select id="encryption" wire:model="encryption"
|
|
class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
|
<option value="tls">TLS</option>
|
|
<option value="ssl">SSL</option>
|
|
<option value="none">{{ __('mail_settings.encryption_none') }}</option>
|
|
</select>
|
|
@error('encryption')<p class="mt-1 text-xs text-danger">{{ $message }}</p>@enderror
|
|
</div>
|
|
</div>
|
|
|
|
@if (! $passwordConfirmed)
|
|
{{-- The second gate. $host is the platform's outbound relay for
|
|
every purpose at once — being signed in is not enough to
|
|
repoint it, the same reasoning as the credentials page. --}}
|
|
<form wire:submit="confirmPassword" class="mt-5 rounded-lg border border-line bg-surface-2 p-4">
|
|
<p class="text-sm text-body">{{ __('mail_settings.confirm_first') }}</p>
|
|
<div class="mt-3 flex flex-wrap items-start gap-2">
|
|
<div class="min-w-56 flex-1">
|
|
<x-ui.input name="confirmablePassword" type="password" autocomplete="current-password"
|
|
:label="__('admin_settings.password_current')" wire:model="confirmablePassword" />
|
|
</div>
|
|
<x-ui.button type="submit" variant="secondary" class="mt-7" wire:loading.attr="disabled" wire:target="confirmPassword">
|
|
{{ __('mail_settings.confirm_button') }}
|
|
</x-ui.button>
|
|
</div>
|
|
</form>
|
|
@else
|
|
<x-ui.button wire:click="saveServer" variant="primary" size="md" class="mt-5">
|
|
{{ __('mail_settings.save') }}
|
|
</x-ui.button>
|
|
@endif
|
|
</x-ui.card>
|
|
@endif
|
|
|
|
{{-- ── 2. Wer sendet ────────────────────────────────────────────────
|
|
Kein Eingabefeld in einem <td> — R20. Das Empfängerfeld für den
|
|
Testversand steht ÜBER der Tabelle, eines für alle Zeilen. --}}
|
|
@if ($tab === 'postfaecher')
|
|
<x-ui.card class="animate-rise [animation-delay:80ms]">
|
|
<h2 class="text-lg font-semibold text-ink">{{ __('mail_settings.boxes_title') }}</h2>
|
|
|
|
<div class="mt-4 max-w-sm">
|
|
<x-ui.input name="testRecipient" type="email" wire:model="testRecipient" :label="__('mail_settings.test_recipient')" />
|
|
<p class="mt-2 text-sm text-muted">{{ __('mail_settings.test_hint') }}</p>
|
|
</div>
|
|
|
|
@if ($testResult)
|
|
<x-ui.alert :variant="$testResult['ok'] ? 'success' : 'danger'" class="mt-4">
|
|
<span class="font-medium">{{ $testedKey }}:</span>
|
|
@if ($testResult['ok'])
|
|
{{ __('mail_settings.test_ok') }}
|
|
@else
|
|
{{ __('mail_settings.test_failed') }} {{ $testResult['error'] }}
|
|
@endif
|
|
</x-ui.alert>
|
|
@endif
|
|
|
|
<table class="mt-5 w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b border-line text-left">
|
|
<th class="lbl pb-2">{{ __('mail_settings.address') }}</th>
|
|
<th class="lbl pb-2">{{ __('mail_settings.last_verified') }}</th>
|
|
<th class="lbl pb-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach ($mailboxes as $box)
|
|
<tr wire:key="mailbox-{{ $box->uuid }}" class="border-b border-line last:border-0">
|
|
<td class="py-3">
|
|
<span class="font-medium text-ink">{{ $box->address }}</span>
|
|
<span class="ml-2 text-muted">{{ $box->key }}</span>
|
|
</td>
|
|
<td class="py-3 text-muted">
|
|
{{ $box->last_verified_at?->local()->isoFormat('DD.MM.YYYY HH:mm') ?? __('mail_settings.never_verified') }}
|
|
</td>
|
|
<td class="py-3 text-right space-x-2">
|
|
{{-- R18: icon beside its text, single line, size-4.
|
|
Two spans swapped by wire:loading, same
|
|
pattern as admin/host-create.blade.php's save
|
|
button — the disabled attribute alone left an
|
|
operator staring at a greyed-out button with
|
|
no sign anything was happening for up to the
|
|
full connect timeout. --}}
|
|
<x-ui.button size="sm" variant="ghost" wire:click="test('{{ $box->uuid }}')"
|
|
wire:loading.attr="disabled" wire:target="test('{{ $box->uuid }}')">
|
|
<x-ui.icon name="send" class="size-4" />
|
|
<span wire:loading.remove wire:target="test('{{ $box->uuid }}')">{{ __('mail_settings.test') }}</span>
|
|
<span wire:loading wire:target="test('{{ $box->uuid }}')">{{ __('mail_settings.testing') }}</span>
|
|
</x-ui.button>
|
|
<x-ui.button size="sm" variant="ghost"
|
|
x-on:click="$dispatch('openModal', { component: 'edit-mailbox', arguments: { uuid: '{{ $box->uuid }}' } })">
|
|
<x-ui.icon name="pencil" class="size-4" />{{ __('mail_settings.edit') }}
|
|
</x-ui.button>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</x-ui.card>
|
|
@endif
|
|
|
|
{{-- ── 3. Was von wo rausgeht ───────────────────────────────────────
|
|
Zwei Listen waren es: fünf Zwecke, darunter siebzehn Mailarten, beide
|
|
als „Beschriftung, Auswahlfeld" und optisch nicht auseinanderzuhalten.
|
|
Dass die zweite Liste auf die erste zurückfällt, stand nur als Satz in
|
|
der leeren Auswahl — siebzehnmal derselbe Satz.
|
|
|
|
Jetzt steht jede Mailart unter ihrem Zweck: oben das Postfach, aus dem
|
|
der Zweck verschickt, darunter eingerückt die Mailarten, die davon
|
|
abweichen dürfen. Die Regel ist damit die Form, und die leere Auswahl
|
|
nennt die Adresse, die stattdessen greift, statt nur den Zwecknamen.
|
|
|
|
Ein <select> je Zeile ist R20's ausdrückliche Ausnahme: ein Klick, ein
|
|
Wert, keine Höhenänderung. --}}
|
|
@if ($tab === 'zuordnung')
|
|
<x-ui.card class="animate-rise [animation-delay:80ms]">
|
|
<h2 class="text-lg font-semibold text-ink">{{ __('mail_settings.purposes_title') }}</h2>
|
|
<p class="mt-1 max-w-[70ch] text-sm text-muted">{{ __('mail_settings.assignments_hint') }}</p>
|
|
|
|
<div class="mt-6 space-y-5">
|
|
@foreach ($catalogueByPurpose as $purpose => $entries)
|
|
@php
|
|
// Die Adresse, die WIRKLICH greift, wenn eine Mailart
|
|
// nichts Eigenes hat — vom Bauteil über dieselbe
|
|
// Auflösung geholt, die beim Senden entscheidet. Das
|
|
// hier zugeordnete Postfach ist nicht dieselbe Antwort:
|
|
// ein abgeschaltetes fällt auf „System" zurück.
|
|
$inherited = $inheritedAddress[$purpose] ?? null;
|
|
@endphp
|
|
|
|
<section wire:key="purpose-{{ $purpose }}" class="rounded-lg border border-line">
|
|
<div class="flex flex-wrap items-center justify-between gap-x-6 gap-y-3 rounded-t-lg bg-surface-2 px-4 py-3">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-ink">{{ __('mail_settings.purpose.'.$purpose) }}</h3>
|
|
<p class="mt-0.5 text-xs text-muted">{{ __('mail_settings.purpose_mailbox') }}</p>
|
|
</div>
|
|
<select wire:model="purposes.{{ $purpose }}"
|
|
aria-label="{{ __('mail_settings.purpose_mailbox') }}"
|
|
class="w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body sm:w-72">
|
|
<option value="">—</option>
|
|
@foreach ($mailboxes as $box)
|
|
<option value="{{ $box->key }}">{{ $box->address }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
@error('purposes.'.$purpose)
|
|
<p class="border-t border-line px-4 py-2 text-xs text-danger">{{ $message }}</p>
|
|
@enderror
|
|
|
|
@if ($entries === [])
|
|
<p class="px-4 py-3 text-sm text-muted">{{ __('mail_settings.purpose_no_types') }}</p>
|
|
@else
|
|
<p class="lbl border-t border-line px-4 pt-3">{{ __('mail_settings.route_exceptions') }}</p>
|
|
<div class="divide-y divide-line px-4 pb-2">
|
|
@foreach ($entries as $key => $entry)
|
|
<div wire:key="route-{{ $key }}"
|
|
class="flex flex-wrap items-center justify-between gap-x-6 gap-y-2 py-2.5">
|
|
<span class="text-sm text-body">{{ $entry['label'] }}</span>
|
|
<select wire:model="routes.{{ $key }}"
|
|
aria-label="{{ $entry['label'] }}"
|
|
class="w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body sm:w-72">
|
|
<option value="">
|
|
{{ $inherited === null
|
|
? __('mail_settings.route_inherit_unset')
|
|
: __('mail_settings.route_inherit', ['address' => $inherited]) }}
|
|
</option>
|
|
@foreach ($mailboxes->where('active', true) as $box)
|
|
<option value="{{ $box->key }}">{{ $box->address }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</section>
|
|
@endforeach
|
|
</div>
|
|
|
|
<x-ui.button wire:click="saveAssignments" variant="primary" size="md" class="mt-6">
|
|
{{ __('mail_settings.save') }}
|
|
</x-ui.button>
|
|
</x-ui.card>
|
|
@endif
|
|
</div>
|