Phase 8 (Rest) + Umbauten vom 10./11.06.: - Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker, PressReleaseCoverImage-Resolver - Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen, Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise) - Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt), geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE) - Quota-Stub (users.press_release_quota) + monatlicher Reset-Command - Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout) KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans): - API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route durch denselben Funnel (Blacklist, Quota, Status-Log) - Klassifikation Rot/Gelb/Gruen asynchron (Queue classification, OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log - Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen -> Auto-Publish; Scheduler publiziert nur gruene faellige PMs - Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl. Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung - Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override Suite: 442 passed, 4 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1241 lines
51 KiB
PHP
1241 lines
51 KiB
PHP
<?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="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||
<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>
|