323 lines
18 KiB
PHP
323 lines
18 KiB
PHP
@push('modal.header')
|
||
<div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-white">
|
||
{{ $aliasId ? 'Alias bearbeiten' : 'Alias anlegen' }}
|
||
</h2>
|
||
<p class="text-[13px] text-white/60">
|
||
{{ $aliasId ? 'Passe den Alias und seine Empfänger an.' : 'Lege Adresse und Empfänger fest.' }}
|
||
</p>
|
||
</div>
|
||
<button wire:click="$dispatch('closeModal')" class="text-white/60 hover:text-white text-lg">
|
||
<i class="ph ph-x"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
@endpush
|
||
|
||
<div class="p-6 space-y-5">
|
||
{{-- alles in ein Formular, wie beim Limits-Modal --}}
|
||
<form id="alias-form" wire:submit.prevent="save" class="space-y-4">
|
||
{{-- Domain + Adresse + Status --}}
|
||
|
||
<div class="flex items-end">
|
||
<label class="inline-flex items-center gap-3 select-none">
|
||
<input type="checkbox" wire:model="is_active"
|
||
class="peer appearance-none w-5 h-5 rounded-md border border-emerald-500/50 bg-transparent
|
||
checked:bg-emerald-500 checked:border-emerald-400 grid place-content-center transition">
|
||
<span class="text-slate-200">Alias aktivieren</span>
|
||
</label>
|
||
</div>
|
||
|
||
{{-- Row 1: Domain + Typ --}}
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
|
||
{{-- DOMAIN (TailwindPlus Elements) --}}
|
||
<div class="relative" wire:ignore id="domain-select-{{ $this->getId() }}">
|
||
<label class="block text-xs text-white/60 mb-1">Domain</label>
|
||
|
||
<el-select id="el-domain-{{ $this->getId() }}" name="domain_id" class="block w-full">
|
||
{{-- Trigger --}}
|
||
<button type="button"
|
||
class="grid w-full grid-cols-1 cursor-default rounded-2xl bg-white/5 border border-white/10 px-3.5 h-11 text-white/90">
|
||
<el-selectedcontent class="col-start-1 row-start-1 flex items-center gap-2 pr-6">
|
||
<i class="ph ph-globe-hemisphere-west text-white/60 text-[16px]"></i>
|
||
<span class="block truncate" data-selected-label>
|
||
{{ optional($domain)->domain ?? 'Domain wählen' }}
|
||
</span>
|
||
</el-selectedcontent>
|
||
<i class="ph ph-caret-down col-start-1 row-start-1 self-center justify-self-end text-white/50"></i>
|
||
</button>
|
||
|
||
{{-- Optionen --}}
|
||
<el-options anchor="bottom start" popover
|
||
class="max-h-60 w-(--button-width) overflow-auto rounded-2xl bg-gray-900/95 backdrop-blur
|
||
py-1 text-base outline-1 -outline-offset-1 outline-white/10 shadow-xl sm:text-sm
|
||
data-leave:transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0">
|
||
@foreach($domains as $d)
|
||
<el-option
|
||
value="{{ $d->id }}"
|
||
data-value="{{ $d->id }}"
|
||
data-label="{{ $d->domain }}"
|
||
@if($domain && $domain->id === $d->id) aria-selected="true" @endif
|
||
class="group/option relative block cursor-default py-2 pr-9 pl-3 text-white/90 select-none
|
||
hover:bg-white/5 focus:bg-white/10 focus:text-white focus:outline-hidden"
|
||
onclick="(function(el){
|
||
const root = el.closest('#domain-select-{{ $this->getId() }}');
|
||
root.querySelectorAll('el-option').forEach(o => o.removeAttribute('aria-selected'));
|
||
el.setAttribute('aria-selected','true');
|
||
const labelEl = root.querySelector('[data-selected-label]');
|
||
if (labelEl) { labelEl.textContent = el.dataset.label; }
|
||
@this.set('domainId', parseInt(el.dataset.value, 10));
|
||
root.querySelector('button')?.focus();
|
||
})(this)">
|
||
<div class="flex items-center gap-2">
|
||
<i class="ph ph-globe-hemisphere-west text-white/60"></i>
|
||
<span
|
||
class="block truncate group-aria-selected/option:font-semibold">{{ $d->domain }}</span>
|
||
</div>
|
||
<span class="absolute inset-y-0 right-0 hidden items-center pr-3 text-emerald-300
|
||
group-aria-selected/option:flex in-[el-selectedcontent]:hidden">
|
||
<i class="ph ph-check text-[18px]"></i>
|
||
</span>
|
||
</el-option>
|
||
@endforeach
|
||
</el-options>
|
||
</el-select>
|
||
|
||
@error('domainId')
|
||
<div class="text-rose-300 text-xs mt-1">{{ $message }}</div> @enderror
|
||
</div>
|
||
|
||
{{-- TYP (TailwindPlus Elements) --}}
|
||
<div class="relative" wire:ignore id="type-select-{{ $this->getId() }}">
|
||
<label class="block text-xs text-white/60 mb-1">Typ</label>
|
||
|
||
<el-select id="el-type-{{ $this->getId() }}" name="alias_type" class="block w-full">
|
||
{{-- Trigger --}}
|
||
<button type="button"
|
||
class="grid w-full grid-cols-1 cursor-default rounded-2xl bg-white/5 border border-white/10 px-3.5 h-11 text-white/90">
|
||
<el-selectedcontent class="col-start-1 row-start-1 flex items-center gap-2 pr-6">
|
||
<i class="ph {{ $type === 'group' ? 'ph-users' : 'ph-user' }} text-white/60 text-[16px]"></i>
|
||
<span class="block truncate" data-selected-label-type>
|
||
{{ $type === 'group' ? 'Gruppe' : 'Single' }}
|
||
</span>
|
||
</el-selectedcontent>
|
||
<i class="ph ph-caret-down col-start-1 row-start-1 self-center justify-self-end text-white/50"></i>
|
||
</button>
|
||
|
||
{{-- Optionen --}}
|
||
<el-options anchor="bottom start" popover
|
||
class="max-h-60 w-(--button-width) overflow-auto rounded-2xl bg-gray-900/95 backdrop-blur
|
||
py-1 text-base outline-1 -outline-offset-1 outline-white/10 shadow-xl sm:text-sm
|
||
data-leave:transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0">
|
||
|
||
{{-- Single --}}
|
||
<el-option data-value="single" data-label="Single"
|
||
@if($type === 'single') aria-selected="true" @endif
|
||
class="group/option relative block cursor-default py-2 pr-9 pl-3 text-white/90 select-none
|
||
hover:bg-white/5 focus:bg-white/10 focus:text-white focus:outline-hidden"
|
||
onclick="(function(el){
|
||
const root = el.closest('#type-select-{{ $this->getId() }}');
|
||
root.querySelectorAll('el-option').forEach(o => o.removeAttribute('aria-selected'));
|
||
el.setAttribute('aria-selected','true');
|
||
const lbl = root.querySelector('[data-selected-label-type]');
|
||
if (lbl) { lbl.textContent = el.dataset.label; }
|
||
// Icon im Trigger wechseln
|
||
const icon = root.querySelector('.ph-user, .ph-users');
|
||
if (icon) { icon.className = 'ph ph-user text-white/60 text-[16px]'; }
|
||
@this.set('type', 'single');
|
||
root.querySelector('button')?.focus();
|
||
})(this)">
|
||
<div class="flex items-center gap-2">
|
||
<i class="ph ph-user text-white/60"></i>
|
||
<span class="block truncate group-aria-selected/option:font-semibold">Single</span>
|
||
</div>
|
||
<span class="absolute inset-y-0 right-0 hidden items-center pr-3 text-emerald-300
|
||
group-aria-selected/option:flex in-[el-selectedcontent]:hidden">
|
||
<i class="ph ph-check text-[18px]"></i>
|
||
</span>
|
||
</el-option>
|
||
|
||
{{-- Gruppe --}}
|
||
<el-option data-value="group" data-label="Gruppe"
|
||
@if($type === 'group') aria-selected="true" @endif
|
||
class="group/option relative block cursor-default py-2 pr-9 pl-3 text-white/90 select-none
|
||
hover:bg-white/5 focus:bg-white/10 focus:text-white focus:outline-hidden"
|
||
onclick="(function(el){
|
||
const root = el.closest('#type-select-{{ $this->getId() }}');
|
||
root.querySelectorAll('el-option').forEach(o => o.removeAttribute('aria-selected'));
|
||
el.setAttribute('aria-selected','true');
|
||
const lbl = root.querySelector('[data-selected-label-type]');
|
||
if (lbl) { lbl.textContent = el.dataset.label; }
|
||
const icon = root.querySelector('.ph-user, .ph-users');
|
||
if (icon) { icon.className = 'ph ph-users text-white/60 text-[16px]'; }
|
||
@this.set('type', 'group');
|
||
root.querySelector('button')?.focus();
|
||
})(this)">
|
||
<div class="flex items-center gap-2">
|
||
<i class="ph ph-users text-white/60"></i>
|
||
<span class="block truncate group-aria-selected/option:font-semibold">Gruppe</span>
|
||
</div>
|
||
<span class="absolute inset-y-0 right-0 hidden items-center pr-3 text-emerald-300
|
||
group-aria-selected/option:flex in-[el-selectedcontent]:hidden">
|
||
<i class="ph ph-check text-[18px]"></i>
|
||
</span>
|
||
</el-option>
|
||
</el-options>
|
||
</el-select>
|
||
|
||
@error('type')
|
||
<div class="text-rose-300 text-xs mt-1">{{ $message }}</div> @enderror
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Row 2: Adresse (volle Breite, gleiche Höhe) --}}
|
||
<div>
|
||
<label class="block text-xs text-white/60 mb-1">Adresse</label>
|
||
<div class="flex">
|
||
<input type="text" wire:model.defer="local" placeholder="info"
|
||
class="flex-1 rounded-l-2xl bg-white/5 border border-white/10 text-white px-3 h-11 text-sm focus:border-white/20 focus:ring-0">
|
||
<span
|
||
class="inline-flex items-center px-3 rounded-r-2xl border border-l-0 border-white/10 bg-white/5 text-white/70 text-sm h-11">
|
||
@ {{ optional($domain)->domain }}
|
||
</span>
|
||
</div>
|
||
@error('local')
|
||
<div class="mt-1 text-xs text-rose-300">{{ $message }}</div> @enderror
|
||
</div>
|
||
|
||
@if($type === 'group')
|
||
<div>
|
||
<label class="block text-xs text-white/60 mb-1">Gruppenname (optional)</label>
|
||
<input type="text" wire:model.defer="group_name"
|
||
class="w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 h-11 text-sm focus:border-white/20 focus:ring-0"
|
||
placeholder="z. B. Office Gruppe">
|
||
@error('group_name') <div class="mt-1 text-xs text-rose-300">{{ $message }}</div> @enderror
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Empfänger --}}
|
||
<div class="space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<h3 class="text-sm text-white/80">Empfänger</h3>
|
||
<div class="text-end">
|
||
@if($type === 'single')
|
||
<div class="text-[11px] text-white/50 mt-1">Bei „Single“ ist nur ein Empfänger erlaubt.</div>
|
||
@else
|
||
<button type="button"
|
||
wire:click="addRecipientRow"
|
||
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-xs
|
||
text-white/80 hover:text-white hover:border-white/20 disabled:opacity-40 disabled:cursor-not-allowed"
|
||
@disabled(! $canAddRecipient)>
|
||
<i class="ph ph-plus"></i> Empfänger
|
||
</button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
@error('recipients')
|
||
<div class="text-xs text-rose-300">{{ $message }}</div>
|
||
@enderror
|
||
|
||
{{-- WICHTIG: stabiler Container-Key um die Liste --}}
|
||
<div class="space-y-4" wire:key="recipients-container">
|
||
@foreach ($recipients as $idx => $r)
|
||
<div class="rounded-xl border border-white/10 bg-white/[0.04] p-3"
|
||
wire:key="recipient-{{ $r['id'] }}">
|
||
{{-- Labels (dynamisches Grid – 3 oder 4 Spalten je nach Typ) --}}
|
||
<div class="grid {{ $type === 'single' ? 'grid-cols-[5fr_auto_5fr]' : 'grid-cols-[5fr_auto_5fr_auto]' }} gap-3 mb-1 text-xs text-white/60">
|
||
<div>Interner Empfänger (Postfach)</div>
|
||
<div></div>
|
||
<div>Externe E-Mail</div>
|
||
@if($type === 'group')
|
||
<div></div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- Eingaben + oder + (optional) Löschen --}}
|
||
<div class="grid {{ $type === 'single' ? 'grid-cols-[5fr_auto_5fr]' : 'grid-cols-[5fr_auto_5fr_auto]' }} gap-3 items-center">
|
||
{{-- Interner Empfänger --}}
|
||
<div>
|
||
<select
|
||
wire:model.live="recipients.{{ $idx }}.mail_user_id"
|
||
class="w-full h-11 rounded-2xl bg-white/5 border border-white/10 text-white px-3 text-sm
|
||
focus:border-white/20 focus:ring-0 disabled:opacity-40 disabled:cursor-not-allowed"
|
||
@disabled($rowState[$idx]['disable_internal'] ?? false)>
|
||
<option value="">—</option>
|
||
@foreach($domainMailUsers as $mu)
|
||
<option value="{{ $mu->id }}"
|
||
@disabled(in_array($mu->id, $disabledMailboxIdsByRow[$idx] ?? [], true))>
|
||
{{ $mu->localpart . '@' . optional($domain)->domain }}
|
||
</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
|
||
{{-- ODER --}}
|
||
<div class="flex items-center justify-center text-white/50 select-none">oder</div>
|
||
|
||
{{-- Externe E-Mail --}}
|
||
<div>
|
||
<input type="email"
|
||
wire:model.live="recipients.{{ $idx }}.email"
|
||
placeholder="user@example.com"
|
||
class="w-full h-11 rounded-2xl bg-white/5 border border-white/10 text-white px-3 text-sm
|
||
focus:border-white/20 focus:ring-0 disabled:opacity-40 disabled:cursor-not-allowed"
|
||
@disabled($rowState[$idx]['disable_external'] ?? false)>
|
||
@error("recipients.$idx.email")
|
||
<div class="mt-1 text-xs text-rose-300">{{ $message }}</div>
|
||
@enderror
|
||
</div>
|
||
|
||
{{-- Löschen (nur bei Gruppe sichtbar) --}}
|
||
@if($type === 'group')
|
||
<div class="flex items-center justify-center">
|
||
<button type="button"
|
||
wire:click="removeRecipientRow({{ $idx }})"
|
||
class="size-7 grid place-items-center rounded-lg bg-rose-500/20 text-rose-200
|
||
border border-rose-400/40 hover:bg-rose-500/30
|
||
disabled:opacity-40 disabled:cursor-not-allowed">
|
||
<i class="ph ph-trash-simple text-base"></i>
|
||
</button>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
|
||
@if($type === 'single' && count($recipients) > 1)
|
||
<div class="text-xs text-amber-300">
|
||
Hinweis: Bei „Single“ wird nur der erste Empfänger gespeichert.
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-white/60 mb-1">Notizen (optional)</label>
|
||
<textarea wire:model.defer="notes" rows="3"
|
||
class="w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm focus:border-white/20 focus:ring-0"></textarea>
|
||
@error('notes')
|
||
<div class="mt-1 text-xs text-rose-300">{{ $message }}</div> @enderror
|
||
</div>
|
||
|
||
</form>
|
||
</div>
|
||
|
||
@push('modal.footer')
|
||
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" wire:click="$dispatch('closeModal')"
|
||
class="px-4 py-2 rounded-lg text-sm border border-white/10 text-white/70 hover:text-white hover:border-white/20">
|
||
Abbrechen
|
||
</button>
|
||
<button type="submit" form="alias-form"
|
||
class="px-4 py-2 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
|
||
{{ $aliasId ? 'Speichern' : 'Anlegen' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
@endpush
|