presseportale/resources/views/livewire/admin/users/edit.blade.php
Kevin Adametz 036a53499f Responsive-Härtung: Seiten-Header, Kontextleiste, Stat-Cards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:08:08 +00:00

1241 lines
51 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\Portal;
use App\Enums\RegistrationType;
use App\Models\Company;
use App\Models\Contact;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class extends Component
{
#[Locked]
public int $id;
public string $name = '';
public string $email = '';
public string $portal = '';
public string $registrationType = '';
public string $language = 'de';
/**
* @var array{
* salutation_key:?string,
* title:?string,
* first_name:?string,
* last_name:?string,
* phone:?string,
* address:?string,
* country_code:?string,
* birthdate:string,
* backlink_url:?string,
* show_stats:bool,
* validation_date:string,
* contract_date:string,
* tax_id_number:?string,
* tax_exempt:bool,
* tax_exempt_reason:?string,
* disable_footer_code:bool
* }
*/
public array $profile = [
'salutation_key' => null,
'title' => null,
'first_name' => null,
'last_name' => null,
'phone' => null,
'address' => null,
'country_code' => null,
'birthdate' => '',
'backlink_url' => null,
'show_stats' => false,
'validation_date' => '',
'contract_date' => '',
'tax_id_number' => null,
'tax_exempt' => false,
'tax_exempt_reason' => null,
'disable_footer_code' => false,
];
public bool $isActive = true;
public bool $isSuperAdmin = false;
/** @var array<int, string> */
public array $selectedRoles = [];
/** @var array<int, int> */
public array $linkedCompanyIds = [];
/** @var array<int, string> */
public array $companyRoles = [];
public string $notification = '';
public string $companyLookup = '';
public ?int $selectedLookupCompanyId = null;
/**
* @var array<int, array{
* id:int,
* company_id:int,
* company_name:string,
* first_name:?string,
* last_name:?string,
* email:?string,
* phone:?string,
* responsibility:?string
* }>
*/
public array $contactForms = [];
/** @var array<int, int> */
public array $linkedContactIds = [];
public string $contactLookup = '';
public ?int $selectedLookupContactId = null;
/**
* @var array{
* salutation_key:?string,
* title:?string,
* name:?string,
* address1:?string,
* address2:?string,
* postal_code:?string,
* city:?string,
* country_code:?string
* }
*/
public array $billing = [
'salutation_key' => null,
'title' => null,
'name' => null,
'address1' => null,
'address2' => null,
'postal_code' => null,
'city' => null,
'country_code' => null,
];
public function mount(int $id): void
{
$this->id = $id;
$user = User::query()
->with([
'roles:id,name',
'companies:id,name,slug',
'contacts:id,company_id,first_name,last_name,email,phone,responsibility',
'profile',
'billingAddress',
])
->findOrFail($id);
$this->name = $user->name;
$this->email = $user->email;
$this->portal = $user->portal?->value ?? Portal::Both->value;
$this->registrationType = $user->registration_type?->value ?? RegistrationType::ExistingLegacy->value;
$this->language = $user->language ?: 'de';
$this->isActive = (bool) $user->is_active;
$this->isSuperAdmin = (bool) $user->is_super_admin;
$this->selectedRoles = $user->roles->pluck('name')->values()->all();
$this->linkedCompanyIds = $user->companies->pluck('id')->map(fn ($id) => (int) $id)->values()->all();
$profile = $user->profile;
if ($profile) {
$this->profile = [
'salutation_key' => $profile->salutation_key,
'title' => $profile->title,
'first_name' => $profile->first_name,
'last_name' => $profile->last_name,
'phone' => $profile->phone,
'address' => $profile->address,
'country_code' => $profile->country_code,
'birthdate' => $profile->birthdate?->format('Y-m-d') ?? '',
'backlink_url' => $profile->backlink_url,
'show_stats' => (bool) $profile->show_stats,
'validation_date' => $profile->validation_date?->format('Y-m-d') ?? '',
'contract_date' => $profile->contract_date?->format('Y-m-d') ?? '',
'tax_id_number' => $profile->tax_id_number,
'tax_exempt' => (bool) $profile->tax_exempt,
'tax_exempt_reason' => $profile->tax_exempt_reason,
'disable_footer_code' => (bool) $profile->disable_footer_code,
];
}
foreach ($user->companies as $company) {
$this->companyRoles[(int) $company->id] = (string) ($company->pivot->role ?? 'member');
}
$this->linkedContactIds = $user->contacts
->pluck('id')
->map(fn ($contactId) => (int) $contactId)
->values()
->all();
if ($this->linkedContactIds === []) {
$this->linkedContactIds = Contact::query()
->whereIn('company_id', $this->linkedCompanyIds ?: [-1])
->orderBy('id')
->limit(40)
->pluck('id')
->map(fn ($contactId) => (int) $contactId)
->values()
->all();
}
$billingAddress = $user->billingAddress;
if ($billingAddress) {
$this->billing = [
'salutation_key' => $billingAddress->salutation_key,
'title' => $billingAddress->title,
'name' => $billingAddress->name,
'address1' => $billingAddress->address1,
'address2' => $billingAddress->address2,
'postal_code' => $billingAddress->postal_code,
'city' => $billingAddress->city,
'country_code' => $billingAddress->country_code,
];
}
$this->syncContactFormsToSelectedCompanies();
}
public function updatedLinkedCompanyIds(): void
{
$this->syncContactFormsToSelectedCompanies();
}
// Combobox-Auswahl löst sofort Zuordnung aus kein separater Button nötig
public function updatedSelectedLookupCompanyId(): void
{
if (! $this->selectedLookupCompanyId) {
return;
}
$this->addLinkedCompany();
}
public function updatedSelectedLookupContactId(): void
{
if (! $this->selectedLookupContactId) {
return;
}
$this->addLinkedContact();
}
public function clearCompanyLookup(): void
{
$this->companyLookup = '';
$this->selectedLookupCompanyId = null;
}
public function clearContactLookup(): void
{
$this->contactLookup = '';
$this->selectedLookupContactId = null;
}
public function addLinkedCompany(): void
{
if (! $this->selectedLookupCompanyId) {
return;
}
if (! in_array($this->selectedLookupCompanyId, $this->linkedCompanyIds, true)) {
$this->linkedCompanyIds[] = $this->selectedLookupCompanyId;
$this->linkedCompanyIds = collect($this->linkedCompanyIds)
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
}
$this->companyRoles[$this->selectedLookupCompanyId] ??= 'member';
$this->persistUserLinks();
$this->companyLookup = '';
$this->selectedLookupCompanyId = null;
$this->syncContactFormsToSelectedCompanies();
$this->notification = __('Firma wurde direkt verknüpft.');
}
public function removeLinkedCompany(int $companyId): void
{
$this->linkedCompanyIds = collect($this->linkedCompanyIds)
->reject(fn ($id): bool => (int) $id === $companyId)
->map(fn ($id) => (int) $id)
->values()
->all();
unset($this->companyRoles[$companyId]);
$removedCompanyContactIds = Contact::query()
->where('company_id', $companyId)
->pluck('id')
->map(fn ($contactId) => (int) $contactId)
->all();
$this->linkedContactIds = collect($this->linkedContactIds)
->reject(fn ($contactId): bool => in_array((int) $contactId, $removedCompanyContactIds, true))
->map(fn ($contactId) => (int) $contactId)
->values()
->all();
$this->persistUserLinks();
$this->syncContactFormsToSelectedCompanies();
$this->notification = __('Firma wurde direkt entfernt.');
}
public function addLinkedContact(): void
{
if (! $this->selectedLookupContactId) {
return;
}
$contact = Contact::withoutGlobalScopes()
->with('company:id,name')
->find($this->selectedLookupContactId);
if (! $contact) {
return;
}
$contactCompanyId = (int) $contact->company_id;
if ($contactCompanyId > 0 && ! in_array($contactCompanyId, $this->linkedCompanyIds, true)) {
$this->linkedCompanyIds[] = $contactCompanyId;
$this->linkedCompanyIds = collect($this->linkedCompanyIds)
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
$this->companyRoles[$contactCompanyId] ??= 'member';
}
$alreadyLinked = collect($this->contactForms)
->contains(fn ($form): bool => (int) $form['id'] === (int) $contact->id);
if (! $alreadyLinked) {
$this->linkedContactIds[] = (int) $contact->id;
$this->linkedContactIds = collect($this->linkedContactIds)
->map(fn ($contactId) => (int) $contactId)
->unique()
->values()
->all();
$this->contactForms[] = [
'id' => (int) $contact->id,
'company_id' => (int) $contact->company_id,
'company_name' => (string) ($contact->company?->name ?? __('Unbekannte Firma')),
'first_name' => $contact->first_name,
'last_name' => $contact->last_name,
'email' => $contact->email,
'phone' => $contact->phone,
'responsibility' => $contact->responsibility,
];
}
$this->persistUserLinks();
$this->contactLookup = '';
$this->selectedLookupContactId = null;
$this->notification = __('Kontakt wurde direkt zugeordnet.');
}
public function removeLinkedContact(int $contactId): void
{
$this->linkedContactIds = collect($this->linkedContactIds)
->reject(fn ($linkedContactId): bool => (int) $linkedContactId === $contactId)
->map(fn ($linkedContactId) => (int) $linkedContactId)
->values()
->all();
$this->contactForms = collect($this->contactForms)
->reject(fn ($form): bool => (int) $form['id'] === $contactId)
->values()
->all();
$this->persistUserLinks();
$this->notification = __('Kontakt wurde direkt entfernt.');
}
public function save(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($this->id)],
'portal' => ['required', Rule::in(array_map(static fn (Portal $portal): string => $portal->value, Portal::cases()))],
'registrationType' => ['required', Rule::in(array_map(static fn (RegistrationType $type): string => $type->value, RegistrationType::cases()))],
'language' => ['required', 'string', Rule::in(['de', 'en'])],
'profile.salutation_key' => ['nullable', 'string', 'max:20'],
'profile.title' => ['nullable', 'string', 'max:80'],
'profile.first_name' => ['nullable', 'string', 'max:80'],
'profile.last_name' => ['nullable', 'string', 'max:80'],
'profile.phone' => ['nullable', 'string', 'max:40'],
'profile.address' => ['nullable', 'string', 'max:1000'],
'profile.country_code' => ['nullable', 'string', 'size:2'],
'profile.birthdate' => ['nullable', 'date'],
'profile.backlink_url' => ['nullable', 'string', 'max:255'],
'profile.show_stats' => ['boolean'],
'profile.validation_date' => ['nullable', 'date'],
'profile.contract_date' => ['nullable', 'date'],
'profile.tax_id_number' => ['nullable', 'string', 'max:255'],
'profile.tax_exempt' => ['boolean'],
'profile.tax_exempt_reason' => ['nullable', 'string', 'max:1000'],
'profile.disable_footer_code' => ['boolean'],
'selectedRoles' => ['required', 'array', 'min:1'],
'selectedRoles.*' => ['string', Rule::exists('roles', 'name')],
'linkedCompanyIds' => ['array'],
'linkedCompanyIds.*' => ['integer', Rule::exists('companies', 'id')],
'companyRoles' => ['array'],
'companyRoles.*' => ['string', Rule::in(['member', 'responsible', 'owner'])],
'contactForms' => ['array'],
'contactForms.*.id' => ['required', 'integer', Rule::exists('contacts', 'id')],
'contactForms.*.company_id' => ['required', 'integer', Rule::exists('companies', 'id')],
'contactForms.*.first_name' => ['nullable', 'string', 'max:80'],
'contactForms.*.last_name' => ['nullable', 'string', 'max:80'],
'contactForms.*.email' => ['nullable', 'email', 'max:255'],
'contactForms.*.phone' => ['nullable', 'string', 'max:40'],
'contactForms.*.responsibility' => ['nullable', 'string', 'max:255'],
'billing.salutation_key' => ['nullable', 'string', 'max:20'],
'billing.title' => ['nullable', 'string', 'max:80'],
'billing.name' => ['nullable', 'string', 'max:255'],
'billing.address1' => ['nullable', 'string', 'max:255'],
'billing.address2' => ['nullable', 'string', 'max:255'],
'billing.postal_code' => ['nullable', 'string', 'max:20'],
'billing.city' => ['nullable', 'string', 'max:120'],
'billing.country_code' => ['nullable', 'string', 'size:2'],
]);
$user = User::query()->findOrFail($this->id);
$user->update([
'name' => $validated['name'],
'email' => $validated['email'],
'portal' => $validated['portal'],
'registration_type' => $validated['registrationType'],
'language' => $validated['language'],
'is_active' => $this->isActive,
'is_super_admin' => $this->isSuperAdmin,
]);
$user->syncRoles($validated['selectedRoles']);
$profilePayload = [
'salutation_key' => $validated['profile']['salutation_key'] ?: null,
'title' => $validated['profile']['title'] ?: null,
'first_name' => $validated['profile']['first_name'] ?: null,
'last_name' => $validated['profile']['last_name'] ?: null,
'phone' => $validated['profile']['phone'] ?: null,
'address' => $validated['profile']['address'] ?: null,
'country_code' => filled($validated['profile']['country_code'])
? strtoupper((string) $validated['profile']['country_code'])
: null,
'birthdate' => filled($validated['profile']['birthdate']) ? $validated['profile']['birthdate'] : null,
'backlink_url' => $validated['profile']['backlink_url'] ?: null,
'show_stats' => (bool) $validated['profile']['show_stats'],
'validation_date' => filled($validated['profile']['validation_date']) ? $validated['profile']['validation_date'] : null,
'contract_date' => filled($validated['profile']['contract_date']) ? $validated['profile']['contract_date'] : null,
'tax_id_number' => $validated['profile']['tax_id_number'] ?: null,
'tax_exempt' => (bool) $validated['profile']['tax_exempt'],
'tax_exempt_reason' => $validated['profile']['tax_exempt_reason'] ?: null,
'disable_footer_code' => (bool) $validated['profile']['disable_footer_code'],
];
if ($this->profileHasInput($profilePayload)) {
$user->profile()->updateOrCreate([], $profilePayload);
} else {
$user->profile()->delete();
}
$companySyncPayload = [];
foreach ($validated['linkedCompanyIds'] as $companyId) {
$companySyncPayload[$companyId] = [
'role' => $validated['companyRoles'][$companyId] ?? 'member',
];
}
$user->companies()->sync($companySyncPayload);
$linkedCompanyIds = array_keys($companySyncPayload);
$contactIds = collect($validated['contactForms'])
->pluck('id')
->map(fn ($id) => (int) $id)
->all();
$this->linkedContactIds = $contactIds;
$contactsById = Contact::query()
->whereIn('id', $contactIds)
->whereIn('company_id', $linkedCompanyIds ?: [-1])
->get()
->keyBy('id');
foreach ($validated['contactForms'] as $contactForm) {
/** @var Contact|null $contact */
$contact = $contactsById->get((int) $contactForm['id']);
if (! $contact) {
continue;
}
$contact->update([
'first_name' => $contactForm['first_name'] ?: null,
'last_name' => $contactForm['last_name'] ?: null,
'email' => $contactForm['email'] ?: null,
'phone' => $contactForm['phone'] ?: null,
'responsibility' => $contactForm['responsibility'] ?: null,
]);
}
$user->contacts()->sync($contactIds);
$billingAddress = $user->billingAddress()->first();
$billingPayload = [
'salutation_key' => $validated['billing']['salutation_key'] ?: null,
'title' => $validated['billing']['title'] ?: null,
'name' => $validated['billing']['name'] ?: null,
'address1' => $validated['billing']['address1'] ?: null,
'address2' => $validated['billing']['address2'] ?: null,
'postal_code' => $validated['billing']['postal_code'] ?: null,
'city' => $validated['billing']['city'] ?: null,
'country_code' => filled($validated['billing']['country_code'])
? strtoupper((string) $validated['billing']['country_code'])
: null,
];
if (! $this->billingHasInput()) {
if ($billingAddress) {
$billingAddress->delete();
}
} elseif ($billingAddress) {
// Payload direkt übernehmen null-Werte leeren das Feld bewusst.
// Pflichtfelder (name, address1, postal_code, city, country_code) müssen befüllt bleiben.
if (
filled($billingPayload['name'])
&& filled($billingPayload['address1'])
&& filled($billingPayload['postal_code'])
&& filled($billingPayload['city'])
&& filled($billingPayload['country_code'])
) {
$billingAddress->update($billingPayload);
}
} elseif ($this->billingIsComplete()) {
$user->billingAddress()->create([
'salutation_key' => $billingPayload['salutation_key'],
'title' => $billingPayload['title'],
'name' => (string) $billingPayload['name'],
'address1' => (string) $billingPayload['address1'],
'address2' => $billingPayload['address2'],
'postal_code' => (string) $billingPayload['postal_code'],
'city' => (string) $billingPayload['city'],
'country_code' => (string) $billingPayload['country_code'],
]);
}
session()->flash('success', __('Benutzer, Profil, Rollen, Firmenzuordnung, Kontakte und Rechnungsadresse wurden gespeichert.'));
$this->redirect(route('admin.users.index'), navigate: true);
}
public function with(): array
{
$linkedCompanyIds = $this->linkedCompanyIds ?: [-1];
$currentContactIds = collect($this->linkedContactIds)
->map(fn ($id) => (int) $id)
->all();
$companyLookupResults = collect();
$companyLookupTerm = trim($this->companyLookup);
if (mb_strlen($companyLookupTerm) >= 1) {
$companyLookupResults = Company::withoutGlobalScopes()
->whereNotIn('id', $this->linkedCompanyIds ?: [-1])
->where(function ($query) use ($companyLookupTerm): void {
if ($this->supportsFullTextSearch($companyLookupTerm)) {
$query->whereFullText(['name', 'email', 'slug'], $companyLookupTerm);
return;
}
$query
->where('name', 'like', '%'.$companyLookupTerm.'%')
->orWhere('slug', 'like', '%'.$companyLookupTerm.'%')
->orWhere('email', 'like', '%'.$companyLookupTerm.'%');
})
->orderBy('name')
->limit(50)
->get(['id', 'name', 'slug', 'email']);
}
$contactLookupResults = collect();
$contactLookupTerm = trim($this->contactLookup);
if (mb_strlen($contactLookupTerm) >= 1) {
$contactLookupResults = Contact::withoutGlobalScopes()
->with('company:id,name')
->whereNotIn('id', $currentContactIds ?: [-1])
->where(function ($query) use ($contactLookupTerm): void {
if ($this->supportsFullTextSearch($contactLookupTerm)) {
$query->whereFullText(['first_name', 'last_name', 'email', 'responsibility'], $contactLookupTerm);
return;
}
$query
->where('first_name', 'like', '%'.$contactLookupTerm.'%')
->orWhere('last_name', 'like', '%'.$contactLookupTerm.'%')
->orWhere('email', 'like', '%'.$contactLookupTerm.'%');
})
->orderBy('last_name')
->orderBy('first_name')
->limit(50)
->get(['id', 'company_id', 'first_name', 'last_name', 'email']);
}
return [
'availableRoles' => $this->availableRoles(),
'linkedCompanies' => Company::withoutGlobalScopes()
->whereIn('id', $linkedCompanyIds)
->orderBy('name')
->get(['id', 'name', 'slug', 'email']),
'companyLookupResults' => $companyLookupResults,
'contactLookupResults' => $contactLookupResults,
'salutations' => config('salutations.items', []),
'countries' => config('countries.items', []),
'portalOptions' => Portal::cases(),
'registrationTypeOptions' => RegistrationType::cases(),
];
}
private function availableRoles(): Collection
{
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::RoleOptions, AdminPerformanceCache::OptionsTtl, fn () => Role::query()
->orderBy('name')
->get(['id', 'name']));
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
protected function syncContactFormsToSelectedCompanies(): void
{
$currentFormsById = collect($this->contactForms)->keyBy('id');
$allowedCompanyIds = $this->linkedCompanyIds ?: [-1];
$currentContactIds = $currentFormsById->keys()->map(fn ($id) => (int) $id)->all();
$contactsQuery = Contact::query()
->with('company:id,name')
->whereIn('company_id', $allowedCompanyIds)
->orderBy('company_id')
->orderBy('id');
if ($currentContactIds !== []) {
$contactsQuery->whereIn('id', $currentContactIds);
} else {
$contactsQuery->limit(40);
}
$contacts = $contactsQuery->get();
$this->contactForms = $contacts->map(function (Contact $contact) use ($currentFormsById): array {
$current = $currentFormsById->get($contact->id);
return [
'id' => (int) $contact->id,
'company_id' => (int) $contact->company_id,
'company_name' => (string) ($contact->company?->name ?? __('Unbekannte Firma')),
'first_name' => $current['first_name'] ?? $contact->first_name,
'last_name' => $current['last_name'] ?? $contact->last_name,
'email' => $current['email'] ?? $contact->email,
'phone' => $current['phone'] ?? $contact->phone,
'responsibility' => $current['responsibility'] ?? $contact->responsibility,
];
})->values()->all();
}
protected function billingHasInput(): bool
{
return collect($this->billing)
->filter(fn ($value): bool => filled($value))
->isNotEmpty();
}
/**
* @param array<string, mixed> $profilePayload
*/
protected function profileHasInput(array $profilePayload): bool
{
return collect($profilePayload)
->except(['show_stats', 'tax_exempt', 'disable_footer_code'])
->filter(fn ($value): bool => filled($value))
->isNotEmpty()
|| (bool) $profilePayload['show_stats']
|| (bool) $profilePayload['tax_exempt']
|| (bool) $profilePayload['disable_footer_code'];
}
protected function persistUserLinks(): void
{
$user = User::query()->find($this->id);
if (! $user) {
return;
}
$companySyncPayload = [];
foreach ($this->linkedCompanyIds as $companyId) {
$companySyncPayload[(int) $companyId] = [
'role' => $this->companyRoles[(int) $companyId] ?? 'member',
];
}
$user->companies()->sync($companySyncPayload);
$user->contacts()->sync($this->linkedContactIds);
}
protected function billingIsComplete(): bool
{
return filled($this->billing['name'])
&& filled($this->billing['address1'])
&& filled($this->billing['postal_code'])
&& filled($this->billing['city'])
&& filled($this->billing['country_code']);
}
}; ?>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Benutzer · Bearbeiten') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ strtoupper($portal) }}</span>
@if ($isActive)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ $name ?: __('Benutzer bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ $email }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="filled" icon="arrow-left" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
@if ($notification)
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 4000)" x-show="show" x-transition
class="rounded-[5px] border-l-[3px] px-4 py-3 text-[12.5px]"
style="border-color: var(--color-ok); background: color-mix(in oklab, var(--color-ok) 10%, var(--color-bg)); color: var(--color-ink);">
{{ $notification }}
</div>
@endif
@php
$hasProfileInput = collect($profile)
->except(['show_stats', 'tax_exempt', 'disable_footer_code'])
->filter(fn ($value) => filled($value))
->isNotEmpty()
|| (bool) $profile['show_stats']
|| (bool) $profile['tax_exempt']
|| (bool) $profile['disable_footer_code'];
$billingComplete = filled($billing['name'])
&& filled($billing['address1'])
&& filled($billing['postal_code'])
&& filled($billing['city'])
&& filled($billing['country_code']);
@endphp
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Bearbeitungsflow') }}</span>
<div class="flex flex-wrap gap-1.5">
<a href="#account"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Account') }}</a>
<a href="#legacy-profile"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Profil') }}</a>
<a href="#company-links"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Firmen') }}</a>
<a href="#contact-links"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Kontakte') }}</a>
<a href="#billing-address"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Rechnung') }}</a>
</div>
</div>
<div class="p-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0 mb-4">
{{ __('Die wichtigsten Pflegebereiche sind hier zusammengefasst. Springe direkt in den Abschnitt, der noch Arbeit braucht.') }}
</p>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Account') }}</div>
<div class="flex flex-wrap gap-2">
@if ($isActive)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
<span class="badge hub">{{ strtoupper($portal) }}</span>
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Legacy-Profil') }}</div>
@if ($hasProfileInput)
<span class="badge ok dot">{{ __('Profil vorhanden') }}</span>
@else
<span class="badge warn dot">{{ __('Profil fehlt') }}</span>
@endif
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Verknüpfungen') }}</div>
<div class="flex flex-wrap gap-2">
<span class="badge {{ count($linkedCompanyIds) > 0 ? 'ok' : 'warn' }} dot">
{{ trans_choice(':count Firma|:count Firmen', count($linkedCompanyIds), ['count' => count($linkedCompanyIds)]) }}
</span>
<span class="badge hub">
{{ trans_choice(':count Kontakt|:count Kontakte', count($contactForms), ['count' => count($contactForms)]) }}
</span>
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Rechnungsadresse') }}</div>
@if ($billingComplete)
<span class="badge ok dot">{{ __('Vollständig') }}</span>
@else
<span class="badge warn dot">{{ __('Unvollständig') }}</span>
@endif
</div>
</div>
</div>
</article>
<flux:card id="account">
<flux:heading size="lg" class="mb-4">{{ __('Basisdaten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Name') }}</flux:label>
<flux:input wire:model="name" />
<flux:error name="name" />
</flux:field>
<flux:field>
<flux:label>{{ __('E-Mail') }}</flux:label>
<flux:input wire:model="email" type="email" />
<flux:error name="email" />
</flux:field>
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
<flux:error name="portal" />
</flux:field>
<flux:field>
<flux:label>{{ __('Registrierungstyp') }}</flux:label>
<flux:select wire:model="registrationType">
@foreach($registrationTypeOptions as $registrationTypeOption)
<option value="{{ $registrationTypeOption->value }}">{{ $registrationTypeOption->label() }}</option>
@endforeach
</flux:select>
<flux:error name="registrationType" />
</flux:field>
</div>
<div class="mt-4 flex flex-wrap gap-6">
<flux:checkbox wire:model="isActive" label="{{ __('Aktiv') }}" />
<flux:checkbox wire:model="isSuperAdmin" label="{{ __('Super Admin') }}" />
</div>
</flux:card>
<flux:card id="legacy-profile">
<flux:heading size="lg" class="mb-4">{{ __('Legacy-Profil') }}</flux:heading>
<flux:text class="mb-4 text-sm text-zinc-500">
{{ __('Diese Daten stammen aus sf_guard_user_profile und sind direkt mit dem Benutzer verknüpft.') }}
</flux:text>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Anrede') }}</flux:label>
<flux:select wire:model="profile.salutation_key">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($salutations as $key => $labels)
<option value="{{ $key }}">{{ $labels['de'] ?? $key }}</option>
@endforeach
</flux:select>
<flux:error name="profile.salutation_key" />
</flux:field>
<flux:field>
<flux:label>{{ __('Titel') }}</flux:label>
<flux:input wire:model="profile.title" />
<flux:error name="profile.title" />
</flux:field>
<flux:field>
<flux:label>{{ __('Vorname') }}</flux:label>
<flux:input wire:model="profile.first_name" />
<flux:error name="profile.first_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Nachname') }}</flux:label>
<flux:input wire:model="profile.last_name" />
<flux:error name="profile.last_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="profile.phone" />
<flux:error name="profile.phone" />
</flux:field>
<flux:field>
<flux:label>{{ __('Geburtsdatum') }}</flux:label>
<flux:date-picker wire:model="profile.birthdate" type="input" placeholder="" clearable />
<flux:error name="profile.birthdate" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Adresse') }}</flux:label>
<flux:textarea wire:model="profile.address" rows="3" />
<flux:error name="profile.address" />
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }}</flux:label>
<flux:select wire:model="profile.country_code">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($countries as $countryCode => $countryName)
<option value="{{ $countryCode }}">{{ $countryName }}</option>
@endforeach
</flux:select>
<flux:error name="profile.country_code" />
</flux:field>
<flux:field>
<flux:label>{{ __('Backlink-URL') }}</flux:label>
<flux:input wire:model="profile.backlink_url" />
<flux:error name="profile.backlink_url" />
</flux:field>
<flux:field>
<flux:label>{{ __('Validierungsdatum') }}</flux:label>
<flux:date-picker wire:model="profile.validation_date" type="input" placeholder="" clearable />
<flux:error name="profile.validation_date" />
</flux:field>
<flux:field>
<flux:label>{{ __('Vertragsdatum') }}</flux:label>
<flux:date-picker wire:model="profile.contract_date" type="input" placeholder="" clearable />
<flux:error name="profile.contract_date" />
</flux:field>
<flux:field>
<flux:label>{{ __('USt-IdNr.') }}</flux:label>
<flux:input wire:model="profile.tax_id_number" />
<flux:error name="profile.tax_id_number" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Steuerbefreiungsgrund') }}</flux:label>
<flux:textarea wire:model="profile.tax_exempt_reason" rows="2" />
<flux:error name="profile.tax_exempt_reason" />
</flux:field>
</div>
<div class="mt-4 flex flex-wrap gap-6">
<flux:checkbox wire:model="profile.show_stats" label="{{ __('Statistiken anzeigen') }}" />
<flux:checkbox wire:model="profile.tax_exempt" label="{{ __('Steuerbefreit') }}" />
<flux:checkbox wire:model="profile.disable_footer_code" label="{{ __('Footer-Code deaktivieren') }}" />
</div>
</flux:card>
<flux:card id="roles">
<flux:heading size="lg" class="mb-4">{{ __('Rollenzuweisung') }}</flux:heading>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach($availableRoles as $role)
<flux:checkbox wire:model="selectedRoles" value="{{ $role->name }}" label="{{ $role->name }}" />
@endforeach
</div>
<flux:error name="selectedRoles" class="mt-4" />
</flux:card>
<flux:card id="company-links">
<flux:heading size="lg" class="mb-4">{{ __('Firmenverknüpfung') }}</flux:heading>
<flux:text class="mb-4 text-sm text-zinc-500">
{{ __('Hier verknüpfst du den Benutzer mit Firmen. Die verknüpften Firmenkontakte können darunter direkt gepflegt werden.') }}
</flux:text>
<div class="flex gap-2">
<flux:select
wire:model.live="selectedLookupCompanyId"
variant="combobox"
:filter="false"
placeholder="{{ __('Firma suchen und auswählen…') }}"
class="min-w-0 flex-1"
>
<x-slot name="input">
<flux:select.input
wire:model.live.debounce.300ms="companyLookup"
placeholder="{{ __('Name, Slug oder E-Mail…') }}"
/>
</x-slot>
@foreach($companyLookupResults as $company)
<flux:select.option :value="$company->id" wire:key="{{ $company->id }}">
{{ $company->name }}@if($company->email)<span class="ml-1 text-zinc-400">· {{ $company->email }}</span>@endif
</flux:select.option>
@endforeach
<x-slot name="empty">
<flux:select.option.empty>
@if(blank(trim($companyLookup)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Keine Firma gefunden.') }}
@endif
</flux:select.option.empty>
</x-slot>
</flux:select>
<flux:button
type="button"
size="sm"
variant="filled"
icon="x-mark"
wire:click="clearCompanyLookup"
title="{{ __('Firmensuche zurücksetzen') }}"
/>
</div>
<div class="mt-4 space-y-3">
@forelse($linkedCompanies as $company)
<div class="grid gap-3 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700 sm:grid-cols-[1fr,auto,auto] sm:items-center">
<div>
<flux:text weight="medium">{{ $company->name }}</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $company->slug }}</flux:text>
</div>
<flux:select wire:model="companyRoles.{{ $company->id }}">
<option value="member">{{ __('Member') }}</option>
<option value="responsible">{{ __('Responsible') }}</option>
<option value="owner">{{ __('Owner') }}</option>
</flux:select>
<flux:button type="button" variant="filled" icon="x-mark" wire:click="removeLinkedCompany({{ $company->id }})">
{{ __('Entfernen') }}
</flux:button>
</div>
@empty
<flux:text class="text-sm text-zinc-500">
{{ __('Aktuell sind keine Firmen verknüpft.') }}
</flux:text>
@endforelse
</div>
</flux:card>
<flux:card id="contact-links">
<flux:heading size="lg" class="mb-4">{{ __('Kontakte der verknüpften Firmen') }}</flux:heading>
<div class="mb-4 flex gap-2">
<flux:select
wire:model.live="selectedLookupContactId"
variant="combobox"
:filter="false"
placeholder="{{ __('Kontakt suchen und auswählen…') }}"
class="min-w-0 flex-1"
>
<x-slot name="input">
<flux:select.input
wire:model.live.debounce.300ms="contactLookup"
placeholder="{{ __('Name oder E-Mail…') }}"
/>
</x-slot>
@foreach($contactLookupResults as $contactResult)
@php
$contactResultName = trim(($contactResult->first_name ?? '').' '.($contactResult->last_name ?? '')) ?: __('Kontakt ohne Name');
@endphp
<flux:select.option :value="$contactResult->id" wire:key="{{ $contactResult->id }}">
{{ $contactResultName }}
<span class="ml-1 text-zinc-400">
@if($contactResult->email)· {{ $contactResult->email }} @endif
· {{ $contactResult->company?->name ?? __('Unbekannte Firma') }}
</span>
</flux:select.option>
@endforeach
<x-slot name="empty">
<flux:select.option.empty>
@if(blank(trim($contactLookup)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Kein Kontakt gefunden.') }}
@endif
</flux:select.option.empty>
</x-slot>
</flux:select>
<flux:button
type="button"
size="sm"
variant="filled"
icon="x-mark"
wire:click="clearContactLookup"
title="{{ __('Kontaktsuche zurücksetzen') }}"
/>
</div>
@forelse($contactForms as $index => $contactForm)
<div class="mb-4 rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="mb-3 flex items-center justify-between gap-2">
<flux:text class="text-sm text-zinc-500">
{{ __('Firma') }}: {{ $contactForm['company_name'] }}
</flux:text>
<flux:button
size="sm"
variant="filled"
icon="x-mark"
type="button"
wire:click="removeLinkedContact({{ $contactForm['id'] }})"
>
{{ __('Entfernen') }}
</flux:button>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Vorname') }}</flux:label>
<flux:input wire:model="contactForms.{{ $index }}.first_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Nachname') }}</flux:label>
<flux:input wire:model="contactForms.{{ $index }}.last_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('E-Mail') }}</flux:label>
<flux:input wire:model="contactForms.{{ $index }}.email" type="email" />
</flux:field>
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="contactForms.{{ $index }}.phone" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Rolle / Verantwortlichkeit') }}</flux:label>
<flux:input wire:model="contactForms.{{ $index }}.responsibility" />
</flux:field>
</div>
</div>
@empty
<flux:text class="text-sm text-zinc-500">
{{ __('Zu den aktuell verknüpften Firmen sind keine Kontakte vorhanden.') }}
</flux:text>
@endforelse
</flux:card>
<flux:card id="billing-address">
<flux:heading size="lg" class="mb-4">{{ __('Rechnungsadresse') }}</flux:heading>
<div class="grid gap-3 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Anrede') }}</flux:label>
<flux:select wire:model="billing.salutation_key">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($salutations as $key => $labels)
<option value="{{ $key }}">{{ $labels['de'] ?? $key }}</option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Titel') }}</flux:label>
<flux:input wire:model="billing.title" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Rechnungsname') }}</flux:label>
<flux:input wire:model="billing.name" />
<flux:error name="billing.name" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Adresse Zeile 1') }}</flux:label>
<flux:input wire:model="billing.address1" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Adresse Zeile 2') }}</flux:label>
<flux:input wire:model="billing.address2" />
</flux:field>
<flux:field>
<flux:label>{{ __('PLZ') }}</flux:label>
<flux:input wire:model="billing.postal_code" />
</flux:field>
<flux:field>
<flux:label>{{ __('Stadt') }}</flux:label>
<flux:input wire:model="billing.city" />
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }}</flux:label>
<flux:select wire:model="billing.country_code">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($countries as $countryCode => $countryName)
<option value="{{ $countryCode }}">{{ $countryName }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
</flux:card>
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="filled" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
{{ __('Speichern') }}
</flux:button>
</div>
</article>
</form>