presseportale/resources/views/livewire/admin/users/edit.blade.php
Kevin Adametz 5b8bdf4182
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
12-05-2026 Frontend dev
2026-05-12 18:32:33 +02:00

1224 lines
49 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-6">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="lg">{{ __('Benutzer bearbeiten') }}</flux:heading>
<flux:subheading>ID: {{ $id }}</flux:subheading>
</div>
<flux:badge color="zinc" size="sm">{{ strtoupper($portal) }}</flux:badge>
</div>
</flux:card>
@if($notification)
<div
x-data="{ show: true }"
x-init="setTimeout(() => show = false, 3000)"
x-show="show"
x-transition
class="rounded-md bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 text-sm text-green-800 dark:text-green-300"
>
{{ $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
<flux:card>
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<flux:heading size="lg">{{ __('Bearbeitungsflow') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Die wichtigsten Pflegebereiche sind hier zusammengefasst. Springe direkt in den Abschnitt, der noch Arbeit braucht.') }}
</flux:text>
</div>
<div class="flex flex-wrap gap-2">
<flux:button size="sm" variant="ghost" href="#account">{{ __('Account') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#legacy-profile">{{ __('Profil') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#company-links">{{ __('Firmen') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#contact-links">{{ __('Kontakte') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#billing-address">{{ __('Rechnung') }}</flux:button>
</div>
</div>
<div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Account') }}</flux:text>
<div class="mt-2 flex flex-wrap gap-2">
<flux:badge :color="$isActive ? 'green' : 'red'" size="sm">
{{ $isActive ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<flux:badge color="zinc" size="sm">{{ strtoupper($portal) }}</flux:badge>
</div>
</div>
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Legacy-Profil') }}</flux:text>
<div class="mt-2">
@if($hasProfileInput)
<flux:badge color="green" size="sm">{{ __('Profil vorhanden') }}</flux:badge>
@else
<flux:badge color="amber" size="sm">{{ __('Profil fehlt') }}</flux:badge>
@endif
</div>
</div>
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Verknüpfungen') }}</flux:text>
<div class="mt-2 flex flex-wrap gap-2">
<flux:badge :color="count($linkedCompanyIds) > 0 ? 'green' : 'amber'" size="sm">
{{ trans_choice(':count Firma|:count Firmen', count($linkedCompanyIds), ['count' => count($linkedCompanyIds)]) }}
</flux:badge>
<flux:badge :color="count($contactForms) > 0 ? 'blue' : 'zinc'" size="sm">
{{ trans_choice(':count Kontakt|:count Kontakte', count($contactForms), ['count' => count($contactForms)]) }}
</flux:badge>
</div>
</div>
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Rechnungsadresse') }}</flux:text>
<div class="mt-2">
@if($billingComplete)
<flux:badge color="green" size="sm">{{ __('Vollständig') }}</flux:badge>
@else
<flux:badge color="zinc" size="sm">{{ __('Unvollständig') }}</flux:badge>
@endif
</div>
</div>
</div>
</flux:card>
<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="ghost"
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="ghost" 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="ghost"
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="ghost"
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>
<flux:card>
<div class="flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Speichern') }}
</flux:button>
</div>
</flux:card>
</form>