355 lines
16 KiB
PHP
355 lines
16 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Component
|
|
{
|
|
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 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 ?? '');
|
|
$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');
|
|
}
|
|
|
|
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'],
|
|
// USt-ID auch an der Rechnungsadresse pflegen — sie wird
|
|
// pro Rechnung eingefroren und bestimmt die Steuer
|
|
// (EU-Befreiung nur mit gültiger USt-ID).
|
|
'vat_id' => $validated['taxIdNumber'] ?: null,
|
|
],
|
|
);
|
|
}
|
|
|
|
session()->flash('profile-status', __('Profil gespeichert.'));
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return [
|
|
'user' => $user,
|
|
'salutations' => collect((array) config('salutations.items', []))
|
|
->map(fn (array $labels) => $labels[$user->language] ?? $labels['de'] ?? '')
|
|
->all(),
|
|
'countries' => (array) config('countries.items', []),
|
|
'billingComplete' => $this->billingIsComplete(),
|
|
];
|
|
}
|
|
|
|
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="page-header">
|
|
<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-[680px] text-[color:var(--color-ink-2)]">
|
|
{{ __('Hier verwalten Sie Ihre Rechnungsadresse und persönlichen Profileinstellungen. Firmendaten liegen separat in der Firmenverwaltung.') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
<flux:button size="sm" variant="filled" icon="building-office" href="{{ route('me.press-kits.index') }}" wire:navigate>
|
|
{{ __('Firmen verwalten') }}
|
|
</flux:button>
|
|
</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">
|
|
<article class="panel" id="rechnungsadresse">
|
|
<div class="panel-head">
|
|
<span class="section-eyebrow">{{ __('Rechnungsadresse') }}</span>
|
|
@if ($billingComplete)
|
|
<span class="badge ok dot">{{ __('vollständig') }}</span>
|
|
@else
|
|
<span class="badge warn dot">{{ __('unvollständig') }}</span>
|
|
@endif
|
|
</div>
|
|
<div class="p-5 grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
|
<div class="space-y-3">
|
|
<p class="text-[13px] leading-[1.55] text-[color:var(--color-ink-2)] m-0">
|
|
{{ __('Diese Adresse ist die maßgebliche Grundlage für Rechnungen und künftige Buchungen.') }}
|
|
</p>
|
|
<p class="text-[12.5px] leading-[1.55] text-[color:var(--color-ink-3)] m-0">
|
|
{{ __('Pflichtangaben sind Rechnungsname, Adresse, PLZ, Ort und Land. Die USt-ID ist optional.') }}
|
|
</p>
|
|
|
|
@if (! $billingComplete)
|
|
<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">
|
|
{{ __('Bitte ergänzen Sie die Rechnungsadresse, damit neue Buchungen sauber abgerechnet werden können.') }}
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
|
|
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 mt-0.5" />
|
|
<div class="flex-1">
|
|
{{ __('Ihre Rechnungsadresse ist vollständig hinterlegt.') }}
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<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>
|
|
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)] flex justify-end">
|
|
<flux:button type="submit" variant="primary">
|
|
{{ __('Rechnungsadresse speichern') }}
|
|
</flux:button>
|
|
</div>
|
|
</article>
|
|
|
|
<div class="grid gap-6 lg:grid-cols-2">
|
|
<article class="panel" id="profil">
|
|
<div class="panel-head">
|
|
<span class="section-eyebrow">{{ __('Profileinstellungen') }}</span>
|
|
</div>
|
|
<div class="p-5 grid gap-4 sm:grid-cols-2">
|
|
<flux:input wire:model="name" :label="__('Anzeigename')" required class="sm:col-span-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:textarea wire:model="address" :label="__('Adresse')" class="sm:col-span-2" />
|
|
<flux:select wire:model="countryCode" :label="__('Land')" class="sm:col-span-2">
|
|
@foreach ($countries as $code => $name)
|
|
<option value="{{ $code }}">{{ $name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</div>
|
|
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)] flex justify-end">
|
|
<flux:button type="submit" variant="primary">
|
|
{{ __('Profil speichern') }}
|
|
</flux:button>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="panel">
|
|
<div class="panel-head">
|
|
<span class="section-eyebrow">{{ __('Konto & Sicherheit') }}</span>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<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 class="pt-3 border-t border-[color:var(--color-bg-rule)]">
|
|
<flux:button size="sm" variant="filled" icon="shield-check" href="{{ route('me.security') }}" wire:navigate>
|
|
{{ __('Konto-Sicherheit öffnen') }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<article class="panel">
|
|
<div class="panel-head">
|
|
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
|
|
</div>
|
|
<div class="p-5 grid gap-4 md:grid-cols-2">
|
|
<flux:switch
|
|
wire:model="showStats"
|
|
align="right"
|
|
:label="__('Statistiken anzeigen')"
|
|
:description="__('Statistiken und Kennzahlen in Ihren Pressemitteilungen anzeigen.')"
|
|
/>
|
|
<flux:switch
|
|
wire:model="disableFooterCode"
|
|
align="right"
|
|
:label="__('Footer-Code deaktivieren')"
|
|
:description="__('Automatische Footer-Codes in Pressemitteilungen für dieses Profil deaktivieren.')"
|
|
/>
|
|
</div>
|
|
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)] flex justify-end">
|
|
<flux:button type="submit" variant="primary">
|
|
{{ __('Einstellungen speichern') }}
|
|
</flux:button>
|
|
</div>
|
|
</article>
|
|
</form>
|
|
</div>
|