presseportale/resources/views/livewire/customer/profile.blade.php
Kevin Adametz 9b47296cea
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Rebrand Hub+Flux
2026-05-20 15:44:15 +02:00

483 lines
20 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\Models\Company;
use App\Models\Profile;
use App\Models\User;
use App\Services\Image\ImageService;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Component
{
use WithFileUploads;
public string $name = '';
public string $language = 'de';
public string $salutationKey = 'none';
public string $firstName = '';
public string $lastName = '';
public string $title = '';
public string $phone = '';
public string $address = '';
public string $countryCode = 'DE';
public string $backlinkUrl = '';
public bool $showStats = false;
public bool $disableFooterCode = false;
public string $taxIdNumber = '';
public string $billingName = '';
public string $billingAddress1 = '';
public string $billingAddress2 = '';
public string $billingPostalCode = '';
public string $billingCity = '';
public string $billingCountryCode = 'DE';
public ?int $editableCompanyId = null;
public string $companyName = '';
public string $companyAddress = '';
public string $companyEmail = '';
public string $companyPhone = '';
public string $companyWebsite = '';
public string $companyCountryCode = 'DE';
public bool $companyDisableFooterCode = false;
public $companyLogo = null;
public bool $removeCompanyLogo = false;
public function mount(): void
{
$user = auth()->user();
$profile = $user->profile;
$this->name = (string) $user->name;
$this->language = $user->language ?? 'de';
$this->salutationKey = (string) ($profile->salutation_key ?? 'none');
$this->firstName = (string) ($profile?->first_name ?? '');
$this->lastName = (string) ($profile?->last_name ?? '');
$this->title = (string) ($profile?->title ?? '');
$this->phone = (string) ($profile?->phone ?? '');
$this->address = (string) ($profile?->address ?? '');
$this->countryCode = (string) ($profile?->country_code ?? 'DE');
$this->backlinkUrl = (string) ($profile?->backlink_url ?? '');
$this->showStats = (bool) ($profile?->show_stats ?? false);
$this->disableFooterCode = (bool) ($profile?->disable_footer_code ?? false);
$this->taxIdNumber = (string) ($profile?->tax_id_number ?? '');
$billingAddress = $user->billingAddress;
$this->billingName = (string) ($billingAddress?->name ?? $user->name);
$this->billingAddress1 = (string) ($billingAddress?->address1 ?? '');
$this->billingAddress2 = (string) ($billingAddress?->address2 ?? '');
$this->billingPostalCode = (string) ($billingAddress?->postal_code ?? '');
$this->billingCity = (string) ($billingAddress?->city ?? '');
$this->billingCountryCode = (string) ($billingAddress?->country_code ?? 'DE');
$this->loadEditableCompany();
}
public function selectCompany(int $companyId): void
{
$this->editableCompanyId = $companyId;
$this->loadEditableCompany();
}
public function saveProfile(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:120'],
'language' => ['required', Rule::in(['de', 'en'])],
'salutationKey' => ['required', Rule::in(array_keys((array) config('salutations.items', [])))],
'firstName' => ['nullable', 'string', 'max:80'],
'lastName' => ['nullable', 'string', 'max:80'],
'title' => ['nullable', 'string', 'max:80'],
'phone' => ['nullable', 'string', 'max:40'],
'address' => ['nullable', 'string', 'max:1000'],
'countryCode' => ['nullable', 'string', 'size:2', Rule::in(array_keys((array) config('countries.items', [])))],
'backlinkUrl' => ['nullable', 'url', 'max:255'],
'taxIdNumber' => ['nullable', 'string', 'max:255'],
'billingName' => ['nullable', 'string', 'max:255'],
'billingAddress1' => ['nullable', 'string', 'max:255'],
'billingAddress2' => ['nullable', 'string', 'max:255'],
'billingPostalCode' => ['nullable', 'string', 'max:20'],
'billingCity' => ['nullable', 'string', 'max:120'],
'billingCountryCode' => ['nullable', 'string', 'size:2', Rule::in(array_keys((array) config('countries.items', [])))],
]);
if ($this->billingHasInput() && ! $this->billingIsComplete()) {
throw ValidationException::withMessages([
'billingName' => __('Bitte füllen Sie für eine Rechnungsadresse Name, Adresse, PLZ, Ort und Land aus.'),
]);
}
/** @var User $user */
$user = auth()->user();
$user->forceFill([
'name' => $validated['name'],
'language' => $validated['language'],
])->save();
$user->profile()->updateOrCreate(
['user_id' => $user->id],
[
'salutation_key' => $validated['salutationKey'],
'first_name' => $validated['firstName'] ?: null,
'last_name' => $validated['lastName'] ?: null,
'title' => $validated['title'] ?: null,
'phone' => $validated['phone'] ?: null,
'address' => $validated['address'] ?: null,
'country_code' => $validated['countryCode'] ?: null,
'backlink_url' => $validated['backlinkUrl'] ?: null,
'show_stats' => $this->showStats,
'disable_footer_code' => $this->disableFooterCode,
'tax_id_number' => $validated['taxIdNumber'] ?: null,
]
);
if (! $this->billingHasInput()) {
$user->billingAddress()->delete();
} else {
$user->billingAddress()->updateOrCreate(
['user_id' => $user->id],
[
'salutation_key' => $validated['salutationKey'] !== 'none' ? $validated['salutationKey'] : null,
'title' => $validated['title'] ?: null,
'name' => $validated['billingName'],
'address1' => $validated['billingAddress1'],
'address2' => $validated['billingAddress2'] ?: null,
'postal_code' => $validated['billingPostalCode'],
'city' => $validated['billingCity'],
'country_code' => $validated['billingCountryCode'],
],
);
}
session()->flash('profile-status', __('Profil gespeichert.'));
}
public function saveCompany(ImageService $imageService): void
{
if (! $this->editableCompanyId) {
return;
}
$company = $this->resolveEditableCompany($this->editableCompanyId);
if (! $company) {
throw ValidationException::withMessages([
'companyName' => __('Diese Firma kann von Ihnen nicht bearbeitet werden.'),
]);
}
$this->authorize('update', $company);
$validated = $this->validate([
'companyName' => ['required', 'string', 'max:255'],
'companyAddress' => ['nullable', 'string', 'max:1000'],
'companyEmail' => ['nullable', 'email', 'max:190'],
'companyPhone' => ['nullable', 'string', 'max:40'],
'companyWebsite' => ['nullable', 'url', 'max:190'],
'companyCountryCode' => ['nullable', 'string', 'size:2', Rule::in(array_keys((array) config('countries.items', [])))],
'companyLogo' => ['nullable', 'image', 'max:'.(int) (ImageService::MAX_LOGO_BYTES / 1024)],
]);
$company->fill([
'name' => $validated['companyName'],
'address' => $validated['companyAddress'] ?: null,
'email' => $validated['companyEmail'] ?: null,
'phone' => $validated['companyPhone'] ?: null,
'website' => $validated['companyWebsite'] ?: null,
'country_code' => $validated['companyCountryCode'] ?: null,
'disable_footer_code' => $this->companyDisableFooterCode,
]);
if ($this->removeCompanyLogo) {
$imageService->deleteCompanyLogo($company->logo_path, $company->logo_variants);
$company->logo_path = null;
$company->logo_variants = null;
}
if ($this->companyLogo) {
$imageService->deleteCompanyLogo($company->logo_path, $company->logo_variants);
$stored = $imageService->storeCompanyLogo(
$this->companyLogo,
$company->portal?->value ?? 'presseecho',
$company->id,
);
$company->logo_path = $stored['path'];
$company->logo_variants = $stored['variants'];
}
$company->save();
$this->companyLogo = null;
$this->removeCompanyLogo = false;
session()->flash('company-status', __('Firmendaten gespeichert.'));
}
public function with(): array
{
$user = auth()->user();
$companies = $user->companies()
->withPivot('role')
->orderBy('name')
->get(['companies.id', 'companies.name', 'companies.portal', 'companies.owner_user_id']);
return [
'user' => $user,
'companies' => $companies,
'salutations' => collect((array) config('salutations.items', []))
->map(fn (array $labels) => $labels[$user->language] ?? $labels['de'] ?? '')
->all(),
'countries' => (array) config('countries.items', []),
'editableCompany' => $this->editableCompanyId
? $this->resolveEditableCompany($this->editableCompanyId)
: null,
];
}
private function loadEditableCompany(): void
{
/** @var User $user */
$user = auth()->user();
$editable = Company::query()
->where(function ($query) use ($user): void {
$query->where('owner_user_id', $user->id)
->orWhereHas('users', fn ($q) => $q->whereKey($user->id)
->whereIn('company_user.role', ['owner', 'responsible']));
})
->orderBy('name');
$company = $this->editableCompanyId
? $editable->whereKey($this->editableCompanyId)->first()
: $editable->first();
if (! $company) {
$this->editableCompanyId = null;
return;
}
$this->editableCompanyId = $company->id;
$this->companyName = (string) $company->name;
$this->companyAddress = (string) ($company->address ?? '');
$this->companyEmail = (string) ($company->email ?? '');
$this->companyPhone = (string) ($company->phone ?? '');
$this->companyWebsite = (string) ($company->website ?? '');
$this->companyCountryCode = (string) ($company->country_code ?? 'DE');
$this->companyDisableFooterCode = (bool) $company->disable_footer_code;
}
private function resolveEditableCompany(int $companyId): ?Company
{
/** @var User $user */
$user = auth()->user();
return Company::query()
->where('id', $companyId)
->where(function ($query) use ($user): void {
$query->where('owner_user_id', $user->id)
->orWhereHas('users', fn ($q) => $q->whereKey($user->id)
->whereIn('company_user.role', ['owner', 'responsible']));
})
->first();
}
public function billingHasInput(): bool
{
return filled($this->billingName)
|| filled($this->billingAddress1)
|| filled($this->billingAddress2)
|| filled($this->billingPostalCode)
|| filled($this->billingCity);
}
public function billingIsComplete(): bool
{
return filled($this->billingName)
&& filled($this->billingAddress1)
&& filled($this->billingPostalCode)
&& filled($this->billingCity)
&& filled($this->billingCountryCode);
}
}; ?>
<div 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-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Profil') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Mein Profil') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Hier pflegen Sie Ihre persönlichen Konto- und Profildaten. Firmendaten verwalten Sie direkt in der jeweiligen Firma.') }}
</p>
</div>
</header>
@if (session('profile-status'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('profile-status') }}
</div>
@endif
<form wire:submit="saveProfile" class="space-y-6">
<div class="grid gap-6 lg:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Konto') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="name" :label="__('Name')" required />
<flux:input value="{{ $user->email }}" :label="__('E-Mail')" disabled :description="__('Änderung über Konto-Sicherheit möglich.')" />
<flux:select wire:model="language" :label="__('Sprache')">
<option value="de">Deutsch</option>
<option value="en">English</option>
</flux:select>
</div>
</article>
<article class="panel" id="profil">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Profil') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:select wire:model="salutationKey" :label="__('Anrede')">
@foreach ($salutations as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</flux:select>
<flux:input wire:model="title" :label="__('Titel')" placeholder="Dr." />
<flux:input wire:model="firstName" :label="__('Vorname')" />
<flux:input wire:model="lastName" :label="__('Nachname')" />
<flux:input wire:model="phone" :label="__('Telefon')" />
<flux:input wire:model="backlinkUrl" :label="__('Backlink-URL')" placeholder="https://..." />
<flux:checkbox wire:model="showStats" :label="__('Statistiken in Pressemitteilungen anzeigen')" class="sm:col-span-2" />
<flux:checkbox wire:model="disableFooterCode" :label="__('Footer-Code in Pressemitteilungen deaktivieren')" class="sm:col-span-2" />
</div>
</article>
</div>
<article class="panel" id="rechnungsadresse">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechnungsadresse') }}</span>
</div>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Diese Angaben werden für künftige Rechnungen verwendet. Eine vollständige Rechnungsadresse benötigt Name, Adresse, PLZ, Ort und Land.') }}
</p>
@if (! $this->billingIsComplete())
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
{{ __('Rechnungsadresse noch unvollständig. Bitte ergänzen Sie die Pflichtangaben, bevor neue Buchungen sauber abgerechnet werden können.') }}
</div>
</div>
@endif
<div class="grid gap-4 sm:grid-cols-2">
<flux:input wire:model="billingName" :label="__('Rechnungsname')" class="sm:col-span-2" />
<flux:input wire:model="billingAddress1" :label="__('Adresse Zeile 1')" class="sm:col-span-2" />
<flux:input wire:model="billingAddress2" :label="__('Adresse Zeile 2')" class="sm:col-span-2" />
<flux:input wire:model="billingPostalCode" :label="__('PLZ')" />
<flux:input wire:model="billingCity" :label="__('Ort')" />
<flux:select wire:model="billingCountryCode" :label="__('Land')">
@foreach ($countries as $code => $name)
<option value="{{ $code }}">{{ $name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="taxIdNumber" :label="__('USt-ID')" />
<flux:error name="billingName" class="sm:col-span-2" />
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
<div class="p-5 flex justify-end">
<flux:button type="submit" variant="primary">{{ __('Profil speichern') }}</flux:button>
</div>
</article>
</form>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zugeordnete Firmen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $companies->count() }} {{ __('Einträge') }}
</span>
</div>
@forelse ($companies as $company)
<div class="flex flex-col gap-2 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-0 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1 min-w-0">
<p class="text-[13px] font-semibold text-[color:var(--color-ink)] m-0">{{ $company->name }}</p>
<div class="flex flex-wrap items-center gap-2">
<span class="badge hub">{{ $company->portal?->label() ?? '' }}</span>
<span class="badge hub">{{ $company->pivot->role ?? 'member' }}</span>
@if ($company->owner_user_id === $user->id)
<span class="badge ok">{{ __('Eigentümer') }}</span>
@endif
</div>
</div>
@if ($company->owner_user_id === $user->id || in_array($company->pivot->role, ['owner', 'responsible'], true))
<flux:button size="sm" variant="ghost" icon="arrow-right" href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate>
{{ __('Firma verwalten') }}
</flux:button>
@else
<flux:button size="sm" variant="ghost" icon="arrow-right" href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate>
{{ __('Firma öffnen') }}
</flux:button>
@endif
</div>
@empty
<div class="p-5 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine Firmen zugeordnet. Bitte wenden Sie sich an den Administrator.') }}
</div>
@endforelse
</article>
</div>