User-Panel-Restarbeiten: PM-Guard, Profil-Rework, USt-ID-Prüfung, Buchungspflicht-Adresse
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
036a53499f
commit
afcca34f91
25 changed files with 905 additions and 140 deletions
|
|
@ -201,7 +201,7 @@ new #[Layout('components.layouts.app'), Title('Neue Firma anlegen')] class exten
|
|||
</flux:field>
|
||||
|
||||
<flux:field class="sm:col-span-2">
|
||||
<flux:checkbox wire:model="disableFooterCode" :label="__('Footer-Code deaktivieren (z. B. wenn die Firma keine Quellenangabe haben möchte)')" />
|
||||
<flux:switch wire:model="disableFooterCode" :label="__('Footer-Code deaktivieren (z. B. wenn die Firma keine Quellenangabe haben möchte)')" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Services\Customer\CustomerCompanyContext;
|
|||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\Url;
|
||||
|
|
@ -313,29 +312,13 @@ new #[Layout('components.layouts.app'), Title('Meine Firmen')] class extends Com
|
|||
|
||||
public function fastLogoUrl(Company $company): ?string
|
||||
{
|
||||
if (blank($company->logo_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$logoPath = trim((string) $company->logo_path);
|
||||
|
||||
if (Str::startsWith($logoPath, ['http://', 'https://']) && blank($company->legacy_portal)) {
|
||||
return $logoPath;
|
||||
}
|
||||
|
||||
if (Str::startsWith($logoPath, '/storage/')) {
|
||||
return asset($logoPath);
|
||||
}
|
||||
|
||||
if (filled($company->legacy_portal)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! Str::startsWith($logoPath, ['http://', 'https://'])) {
|
||||
return asset('storage/'.ltrim($logoPath, '/'));
|
||||
}
|
||||
|
||||
return null;
|
||||
// Delegiert an die zentrale Auflösung inkl. der migrierten
|
||||
// Legacy-Pfade (company-logos/{portal}/{id}/…) — die frühere
|
||||
// „schnelle" Variante übersprang Legacy-Firmen komplett, wodurch
|
||||
// in der Übersicht trotz vorhandenem Logo nur die Initialen
|
||||
// erschienen. Die Existenz-Checks laufen auf dem lokalen Disk
|
||||
// und sind für 50 Karten pro Seite unkritisch.
|
||||
return $company->logoUrl();
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
|
|
@ -389,7 +372,7 @@ new #[Layout('components.layouts.app'), Title('Meine Firmen')] class extends Com
|
|||
$company->setAttribute('panel_meta_line', $this->metaLine($company));
|
||||
$company->setAttribute(
|
||||
'panel_last_press_release_short',
|
||||
$lastPublishedAt?->format('d.m.') ?? '—'
|
||||
$lastPublishedAt?->format('d.m.Y') ?? '—'
|
||||
);
|
||||
$company->setAttribute(
|
||||
'panel_last_press_release_date',
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
|
|||
<flux:error name="companyCountryCode" />
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model="companyDisableFooterCode" :label="__('Footer-Code deaktivieren')" />
|
||||
<flux:switch wire:model="companyDisableFooterCode" :label="__('Footer-Code deaktivieren')" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,22 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
|
||||
public string $placeholderVariant = '';
|
||||
|
||||
public bool $hasCompanies = true;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
$context = app(CustomerCompanyContext::class);
|
||||
|
||||
// Ohne Firma kein PM-Formular: statt eines leeren Editors, in dem
|
||||
// weder Firma wählbar noch Speichern möglich ist, erscheint eine
|
||||
// Meldung mit dem direkten Weg zur Firmen-Anlage.
|
||||
$this->hasCompanies = $context->companyCountFor($user) > 0;
|
||||
|
||||
if (! $this->hasCompanies) {
|
||||
return;
|
||||
}
|
||||
|
||||
$firstCompany = $context->selectedCompany($user) ?? $context->latestCompaniesFor($user, 1)->first();
|
||||
|
||||
if ($firstCompany) {
|
||||
|
|
@ -711,6 +723,33 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
</div>
|
||||
</header>
|
||||
|
||||
@if (! $hasCompanies)
|
||||
{{-- ============== KEINE FIRMA: MELDUNG STATT FORMULAR ============== --}}
|
||||
<article class="panel">
|
||||
<div class="p-8 flex flex-col items-center text-center gap-4">
|
||||
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center
|
||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.building-office class="size-6" />
|
||||
</div>
|
||||
<div class="space-y-1 max-w-[520px]">
|
||||
<h2 class="text-[16px] font-semibold text-[color:var(--color-ink)] m-0">
|
||||
{{ __('Ohne Firma kann keine Pressemitteilung angelegt werden.') }}
|
||||
</h2>
|
||||
<p class="text-[13px] leading-[1.55] text-[color:var(--color-ink-2)] m-0">
|
||||
{{ __('Jede Pressemitteilung erscheint im Namen einer Firma. Bitte legen Sie zuerst eine Firma an — danach können Sie hier direkt loslegen.') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 pt-1">
|
||||
<flux:button variant="primary" icon="plus" href="{{ route('me.press-kits.create') }}" wire:navigate>
|
||||
{{ __('Firma anlegen') }}
|
||||
</flux:button>
|
||||
<flux:button variant="filled" href="{{ route('me.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zur PM-Übersicht') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@else
|
||||
{{-- ============== 2-COLUMN GRID ============== --}}
|
||||
<div class="grid gap-6 pr-editor-layout">
|
||||
|
||||
|
|
@ -952,7 +991,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
— {{ __('Boilerplate aus Firma') }}
|
||||
</span>
|
||||
</span>
|
||||
<flux:checkbox
|
||||
<flux:switch
|
||||
wire:model.live="useBoilerplateOverride"
|
||||
:label="__('Für diese PM überschreiben')"
|
||||
/>
|
||||
|
|
@ -1334,4 +1373,5 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
:confirm-label="__('Zur Prüfung senden')"
|
||||
:quota-total="$quotaTotal"
|
||||
:quota-remaining="$quotaRemaining" />
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -901,7 +901,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
— {{ __('Boilerplate aus Firma') }}
|
||||
</span>
|
||||
</span>
|
||||
<flux:checkbox
|
||||
<flux:switch
|
||||
wire:model.live="useBoilerplateOverride"
|
||||
:label="__('Für diese PM überschreiben')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\VatIdCheckStatus;
|
||||
use App\Models\User;
|
||||
use App\Services\Billing\VatIdValidationService;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
|
|
@ -35,7 +36,13 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|
||||
public string $taxIdNumber = '';
|
||||
|
||||
public string $billingName = '';
|
||||
public string $billingSalutationKey = 'none';
|
||||
|
||||
public string $billingCompany = '';
|
||||
|
||||
public string $billingFirstName = '';
|
||||
|
||||
public string $billingLastName = '';
|
||||
|
||||
public string $billingAddress1 = '';
|
||||
|
||||
|
|
@ -47,6 +54,10 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|
||||
public string $billingCountryCode = 'DE';
|
||||
|
||||
public ?string $vatCheckStatus = null;
|
||||
|
||||
public ?string $vatCheckMessage = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
|
@ -68,17 +79,72 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
$this->taxIdNumber = (string) ($profile?->tax_id_number ?? '');
|
||||
|
||||
$billingAddress = $user->billingAddress;
|
||||
$this->billingName = (string) ($billingAddress?->name ?? '');
|
||||
$this->billingSalutationKey = (string) ($billingAddress?->salutation_key ?? 'none');
|
||||
$this->billingCompany = (string) ($billingAddress?->company ?? '');
|
||||
$this->billingFirstName = (string) ($billingAddress?->first_name ?? '');
|
||||
$this->billingLastName = (string) ($billingAddress?->last_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');
|
||||
|
||||
// Bestandsdaten vor der Feld-Trennung: `name` war eine freie
|
||||
// Empfängerzeile — einmalig in Vor-/Nachname aufteilen.
|
||||
if (blank($this->billingFirstName) && blank($this->billingLastName) && filled($billingAddress?->name)) {
|
||||
$parts = preg_split('/\s+/u', trim((string) $billingAddress->name)) ?: [];
|
||||
$this->billingLastName = (string) array_pop($parts);
|
||||
$this->billingFirstName = implode(' ', $parts);
|
||||
}
|
||||
|
||||
if (filled($this->taxIdNumber)) {
|
||||
$this->refreshVatCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persönliche Daten als Rechnungsempfänger übernehmen — löst die von
|
||||
* Kevin angemerkte Doppel-Eingabe auf, ohne die Datensätze zu koppeln.
|
||||
*/
|
||||
public function copyProfileToBilling(): void
|
||||
{
|
||||
$this->billingSalutationKey = $this->salutationKey;
|
||||
$this->billingFirstName = $this->firstName;
|
||||
$this->billingLastName = $this->lastName;
|
||||
|
||||
if (filled($this->countryCode)) {
|
||||
$this->billingCountryCode = $this->countryCode;
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedTaxIdNumber(): void
|
||||
{
|
||||
$this->refreshVatCheck();
|
||||
}
|
||||
|
||||
public function updatedBillingCountryCode(): void
|
||||
{
|
||||
$this->refreshVatCheck();
|
||||
}
|
||||
|
||||
private function refreshVatCheck(): void
|
||||
{
|
||||
if (blank($this->taxIdNumber)) {
|
||||
$this->vatCheckStatus = null;
|
||||
$this->vatCheckMessage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = app(VatIdValidationService::class)->check($this->taxIdNumber, $this->billingCountryCode);
|
||||
|
||||
$this->vatCheckStatus = $result['status']->value;
|
||||
$this->vatCheckMessage = $result['message'];
|
||||
}
|
||||
|
||||
public function saveProfile(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:120'],
|
||||
'language' => ['required', Rule::in(['de', 'en'])],
|
||||
'salutationKey' => ['required', Rule::in(array_keys((array) config('salutations.items', [])))],
|
||||
|
|
@ -90,18 +156,47 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
'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'],
|
||||
'billingSalutationKey' => ['required', Rule::in(array_keys((array) config('salutations.items', [])))],
|
||||
'billingCompany' => ['nullable', 'string', 'max:255'],
|
||||
'billingFirstName' => ['nullable', 'string', 'max:80'],
|
||||
'billingLastName' => ['nullable', 'string', 'max:80'],
|
||||
'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', [])))],
|
||||
];
|
||||
|
||||
// Sobald irgendein Rechnungsfeld gefüllt ist, werden die
|
||||
// Pflichtfelder einzeln eingefordert — die Meldung erscheint genau
|
||||
// einmal unter dem jeweils fehlenden Feld (vorher: eine generische
|
||||
// Sammelmeldung zusätzlich zur Feldmeldung).
|
||||
if ($this->billingHasInput()) {
|
||||
$rules['billingLastName'] = ['required', 'string', 'max:80'];
|
||||
$rules['billingAddress1'] = ['required', 'string', 'max:255'];
|
||||
$rules['billingPostalCode'] = ['required', 'string', 'max:20'];
|
||||
$rules['billingCity'] = ['required', 'string', 'max:120'];
|
||||
$rules['billingCountryCode'] = ['required', 'string', 'size:2', Rule::in(array_keys((array) config('countries.items', [])))];
|
||||
}
|
||||
|
||||
$validated = $this->validate($rules, attributes: [
|
||||
'billingLastName' => __('Nachname (Rechnung)'),
|
||||
'billingAddress1' => __('Straße und Hausnummer'),
|
||||
'billingPostalCode' => __('PLZ'),
|
||||
'billingCity' => __('Ort'),
|
||||
'billingCountryCode' => __('Land'),
|
||||
]);
|
||||
|
||||
if ($this->billingHasInput() && ! $this->billingIsComplete()) {
|
||||
throw ValidationException::withMessages([
|
||||
'billingName' => __('Bitte füllen Sie für eine Rechnungsadresse Name, Adresse, PLZ, Ort und Land aus.'),
|
||||
]);
|
||||
// USt-ID: hartes Format-Gate; die Online-Bestätigung (eVatR) bleibt
|
||||
// ein Hinweis und blockiert das Speichern nicht.
|
||||
if (filled($validated['taxIdNumber'])) {
|
||||
$this->refreshVatCheck();
|
||||
|
||||
if ($this->vatCheckStatus === VatIdCheckStatus::FormatInvalid->value) {
|
||||
$this->addError('taxIdNumber', (string) $this->vatCheckMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
|
|
@ -135,9 +230,12 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
$user->billingAddress()->updateOrCreate(
|
||||
['user_id' => $user->id],
|
||||
[
|
||||
'salutation_key' => $validated['salutationKey'] !== 'none' ? $validated['salutationKey'] : null,
|
||||
'title' => $validated['title'] ?: null,
|
||||
'name' => $validated['billingName'],
|
||||
'salutation_key' => $validated['billingSalutationKey'] !== 'none' ? $validated['billingSalutationKey'] : null,
|
||||
'company' => $validated['billingCompany'] ?: null,
|
||||
'first_name' => $validated['billingFirstName'] ?: null,
|
||||
'last_name' => $validated['billingLastName'] ?: null,
|
||||
// Zusammengesetzte Empfängerzeile für Rechnungs-Snapshots.
|
||||
'name' => trim($validated['billingFirstName'].' '.$validated['billingLastName']),
|
||||
'address1' => $validated['billingAddress1'],
|
||||
'address2' => $validated['billingAddress2'] ?: null,
|
||||
'postal_code' => $validated['billingPostalCode'],
|
||||
|
|
@ -170,7 +268,9 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|
||||
public function billingHasInput(): bool
|
||||
{
|
||||
return filled($this->billingName)
|
||||
return filled($this->billingCompany)
|
||||
|| filled($this->billingFirstName)
|
||||
|| filled($this->billingLastName)
|
||||
|| filled($this->billingAddress1)
|
||||
|| filled($this->billingAddress2)
|
||||
|| filled($this->billingPostalCode)
|
||||
|
|
@ -179,7 +279,7 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|
||||
public function billingIsComplete(): bool
|
||||
{
|
||||
return filled($this->billingName)
|
||||
return filled($this->billingLastName)
|
||||
&& filled($this->billingAddress1)
|
||||
&& filled($this->billingPostalCode)
|
||||
&& filled($this->billingCity)
|
||||
|
|
@ -191,7 +291,7 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="page-header">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
||||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · Profil') }}</span>
|
||||
</div>
|
||||
|
|
@ -199,7 +299,7 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
{{ __('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.') }}
|
||||
{{ __('Persönliche Daten, Rechnungsadresse und Konto-Einstellungen. Firmendaten liegen separat in der Firmenverwaltung.') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
|
|
@ -217,94 +317,44 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('checkout-notice'))
|
||||
<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">{{ session('checkout-notice') }}</div>
|
||||
</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>
|
||||
|
||||
{{-- ============== 1) PERSÖNLICHE DATEN + KONTO ============== --}}
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<article class="panel" id="profil">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Profileinstellungen') }}</span>
|
||||
<span class="section-eyebrow">{{ __('Persönliche Daten') }}</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 class="p-5 space-y-4">
|
||||
<p class="text-[12.5px] leading-[1.55] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Ihre Kontaktdaten für Ansprache und Rückfragen — unabhängig von der Rechnungsadresse unten.') }}
|
||||
</p>
|
||||
<div class="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="__('Kontaktadresse')" 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>
|
||||
</article>
|
||||
|
||||
|
|
@ -327,6 +377,121 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
</article>
|
||||
</div>
|
||||
|
||||
{{-- ============== 2) RECHNUNGSADRESSE ============== --}}
|
||||
<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">
|
||||
{{ __('Grundlage für Rechnungen und Pflicht für jede Tarif- oder Einzel-PM-Buchung. Die Daten werden bei der Buchung an Stripe übergeben.') }}
|
||||
</p>
|
||||
<p class="text-[12.5px] leading-[1.55] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Pflichtangaben: Nachname, Straße, PLZ, Ort und Land. Firmenname und USt-ID sind 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">
|
||||
{{ __('Ohne vollständige Rechnungsadresse können keine Tarife oder Einzel-PMs gebucht werden.') }}
|
||||
</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
|
||||
|
||||
<flux:button size="sm" variant="filled" icon="arrow-down-on-square" wire:click="copyProfileToBilling">
|
||||
{{ __('Persönliche Daten übernehmen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold tracking-[0.14em] uppercase text-[color:var(--color-ink-3)] mb-3">
|
||||
{{ __('Rechnungsempfänger') }}
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<flux:select wire:model="billingSalutationKey" :label="__('Anrede')">
|
||||
@foreach ($salutations as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input wire:model="billingCompany" :label="__('Firmenname (optional)')" />
|
||||
<flux:input wire:model="billingFirstName" :label="__('Vorname')" />
|
||||
<flux:input wire:model="billingLastName" :label="__('Nachname')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold tracking-[0.14em] uppercase text-[color:var(--color-ink-3)] mb-3">
|
||||
{{ __('Anschrift') }}
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<flux:input wire:model="billingAddress1" :label="__('Straße und Hausnummer')" class="sm:col-span-2" />
|
||||
<flux:input wire:model="billingAddress2" :label="__('Adresszusatz (optional)')" class="sm:col-span-2" />
|
||||
<flux:input wire:model="billingPostalCode" :label="__('PLZ')" />
|
||||
<flux:input wire:model="billingCity" :label="__('Ort')" />
|
||||
<flux:select wire:model.live="billingCountryCode" :label="__('Land')" class="sm:col-span-2">
|
||||
@foreach ($countries as $code => $name)
|
||||
<option value="{{ $code }}">{{ $name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold tracking-[0.14em] uppercase text-[color:var(--color-ink-3)] mb-3">
|
||||
{{ __('Steuern') }}
|
||||
</div>
|
||||
<flux:input
|
||||
wire:model.live.debounce.600ms="taxIdNumber"
|
||||
:label="__('USt-ID (optional)')"
|
||||
placeholder="DE123456789"
|
||||
:description="__('Für EU-Firmen außerhalb Deutschlands entfällt mit gültiger USt-ID die deutsche Umsatzsteuer (Reverse Charge).')"
|
||||
/>
|
||||
@if ($vatCheckMessage)
|
||||
@if ($vatCheckStatus === 'valid')
|
||||
<div class="mt-2 text-[12px] flex items-start gap-1.5 text-[color:var(--color-gain-deep)]">
|
||||
<flux:icon.check-circle class="size-[14px] flex-shrink-0 mt-0.5" />
|
||||
<span>{{ $vatCheckMessage }}</span>
|
||||
</div>
|
||||
@elseif (in_array($vatCheckStatus, ['invalid', 'format_invalid'], true))
|
||||
<div class="mt-2 text-[12px] flex items-start gap-1.5 text-[color:var(--color-err,#b91c1c)]">
|
||||
<flux:icon.x-circle class="size-[14px] flex-shrink-0 mt-0.5" />
|
||||
<span>{{ $vatCheckMessage }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-2 text-[12px] flex items-start gap-1.5 text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.information-circle class="size-[14px] flex-shrink-0 mt-0.5" />
|
||||
<span>{{ $vatCheckMessage }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</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">
|
||||
{{ __('Speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- ============== 3) EINSTELLUNGEN ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
|
||||
|
|
@ -336,18 +501,18 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
wire:model="showStats"
|
||||
align="right"
|
||||
:label="__('Statistiken anzeigen')"
|
||||
:description="__('Statistiken und Kennzahlen in Ihren Pressemitteilungen anzeigen.')"
|
||||
:description="__('Statistiken und Kennzahlen in Ihren Pressemitteilungen anzeigen. Greift mit dem Relaunch der Portal-Seiten.')"
|
||||
/>
|
||||
<flux:switch
|
||||
wire:model="disableFooterCode"
|
||||
align="right"
|
||||
:label="__('Footer-Code deaktivieren')"
|
||||
:description="__('Automatische Footer-Codes in Pressemitteilungen für dieses Profil deaktivieren.')"
|
||||
:description="__('Automatische Footer-Codes in Pressemitteilungen für dieses Profil deaktivieren. Greift mit dem Relaunch der Portal-Seiten.')"
|
||||
/>
|
||||
</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') }}
|
||||
{{ __('Speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue