22-05-2026 Optimierung der User und Admin Panels
This commit is contained in:
parent
d2ba22c0cf
commit
e8c47b7553
73 changed files with 10282 additions and 1546 deletions
|
|
@ -1,20 +1,14 @@
|
|||
<?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';
|
||||
|
|
@ -53,26 +47,6 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|
||||
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();
|
||||
|
|
@ -81,7 +55,7 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
$this->name = (string) $user->name;
|
||||
$this->language = $user->language ?? 'de';
|
||||
|
||||
$this->salutationKey = (string) ($profile->salutation_key ?? 'none');
|
||||
$this->salutationKey = (string) ($profile?->salutation_key ?? 'none');
|
||||
$this->firstName = (string) ($profile?->first_name ?? '');
|
||||
$this->lastName = (string) ($profile?->last_name ?? '');
|
||||
$this->title = (string) ($profile?->title ?? '');
|
||||
|
|
@ -94,20 +68,12 @@ 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 ?? $user->name);
|
||||
$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');
|
||||
|
||||
$this->loadEditableCompany();
|
||||
}
|
||||
|
||||
public function selectCompany(int $companyId): void
|
||||
{
|
||||
$this->editableCompanyId = $companyId;
|
||||
$this->loadEditableCompany();
|
||||
}
|
||||
|
||||
public function saveProfile(): void
|
||||
|
|
@ -184,139 +150,20 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
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,
|
||||
'billingComplete' => $this->billingIsComplete(),
|
||||
];
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -347,10 +194,15 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
<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 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="ghost" icon="building-office" href="{{ route('me.press-kits.index') }}" wire:navigate>
|
||||
{{ __('Firmen verwalten') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (session('profile-status'))
|
||||
|
|
@ -362,60 +214,42 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
@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>
|
||||
@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" />
|
||||
|
|
@ -432,52 +266,86 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
<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="ghost" 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">{{ __('Aktionen') }}</span>
|
||||
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex justify-end">
|
||||
<flux:button type="submit" variant="primary">{{ __('Profil speichern') }}</flux:button>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue