549 lines
23 KiB
PHP
549 lines
23 KiB
PHP
<?php
|
|
|
|
use App\Models\Partner;
|
|
use App\Models\Brand;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\WithFileUploads;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends Component {
|
|
use WithFileUploads;
|
|
|
|
public Partner $partner;
|
|
public string $partnerType;
|
|
public int $currentStep = 1;
|
|
public int $totalSteps;
|
|
public array $steps = [];
|
|
|
|
// Schritt 1: Stammdaten (alle Rollen)
|
|
public string $companyName = '';
|
|
public $logo = null;
|
|
public string $description = '';
|
|
public string $street = '';
|
|
public string $zip = '';
|
|
public string $city = '';
|
|
public string $website = '';
|
|
|
|
// Schritt 2: Retailer - Liefergebiete
|
|
public ?int $deliveryRadius = null;
|
|
public ?int $assemblyRadius = null;
|
|
|
|
// Schritt 2: Manufacturer - Marke
|
|
public string $brandName = '';
|
|
public $brandLogo = null;
|
|
public string $brandDescription = '';
|
|
|
|
public string $roleIcon = 'shield-check';
|
|
public string $roleName = '-';
|
|
|
|
public function mount(): void
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if (!$user->partner_id) {
|
|
$this->redirect(route('dashboard'), navigate: true);
|
|
return;
|
|
}
|
|
|
|
$role = $user->roles->first();
|
|
if ($role) {
|
|
$this->roleIcon = $role->icon ?? 'shield-check';
|
|
$this->roleName = $role->display_name ?? $role->name;
|
|
}
|
|
|
|
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
|
|
$this->partnerType = $this->partner->type;
|
|
|
|
// Vorausfüllen
|
|
$this->companyName = $this->partner->company_name;
|
|
$this->description = $this->partner->description ?? '';
|
|
$this->website = '';
|
|
|
|
// Definiere Schritte basierend auf Rolle
|
|
$this->defineSteps();
|
|
}
|
|
|
|
protected function defineSteps(): void
|
|
{
|
|
switch ($this->partnerType) {
|
|
case 'Retailer':
|
|
$this->steps = ['Stammdaten', 'Liefergebiete', 'Fertig'];
|
|
$this->totalSteps = 3;
|
|
break;
|
|
case 'Manufacturer':
|
|
$this->steps = ['Stammdaten', 'Marke anlegen', 'Fertig'];
|
|
$this->totalSteps = 3;
|
|
break;
|
|
case 'Estate-Agent':
|
|
$this->steps = ['Profil', 'Fertig'];
|
|
$this->totalSteps = 2;
|
|
break;
|
|
default:
|
|
$this->steps = ['Stammdaten', 'Fertig'];
|
|
$this->totalSteps = 2;
|
|
break;
|
|
}
|
|
}
|
|
|
|
public function saveStep1(): void
|
|
{
|
|
$this->validate(
|
|
[
|
|
'companyName' => 'required|string|max:255',
|
|
'description' => 'nullable|string|max:1000',
|
|
'street' => 'required|string|max:255',
|
|
'zip' => 'required|string|max:10',
|
|
'city' => 'required|string|max:255',
|
|
'website' => 'nullable|url|max:255',
|
|
'logo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
|
],
|
|
[
|
|
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
|
'street.required' => __('Bitte geben Sie eine Straße ein.'),
|
|
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
|
|
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
|
|
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
|
|
'logo.image' => __('Das Logo muss eine Bilddatei sein.'),
|
|
'logo.mimes' => __('Das Logo muss im Format JPG, PNG oder WebP sein.'),
|
|
'logo.max' => __('Das Logo darf maximal 2 MB groß sein.'),
|
|
],
|
|
);
|
|
|
|
// Speichere Logo falls hochgeladen
|
|
$logoPath = null;
|
|
if ($this->logo) {
|
|
$logoPath = $this->logo->store('partner-logos', 'public');
|
|
}
|
|
|
|
// Update Partner
|
|
$this->partner->update([
|
|
'company_name' => $this->companyName,
|
|
'description' => $this->description,
|
|
'logo_url' => $logoPath ?? $this->partner->logo_url,
|
|
]);
|
|
|
|
// TODO: Adresse speichern (separates Address-Model oder JSON-Feld)
|
|
|
|
$this->currentStep = 2;
|
|
}
|
|
|
|
public function saveStep2Retailer(): void
|
|
{
|
|
$this->validate(
|
|
[
|
|
'deliveryRadius' => 'required|integer|min:1|max:500',
|
|
'assemblyRadius' => 'required|integer|min:1|max:500',
|
|
],
|
|
[
|
|
'deliveryRadius.required' => __('Bitte geben Sie einen Lieferradius ein.'),
|
|
'deliveryRadius.min' => __('Der Lieferradius muss mindestens 1 km betragen.'),
|
|
'assemblyRadius.required' => __('Bitte geben Sie einen Montageradius ein.'),
|
|
'assemblyRadius.min' => __('Der Montageradius muss mindestens 1 km betragen.'),
|
|
],
|
|
);
|
|
|
|
$this->partner->update([
|
|
'delivery_radius_km' => $this->deliveryRadius,
|
|
'assembly_radius_km' => $this->assemblyRadius,
|
|
]);
|
|
|
|
$this->completeSetup();
|
|
}
|
|
|
|
public function saveStep2Manufacturer(): void
|
|
{
|
|
$this->validate(
|
|
[
|
|
'brandName' => 'required|string|max:255',
|
|
'brandDescription' => 'nullable|string|max:1000',
|
|
'brandLogo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
|
],
|
|
[
|
|
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
|
|
'brandLogo.image' => __('Das Marken-Logo muss eine Bilddatei sein.'),
|
|
'brandLogo.mimes' => __('Das Marken-Logo muss im Format JPG, PNG oder WebP sein.'),
|
|
'brandLogo.max' => __('Das Marken-Logo darf maximal 2 MB groß sein.'),
|
|
],
|
|
);
|
|
|
|
// Speichere Brand-Logo falls hochgeladen
|
|
$brandLogoPath = null;
|
|
if ($this->brandLogo) {
|
|
$brandLogoPath = $this->brandLogo->store('brand-logos', 'public');
|
|
}
|
|
|
|
// Erstelle Brand
|
|
Brand::create([
|
|
'partner_id' => $this->partner->id,
|
|
'name' => $this->brandName,
|
|
'slug' => Str::slug($this->brandName),
|
|
'description' => $this->brandDescription,
|
|
'logo_url' => $brandLogoPath,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->completeSetup();
|
|
}
|
|
|
|
protected function completeSetup(): void
|
|
{
|
|
$this->partner->update([
|
|
'is_active' => true,
|
|
'setup_completed' => true,
|
|
'setup_completed_at' => now(),
|
|
]);
|
|
|
|
$this->currentStep = $this->totalSteps;
|
|
}
|
|
|
|
public function goToDashboard(): void
|
|
{
|
|
$this->redirect(route('dashboard'), navigate: true);
|
|
}
|
|
|
|
public function logout(): void
|
|
{
|
|
Auth::logout();
|
|
|
|
request()->session()->invalidate();
|
|
request()->session()->regenerateToken();
|
|
|
|
$this->redirect(route('home'), navigate: true);
|
|
}
|
|
}; ?>
|
|
|
|
<div class="w-full max-w-3xl">
|
|
{{-- Header --}}
|
|
<div class="text-center mb-8">
|
|
@include('partials.logo-head')
|
|
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white mb-2">
|
|
{{ __('Vervollständigen Sie Ihr Profil') }}
|
|
</h1>
|
|
</div>
|
|
|
|
{{-- Progress Indicator --}}
|
|
<x-wizard-progress :currentStep="$currentStep" :totalSteps="$totalSteps" :steps="$steps" />
|
|
|
|
{{-- Wizard Content --}}
|
|
<flux:card class="shadow-2xl">
|
|
{{-- Step 1: Stammdaten (für alle Rollen) --}}
|
|
@if ($currentStep === 1)
|
|
<form wire:submit="saveStep1" class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg" class="mb-2">
|
|
<div class="flex items-center gap-2">
|
|
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5')
|
|
|
|
@if ($partnerType === 'Retailer')
|
|
{{ __('Ihre Stammdaten') }}
|
|
@elseif($partnerType === 'Manufacturer')
|
|
{{ __('Ihre Stammdaten') }}
|
|
@else
|
|
{{ __('Ihr Profil') }}
|
|
@endif
|
|
/ {{ $roleName }}
|
|
</div>
|
|
</flux:heading>
|
|
<flux:subheading>
|
|
{{ __('Diese Informationen helfen uns, Ihr Profil zu vervollständigen.') }}
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:input wire:model="companyName" icon="building-office" />
|
|
@error('companyName')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Logo (optional)') }}</flux:label>
|
|
<flux:description>{{ __('Laden Sie Ihr Firmenlogo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
|
|
|
<div class="space-y-2">
|
|
<input type="file" wire:model.live="logo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
|
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
|
|
|
@error('logo')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
|
|
<div wire:loading wire:target="logo" class="text-sm text-accent-600 dark:text-accent-400">
|
|
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
|
|
{{ __('Wird hochgeladen...') }}
|
|
</div>
|
|
|
|
@if ($logo)
|
|
<div wire:loading.remove wire:target="logo">
|
|
@try
|
|
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<img src="{{ $logo->temporaryUrl() }}" class="h-16 w-16 object-contain rounded border"
|
|
alt="Logo Preview">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
|
|
<p class="text-xs text-green-600 dark:text-green-400">{{ $logo->getClientOriginalName() }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@catch(\Exception $e)
|
|
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
|
|
</div>
|
|
@endtry
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
|
<flux:textarea wire:model="description" rows="4"
|
|
placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
|
|
@error('description')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:separator />
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<flux:field class="md:col-span-2">
|
|
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:input wire:model="street" wire:/>
|
|
@error('street')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('PLZ') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:input wire:model="zip" />
|
|
@error('zip')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
</div>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:input wire:model="city" icon="map-pin" />
|
|
@error('city')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Website (optional)') }}</flux:label>
|
|
<flux:input wire:model="website" type="url" icon="globe-alt" placeholder="https://..." />
|
|
@error('website')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:separator />
|
|
|
|
<x-error-alert />
|
|
|
|
<div class="flex justify-between">
|
|
|
|
<flux:button type="button" variant="outline" icon="arrow-left-start-on-rectangle"
|
|
wire:click="logout">
|
|
{{ __('Logout') }}
|
|
</flux:button>
|
|
<flux:button type="submit" variant="primary" icon="arrow-right">
|
|
@if ($partnerType === 'Retailer')
|
|
{{ __('Weiter zu Liefergebiete') }}
|
|
@elseif($partnerType === 'Manufacturer')
|
|
{{ __('Weiter zu Marke') }}
|
|
@else
|
|
{{ __('Setup abschließen') }}
|
|
@endif
|
|
</flux:button>
|
|
</div>
|
|
</form>
|
|
@endif
|
|
|
|
{{-- Step 2: Retailer - Liefergebiete --}}
|
|
@if ($currentStep === 2 && $partnerType === 'Retailer')
|
|
<form wire:submit="saveStep2Retailer" class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg" class="mb-2">
|
|
🚚 {{ __('Ihre Liefergebiete') }}
|
|
</flux:heading>
|
|
<flux:subheading>
|
|
{{ __('Wie weit liefern und montieren Sie von Ihrer Adresse (:zip :city) aus?', ['zip' => $zip, 'city' => $city]) }}
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Lieferradius') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
|
|
<div class="flex items-center gap-4">
|
|
<flux:input wire:model="deliveryRadius" type="number" min="1" max="500"
|
|
class="flex-1" />
|
|
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
|
</div>
|
|
@error('deliveryRadius')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Montageradius') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
|
|
<div class="flex items-center gap-4">
|
|
<flux:input wire:model="assemblyRadius" type="number" min="1" max="500"
|
|
class="flex-1" />
|
|
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
|
</div>
|
|
@error('assemblyRadius')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:separator />
|
|
|
|
<x-error-alert />
|
|
|
|
<div class="flex justify-end">
|
|
<flux:button type="submit" variant="primary" icon="check">
|
|
{{ __('Setup abschließen') }}
|
|
</flux:button>
|
|
</div>
|
|
</form>
|
|
@endif
|
|
|
|
{{-- Step 2: Manufacturer - Marke anlegen --}}
|
|
@if ($currentStep === 2 && $partnerType === 'Manufacturer')
|
|
<form wire:submit="saveStep2Manufacturer" class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg" class="mb-2">
|
|
™️ {{ __('Ihre Marke') }}
|
|
</flux:heading>
|
|
<flux:subheading>
|
|
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }}
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
|
|
<flux:input wire:model="brandName" icon="tag"
|
|
placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
|
|
@error('brandName')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Marken-Logo (optional)') }}</flux:label>
|
|
<flux:description>{{ __('Laden Sie Ihr Marken-Logo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
|
|
|
<div class="space-y-2">
|
|
<input type="file" wire:model.live="brandLogo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
|
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
|
|
|
@error('brandLogo')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
|
|
<div wire:loading wire:target="brandLogo" class="text-sm text-accent-600 dark:text-accent-400">
|
|
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
|
|
{{ __('Wird hochgeladen...') }}
|
|
</div>
|
|
|
|
@if ($brandLogo)
|
|
<div wire:loading.remove wire:target="brandLogo">
|
|
@try
|
|
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<img src="{{ $brandLogo->temporaryUrl() }}"
|
|
class="h-16 w-16 object-contain rounded border" alt="Brand Logo Preview">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
|
|
<p class="text-xs text-green-600 dark:text-green-400">{{ $brandLogo->getClientOriginalName() }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@catch(\Exception $e)
|
|
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
|
|
</div>
|
|
@endtry
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
|
|
<flux:textarea wire:model="brandDescription" rows="4"
|
|
placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
|
|
@error('brandDescription')
|
|
<flux:error>{{ $message }}</flux:error>
|
|
@enderror
|
|
</flux:field>
|
|
|
|
<flux:separator />
|
|
|
|
<x-error-alert />
|
|
|
|
<div class="flex justify-end">
|
|
<flux:button type="submit" variant="primary" icon="check">
|
|
{{ __('Setup abschließen') }}
|
|
</flux:button>
|
|
</div>
|
|
</form>
|
|
@endif
|
|
|
|
{{-- Final Step: Fertig! --}}
|
|
@if ($currentStep === $totalSteps)
|
|
<div class="text-center space-y-6 py-8">
|
|
<div class="flex justify-center">
|
|
<div
|
|
class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/20">
|
|
<flux:icon.check-circle class="h-12 w-12 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:heading size="xl" class="mb-2">
|
|
✅ {{ __('Einrichtung abgeschlossen!') }}
|
|
</flux:heading>
|
|
<flux:subheading>
|
|
@if ($partnerType === 'Retailer')
|
|
{{ __('Sie sind nun ein aktiver Händler. Sie können jetzt Ihr Sortiment pflegen.') }}
|
|
@elseif($partnerType === 'Manufacturer')
|
|
{{ __('Sie sind nun ein aktiver Hersteller. Sie können jetzt Ihre Produkte anlegen.') }}
|
|
@else
|
|
{{ __('Ihr Profil ist aktiv. In Ihrem Dashboard finden Sie Ihre persönlichen Einladungslinks.') }}
|
|
@endif
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
|
@if ($partnerType !== 'Estate-Agent')
|
|
<flux:button variant="primary" icon="plus" size="lg">
|
|
{{ __('Erstes Produkt anlegen') }}
|
|
</flux:button>
|
|
@endif
|
|
<flux:button wire:click="goToDashboard" variant="outline" icon="home" size="lg">
|
|
{{ __('Zum Dashboard') }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</flux:card>
|
|
</div>
|