b2in/resources/views/livewire/partner/my-data.blade.php

1105 lines
56 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\Brand;
use App\Models\Media;
use App\Models\Partner;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use function Livewire\Volt\layout;
use function Livewire\Volt\title;
layout('components.layouts.app');
title('Meine Daten');
new class extends Component
{
use WithFileUploads;
public Partner $partner;
public string $partnerType;
public string $activeTab = 'stammdaten';
// Stammdaten
public string $companyName = '';
public string $displayName = '';
public string $salutation = '';
public string $firstName = '';
public string $lastName = '';
public string $description = '';
public string $street = '';
public string $houseNumber = '';
public string $zip = '';
public string $city = '';
public string $country = 'Deutschland';
public string $phone = '';
public string $website = '';
// Retailer Liefergebiete
public ?int $deliveryRadius = null;
public ?int $assemblyRadius = null;
// Manufacturer Marke
public string $brandName = '';
public string $brandDescription = '';
// Profil-Felder
public string $storyText = '';
public int|string $foundedYear = '';
public string $specialtiesInput = '';
/**
* Öffnungszeiten (nur Retailer).
*
* @var array<string, array{open: string, close: string, closed: bool}>
*/
public array $openingHours = [
'monday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'tuesday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'wednesday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'thursday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'friday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'saturday' => ['open' => '10:00', 'close' => '16:00', 'closed' => false],
'sunday' => ['open' => '', 'close' => '', 'closed' => true],
];
// Neue Fotos (temporäre Upload-Objekte)
/** @var array<int, \Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
public array $newTeamPhotos = [];
/** @var array<int, \Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
public array $newShowroomPhotos = [];
/** @var array<int, \Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
public array $newBrandImages = [];
// Bestehende Fotos (aus DB, für Drag-&-Drop)
/** @var array<int, array{id: int, file_path: string, alt_text: string}> */
public array $existingTeamPhotos = [];
/** @var array<int, array{id: int, file_path: string, alt_text: string}> */
public array $existingShowroomPhotos = [];
/** @var array<int, array{id: int, file_path: string, alt_text: string}> */
public array $existingBrandImages = [];
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', 'media'])->findOrFail($user->partner_id);
$this->partnerType = $this->partner->type?->value ?? '';
// Stammdaten
$this->companyName = $this->partner->company_name ?? '';
$this->displayName = $this->partner->display_name ?? '';
$this->salutation = $this->partner->salutation ?? '';
$this->firstName = $this->partner->first_name ?? '';
$this->lastName = $this->partner->last_name ?? '';
$this->description = $this->partner->description ?? '';
$this->street = $this->partner->street ?? '';
$this->houseNumber = $this->partner->house_number ?? '';
$this->zip = $this->partner->zip ?? '';
$this->city = $this->partner->city ?? '';
$this->country = $this->partner->country ?? 'Deutschland';
$this->phone = $this->partner->phone ?? '';
$this->website = $this->partner->website ?? '';
$this->deliveryRadius = $this->partner->delivery_radius_km;
$this->assemblyRadius = $this->partner->assembly_radius_km;
// Platzhalter wie "roles.broker M10000004" ersetzen
if (str_contains($this->companyName, 'roles.')) {
$parts = explode(' ', $this->companyName, 2);
$translatedRole = isset($parts[0]) ? __($parts[0]) : $this->companyName;
$partnerId = isset($parts[1]) ? ' '.$parts[1] : '';
$this->companyName = $translatedRole.$partnerId;
}
// Namen aus User übernehmen, falls Partner-Felder leer sind
if (empty($this->firstName) && empty($this->lastName)) {
$nameParts = explode(' ', $user->name, 2);
$this->firstName = $nameParts[0] ?? '';
$this->lastName = $nameParts[1] ?? '';
}
// Profil-Felder
$this->storyText = $this->partner->story_text ?? '';
$this->foundedYear = $this->partner->founded_year ?? '';
$this->specialtiesInput = $this->partner->specialties
? implode(', ', $this->partner->specialties)
: '';
if ($this->partner->opening_hours) {
$this->openingHours = array_merge($this->openingHours, $this->partner->opening_hours);
}
// Marke für Manufacturer
if ($this->isManufacturer()) {
$brand = Brand::where('partner_id', $this->partner->id)->first();
if ($brand) {
$this->brandName = $brand->name;
$this->brandDescription = $brand->description ?? '';
}
}
$this->loadExistingMedia();
}
// ── Foto-Verwaltung ────────────────────────────────────────────────────────
public function removeQueuedPhoto(int $index, string $type): void
{
$property = $this->queuedPropertyForType($type);
if ($property && isset($this->$property[$index])) {
unset($this->$property[$index]);
$this->$property = array_values($this->$property);
}
}
public function removeExistingPhoto(int $mediaId, string $type): void
{
$media = Media::where('id', $mediaId)
->where('model_type', Partner::class)
->where('model_id', $this->partner->id)
->firstOrFail();
Storage::disk('public')->delete($media->file_path);
$media->delete();
$property = $this->existingPropertyForType($type);
if ($property) {
$this->$property = array_values(
array_filter($this->$property, fn ($m) => $m['id'] !== $mediaId)
);
}
}
/**
* Reihenfolge per Drag-&-Drop aktualisieren.
*
* @param array<int> $orderedIds
*/
public function updatePhotoOrder(array $orderedIds, string $type): void
{
foreach ($orderedIds as $position => $mediaId) {
$this->partner->media()
->where('id', $mediaId)
->where('type', $type)
->update(['order_column' => $position + 1]);
}
$property = $this->existingPropertyForType($type);
if (! $property) {
return;
}
$reordered = collect($orderedIds)->map(function ($id, $index) use ($property) {
$media = collect($this->$property)->firstWhere('id', $id);
return $media ? array_merge($media, ['order_column' => $index + 1]) : null;
})->filter()->values()->toArray();
$this->$property = $reordered;
}
// ── Speichern ──────────────────────────────────────────────────────────────
public function saveData(): void
{
$rules = [
'salutation' => 'required|in:Herr,Frau,Divers',
'firstName' => 'required|string|max:255',
'lastName' => 'required|string|max:255',
'street' => 'required|string|max:255',
'houseNumber' => 'required|string|max:20',
'zip' => 'required|string|max:10',
'city' => 'required|string|max:255',
'country' => 'required|string|max:255',
'phone' => 'nullable|string|max:50',
'storyText' => 'nullable|string|max:2000',
'foundedYear' => 'nullable|integer|min:1800|max:'.now()->year,
'specialtiesInput' => 'nullable|string|max:500',
];
if (! $this->isCustomer()) {
$rules['companyName'] = 'required|string|max:255';
$rules['description'] = 'nullable|string|max:1000';
$rules['website'] = 'nullable|url|max:255';
}
if ($this->isBroker()) {
$rules['displayName'] = 'required|string|max:255';
}
if ($this->isRetailer()) {
$rules['deliveryRadius'] = 'required|integer|min:1|max:500';
$rules['assemblyRadius'] = 'required|integer|min:1|max:500';
$rules['newTeamPhotos'] = 'nullable|array|max:10';
$rules['newTeamPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:204800';
$rules['newShowroomPhotos'] = 'nullable|array|max:20';
$rules['newShowroomPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:204800';
}
if ($this->isManufacturer()) {
$rules['brandName'] = 'required|string|max:255';
$rules['brandDescription'] = 'nullable|string|max:1000';
$rules['newBrandImages'] = 'nullable|array|max:10';
$rules['newBrandImages.*'] = 'image|mimes:jpeg,jpg,png|max:204800';
}
$this->validate($rules, [
'salutation.required' => __('Bitte wählen Sie eine Anrede.'),
'firstName.required' => __('Bitte geben Sie einen Vornamen ein.'),
'lastName.required' => __('Bitte geben Sie einen Nachnamen ein.'),
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
'displayName.required' => __('Bitte geben Sie einen Anzeigenamen ein.'),
'street.required' => __('Bitte geben Sie eine Straße ein.'),
'houseNumber.required' => __('Bitte geben Sie eine Hausnummer ein.'),
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
'country.required' => __('Bitte wählen Sie ein Land.'),
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
'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.'),
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
'foundedYear.integer' => __('Bitte geben Sie eine gültige Jahreszahl ein.'),
'foundedYear.min' => __('Das Gründungsjahr muss nach 1800 liegen.'),
'foundedYear.max' => __('Das Gründungsjahr darf nicht in der Zukunft liegen.'),
'newTeamPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
'newTeamPhotos.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
'newShowroomPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
'newShowroomPhotos.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
'newBrandImages.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
'newBrandImages.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
]);
$specialties = array_values(array_filter(
array_map('trim', explode(',', $this->specialtiesInput))
));
$updateData = [
'salutation' => $this->salutation,
'first_name' => $this->firstName,
'last_name' => $this->lastName,
'street' => $this->street,
'house_number' => $this->houseNumber,
'zip' => $this->zip,
'city' => $this->city,
'country' => $this->country,
'phone' => $this->phone,
'story_text' => $this->storyText ?: null,
'founded_year' => $this->foundedYear ?: null,
'specialties' => $specialties ?: null,
];
if (! $this->isCustomer()) {
$updateData['company_name'] = $this->companyName;
$updateData['description'] = $this->description;
$updateData['website'] = $this->website;
}
if ($this->isBroker()) {
$updateData['display_name'] = $this->displayName;
}
if ($this->isRetailer()) {
$updateData['delivery_radius_km'] = $this->deliveryRadius;
$updateData['assembly_radius_km'] = $this->assemblyRadius;
$updateData['opening_hours'] = $this->openingHours;
}
$this->partner->update($updateData);
if ($this->isManufacturer()) {
Brand::updateOrCreate(
['partner_id' => $this->partner->id],
[
'name' => $this->brandName,
'slug' => Str::slug($this->brandName),
'description' => $this->brandDescription,
'is_active' => true,
]
);
}
$this->saveUploadedPhotos($this->newTeamPhotos, 'team_photo');
$this->saveUploadedPhotos($this->newShowroomPhotos, 'showroom');
$this->saveUploadedPhotos($this->newBrandImages, 'brand_image');
$this->newTeamPhotos = [];
$this->newShowroomPhotos = [];
$this->newBrandImages = [];
$this->partner->load('media');
$this->loadExistingMedia();
session()->flash('message', __('Ihre Daten wurden erfolgreich aktualisiert.'));
}
// ── Hilfsmethoden ──────────────────────────────────────────────────────────
private function isCustomer(): bool
{
return strtolower(str_replace('-', '', $this->partnerType)) === 'customer';
}
private function isRetailer(): bool
{
return strtolower(str_replace('-', '', $this->partnerType)) === 'retailer';
}
private function isManufacturer(): bool
{
return strtolower(str_replace('-', '', $this->partnerType)) === 'manufacturer';
}
private function isBroker(): bool
{
$t = strtolower(str_replace('-', '', $this->partnerType));
return $t === 'broker' || $t === 'estateagent';
}
private function loadExistingMedia(): void
{
$this->existingTeamPhotos = $this->partner->media
->where('type', 'team_photo')
->sortBy('order_column')
->values()
->map(fn ($m) => ['id' => $m->id, 'file_path' => $m->file_path, 'alt_text' => $m->alt_text ?? ''])
->toArray();
$this->existingShowroomPhotos = $this->partner->media
->where('type', 'showroom')
->sortBy('order_column')
->values()
->map(fn ($m) => ['id' => $m->id, 'file_path' => $m->file_path, 'alt_text' => $m->alt_text ?? ''])
->toArray();
$this->existingBrandImages = $this->partner->media
->where('type', 'brand_image')
->sortBy('order_column')
->values()
->map(fn ($m) => ['id' => $m->id, 'file_path' => $m->file_path, 'alt_text' => $m->alt_text ?? ''])
->toArray();
}
/** @param array<int, \Livewire\Features\SupportFileUploads\TemporaryUploadedFile> $files */
private function saveUploadedPhotos(array $files, string $type): void
{
if (empty($files)) {
return;
}
$maxOrder = $this->partner->media()
->where('type', $type)
->max('order_column') ?? 0;
$index = $maxOrder + 1;
foreach ($files as $file) {
$path = $file->store('partners/'.$this->partner->id.'/'.$type, 'public');
$this->partner->media()->create([
'file_path' => $path,
'type' => $type,
'alt_text' => $this->partner->company_name,
'order_column' => $index++,
]);
}
}
private function queuedPropertyForType(string $type): ?string
{
return match ($type) {
'team_photo' => 'newTeamPhotos',
'showroom' => 'newShowroomPhotos',
'brand_image' => 'newBrandImages',
default => null,
};
}
private function existingPropertyForType(string $type): ?string
{
return match ($type) {
'team_photo' => 'existingTeamPhotos',
'showroom' => 'existingShowroomPhotos',
'brand_image' => 'existingBrandImages',
default => null,
};
}
/** @return array<string, string> */
protected function dayLabels(): array
{
return [
'monday' => __('Montag'),
'tuesday' => __('Dienstag'),
'wednesday' => __('Mittwoch'),
'thursday' => __('Donnerstag'),
'friday' => __('Freitag'),
'saturday' => __('Samstag'),
'sunday' => __('Sonntag'),
];
}
public function with(): array
{
return [
'dayLabels' => $this->dayLabels(),
'isRetailer' => $this->isRetailer(),
'isManufacturer' => $this->isManufacturer(),
'isCustomer' => $this->isCustomer(),
'isBroker' => $this->isBroker(),
];
}
}; ?>
<div class="space-y-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Meine Daten') }}</flux:heading>
<flux:subheading>{{ __('Verwalten Sie Ihre Firmendaten und Ihr Profil') }}</flux:subheading>
</div>
<div class="flex items-center gap-2">
@svg('heroicon-o-'.$roleIcon, 'w-6 h-6 text-accent-600 dark:text-accent-400')
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $roleName }}</span>
</div>
</div>
<x-success-alert />
<form wire:submit="saveData" class="space-y-6">
{{-- Tab-Navigation --}}
<flux:tabs wire:model.live="activeTab" variant="segmented">
<flux:tab name="stammdaten" icon="building-office">{{ __('Stammdaten') }}</flux:tab>
<flux:tab name="praesentation" icon="sparkles">{{ $isRetailer ? __('Präsentation') : __('Über uns') }}</flux:tab>
@if ($isRetailer)
<flux:tab name="oeffnungszeiten" icon="clock">{{ __('Öffnungszeiten') }}</flux:tab>
@endif
@if ($isRetailer || $isManufacturer)
<flux:tab name="fotos" icon="photo">{{ __('Fotos') }}</flux:tab>
@endif
</flux:tabs>
{{-- ── TAB 1: STAMMDATEN ── --}}
@if ($activeTab === 'stammdaten')
<div class="space-y-6">
<flux:card class="space-y-6 shadow-elegant">
<div>
<flux:heading size="lg">{{ __('Firmendaten') }}</flux:heading>
<flux:subheading>{{ __('Ihre Kontakt- und Adressdaten') }}</flux:subheading>
</div>
<flux:separator />
@if (!$isCustomer)
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="companyName" name="companyName" icon="building-office" placeholder="{{ __('z.B. Müller GmbH') }}" />
@error('companyName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
@if ($isBroker)
<flux:field>
<flux:label>{{ __('Anzeigename') }} <span class="text-red-500">*</span></flux:label>
<flux:description>{{ __('Der Name, der Ihren Kunden angezeigt wird') }}</flux:description>
<flux:input wire:model="displayName" name="displayName" icon="user" />
@error('displayName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
@endif
<flux:field>
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
<flux:textarea wire:model="description" name="description" rows="3" placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
@error('description') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:separator />
@endif
{{-- Persönliche Daten --}}
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:field>
<flux:label>{{ __('Anrede') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="salutation" name="salutation">
<flux:select.option value="">{{ __('Bitte wählen') }}</flux:select.option>
<flux:select.option value="Herr">{{ __('Herr') }}</flux:select.option>
<flux:select.option value="Frau">{{ __('Frau') }}</flux:select.option>
<flux:select.option value="Divers">{{ __('Divers') }}</flux:select.option>
</flux:select>
@error('salutation') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Vorname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="firstName" name="firstName" icon="user" />
@error('firstName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Nachname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="lastName" name="lastName" icon="user" />
@error('lastName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<flux:separator />
{{-- Adresse --}}
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<flux:field class="md:col-span-3">
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="street" name="street" icon="map-pin" placeholder="{{ __('Musterstraße') }}" />
@error('street') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Hausnummer') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="houseNumber" name="houseNumber" placeholder="{{ __('123a') }}" />
@error('houseNumber') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:field>
<flux:label>{{ __('Postleitzahl') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="zip" name="zip" icon="map" placeholder="{{ __('12345') }}" />
@error('zip') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field class="md:col-span-2">
<flux:label>{{ __('Ort') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="city" name="city" icon="building-office" placeholder="{{ __('Musterstadt') }}" />
@error('city') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="country" name="country">
<flux:select.option value="Deutschland">{{ __('Deutschland') }}</flux:select.option>
<flux:select.option value="Österreich">{{ __('Österreich') }}</flux:select.option>
<flux:select.option value="Schweiz">{{ __('Schweiz') }}</flux:select.option>
</flux:select>
@error('country') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="phone" name="phone" type="tel" icon="phone" placeholder="{{ __('optional') }}" />
@error('phone') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
@if (!$isCustomer)
<flux:field>
<flux:label>{{ __('Website') }}</flux:label>
<flux:input wire:model="website" name="website" type="url" icon="globe-alt" placeholder="https://..." />
@error('website') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
@endif
</div>
{{-- Liefergebiete (nur Händler) --}}
@if ($isRetailer)
<flux:separator />
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<p class="mb-1 text-sm font-medium text-blue-800 dark:text-blue-200">{{ __('Liefergebiete') }}</p>
<p class="text-xs text-blue-600 dark:text-blue-300">{{ __('Definieren Sie, in welchem Umkreis Sie liefern und montieren können.') }}</p>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Lieferradius (km)') }} <span class="text-red-500">*</span></flux:label>
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
<flux:input wire:model="deliveryRadius" name="deliveryRadius" type="number" min="1" max="500" placeholder="z.B. 50" />
@error('deliveryRadius') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Montageradius (km)') }} <span class="text-red-500">*</span></flux:label>
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
<flux:input wire:model="assemblyRadius" name="assemblyRadius" type="number" min="1" max="500" placeholder="z.B. 30" />
@error('assemblyRadius') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
@endif
</flux:card>
</div>
@endif
{{-- ── TAB 2: PRÄSENTATION / ÜBER UNS ── --}}
@if ($activeTab === 'praesentation')
<div class="space-y-6">
{{-- Story & Profil --}}
<flux:card class="space-y-6 shadow-elegant">
<div>
<flux:heading size="lg">{{ $isRetailer ? __('Präsentation & Story') : __('Über das Unternehmen') }}</flux:heading>
<flux:subheading>
{{ $isRetailer
? __('Erzählen Sie Ihre Geschichte was macht Ihren Showroom besonders?')
: __('Teilen Sie Ihre Unternehmensgeschichte und Spezialisierungen.') }}
</flux:subheading>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Über uns / Story') }}</flux:label>
<flux:description>{{ __('Sichtbar auf Ihrem öffentlichen Profil. Max. 2000 Zeichen.') }}</flux:description>
<flux:textarea
wire:model="storyText"
name="storyText"
rows="8"
placeholder="{{ $isRetailer ? __('Erzählen Sie von Ihrem Showroom, Ihrer Geschichte und was Sie besonders macht...') : __('Erzählen Sie von Ihrer Marke, Ihren Werten und Ihrer Philosophie...') }}"
/>
<div class="mt-1 text-right text-xs text-zinc-400">{{ strlen($storyText) }} / 2000</div>
@error('storyText') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Gründungsjahr') }}</flux:label>
<flux:input wire:model="foundedYear" name="foundedYear" type="number" min="1800" max="{{ now()->year }}" placeholder="{{ __('z.B. 1998') }}" icon="calendar" />
@error('foundedYear') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Spezialisierungen') }}</flux:label>
<flux:description>{{ __('Kommagetrennt, z.B.: Polstermöbel, Outdoor, Küchen') }}</flux:description>
<flux:input wire:model="specialtiesInput" name="specialtiesInput" icon="tag" placeholder="{{ __('Polstermöbel, Outdoor, Küchen') }}" />
@error('specialtiesInput') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
</flux:card>
{{-- Marke (nur Hersteller) --}}
@if ($isManufacturer)
<flux:card class="space-y-6 shadow-elegant">
<div>
<flux:heading size="lg">{{ __('Ihre Marke') }}</flux:heading>
<flux:subheading>{{ __('Unter dieser Marke werden Ihre Produkte auf B2in gelistet.') }}</flux:subheading>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="brandName" name="brandName" icon="tag" placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
@error('brandName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
<flux:textarea wire:model="brandDescription" name="brandDescription" rows="4" placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
@error('brandDescription') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</flux:card>
@endif
</div>
@endif
{{-- ── TAB 3: ÖFFNUNGSZEITEN (nur Händler) ── --}}
@if ($activeTab === 'oeffnungszeiten' && $isRetailer)
<div class="space-y-6">
<flux:card class="space-y-6 shadow-elegant">
<div>
<flux:heading size="lg">{{ __('Öffnungszeiten') }}</flux:heading>
<flux:subheading>{{ __('Wann sind Sie für Kunden erreichbar oder Ihr Showroom geöffnet?') }}</flux:subheading>
</div>
<flux:separator />
<div class="space-y-3">
@foreach ($dayLabels as $dayKey => $dayLabel)
<div class="flex flex-wrap items-center gap-3 rounded-lg px-3 py-2 even:bg-zinc-50 dark:even:bg-zinc-800/50">
<div class="w-28 shrink-0 text-sm font-medium text-zinc-700 dark:text-zinc-300">
{{ $dayLabel }}
</div>
<flux:checkbox
wire:model.live="openingHours.{{ $dayKey }}.closed"
label="{{ __('Geschlossen') }}"
/>
@unless ($openingHours[$dayKey]['closed'] ?? false)
<div class="flex items-center gap-2">
<flux:input
wire:model="openingHours.{{ $dayKey }}.open"
type="time"
size="sm"
/>
<span class="text-sm text-zinc-500"></span>
<flux:input
wire:model="openingHours.{{ $dayKey }}.close"
type="time"
size="sm"
/>
</div>
@endunless
</div>
@endforeach
</div>
</flux:card>
</div>
@endif
{{-- ── TAB 4: FOTOS ── --}}
@if ($activeTab === 'fotos' && ($isRetailer || $isManufacturer))
<div class="space-y-6">
@if ($isRetailer)
{{-- ── Team-Fotos ── --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Team-Fotos') }}</flux:heading>
<flux:subheading>{{ __('Nur JPG/PNG max. 200 MB pro Bild') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
@if (count($existingTeamPhotos) > 0)
<div class="mb-6">
<flux:heading size="sm" class="mb-3">{{ __('Vorhandene Fotos') }}</flux:heading>
<flux:text size="sm" class="mb-3 text-zinc-500">{{ __('Per Drag & Drop sortieren das erste Foto wird als Hauptbild verwendet.') }}</flux:text>
<div
x-data="{
dragging: null,
dragOver: null,
items: @js(collect($existingTeamPhotos)->pluck('id')->toArray()),
onDragStart(e, id) {
this.dragging = id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', id);
},
onDragOver(e, id) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
this.dragOver = id;
},
onDrop(e, targetId) {
e.preventDefault();
if (this.dragging === targetId) { this.dragOver = null; return; }
const fromIdx = this.items.indexOf(this.dragging);
const toIdx = this.items.indexOf(targetId);
this.items.splice(fromIdx, 1);
this.items.splice(toIdx, 0, this.dragging);
this.dragging = null;
this.dragOver = null;
$wire.updatePhotoOrder(this.items, 'team_photo');
},
onDragEnd() { this.dragging = null; this.dragOver = null; }
}"
class="flex flex-wrap items-start gap-3"
>
@foreach ($existingTeamPhotos as $photo)
<div
wire:key="team-photo-{{ $photo['id'] }}"
draggable="true"
x-on:dragstart="onDragStart($event, {{ $photo['id'] }})"
x-on:dragover="onDragOver($event, {{ $photo['id'] }})"
x-on:drop="onDrop($event, {{ $photo['id'] }})"
x-on:dragend="onDragEnd()"
:class="{
'opacity-50 scale-95': dragging === {{ $photo['id'] }},
'ring-2 ring-blue-400 ring-offset-2 dark:ring-offset-zinc-800': dragOver === {{ $photo['id'] }} && dragging !== {{ $photo['id'] }}
}"
class="group relative cursor-grab transition-all duration-150 active:cursor-grabbing"
>
<img
src="{{ Storage::url($photo['file_path']) }}"
alt="{{ $photo['alt_text'] }}"
class="h-24 w-24 rounded-lg border border-zinc-200 object-cover dark:border-zinc-700"
/>
<flux:button
wire:click="removeExistingPhoto({{ $photo['id'] }}, 'team_photo')"
wire:confirm="{{ __('Foto wirklich löschen?') }}"
variant="filled" size="xs" icon="trash"
class="absolute -right-2 -top-2 !bg-red-500 !text-white hover:!bg-red-600"
/>
<div class="absolute bottom-1 left-1/2 -translate-x-1/2 opacity-0 transition-opacity group-hover:opacity-100">
<flux:icon.arrows-up-down class="h-4 w-4 text-white drop-shadow-md" />
</div>
</div>
@endforeach
</div>
</div>
<flux:separator class="mb-6" />
@endif
<flux:file-upload wire:model="newTeamPhotos" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
<flux:file-upload.dropzone heading="{{ __('Team-Fotos hochladen') }}" text="{{ __('JPEG oder PNG max. 200 MB') }}" with-progress />
</flux:file-upload>
@if (count($newTeamPhotos) > 0)
<div class="mt-4 flex flex-wrap items-center gap-3">
@foreach ($newTeamPhotos as $index => $photo)
<flux:file-item
:heading="$photo->getClientOriginalName()"
:image="(str_starts_with($photo->getMimeType() ?? '', 'image/') && $photo->isPreviewable()) ? $photo->temporaryUrl() : null"
:size="$photo->getSize()"
>
<x-slot name="actions">
<flux:file-item.remove wire:click="removeQueuedPhoto({{ $index }}, 'team_photo')" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="newTeamPhotos.*" />
</flux:card>
{{-- ── Showroom-Galerie ── --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Showroom-Galerie') }}</flux:heading>
<flux:subheading>{{ __('Bilder Ihres Showrooms für das öffentliche Profil nur JPG/PNG, max. 200 MB') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
@if (count($existingShowroomPhotos) > 0)
<div class="mb-6">
<flux:heading size="sm" class="mb-3">{{ __('Vorhandene Bilder') }}</flux:heading>
<flux:text size="sm" class="mb-3 text-zinc-500">{{ __('Per Drag & Drop sortieren.') }}</flux:text>
<div
x-data="{
dragging: null,
dragOver: null,
items: @js(collect($existingShowroomPhotos)->pluck('id')->toArray()),
onDragStart(e, id) {
this.dragging = id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', id);
},
onDragOver(e, id) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
this.dragOver = id;
},
onDrop(e, targetId) {
e.preventDefault();
if (this.dragging === targetId) { this.dragOver = null; return; }
const fromIdx = this.items.indexOf(this.dragging);
const toIdx = this.items.indexOf(targetId);
this.items.splice(fromIdx, 1);
this.items.splice(toIdx, 0, this.dragging);
this.dragging = null;
this.dragOver = null;
$wire.updatePhotoOrder(this.items, 'showroom');
},
onDragEnd() { this.dragging = null; this.dragOver = null; }
}"
class="flex flex-wrap items-start gap-3"
>
@foreach ($existingShowroomPhotos as $photo)
<div
wire:key="showroom-photo-{{ $photo['id'] }}"
draggable="true"
x-on:dragstart="onDragStart($event, {{ $photo['id'] }})"
x-on:dragover="onDragOver($event, {{ $photo['id'] }})"
x-on:drop="onDrop($event, {{ $photo['id'] }})"
x-on:dragend="onDragEnd()"
:class="{
'opacity-50 scale-95': dragging === {{ $photo['id'] }},
'ring-2 ring-blue-400 ring-offset-2 dark:ring-offset-zinc-800': dragOver === {{ $photo['id'] }} && dragging !== {{ $photo['id'] }}
}"
class="group relative cursor-grab transition-all duration-150 active:cursor-grabbing"
>
<img
src="{{ Storage::url($photo['file_path']) }}"
alt="{{ $photo['alt_text'] }}"
class="h-24 w-24 rounded-lg border border-zinc-200 object-cover dark:border-zinc-700"
/>
<flux:button
wire:click="removeExistingPhoto({{ $photo['id'] }}, 'showroom')"
wire:confirm="{{ __('Foto wirklich löschen?') }}"
variant="filled" size="xs" icon="trash"
class="absolute -right-2 -top-2 !bg-red-500 !text-white hover:!bg-red-600"
/>
<div class="absolute bottom-1 left-1/2 -translate-x-1/2 opacity-0 transition-opacity group-hover:opacity-100">
<flux:icon.arrows-up-down class="h-4 w-4 text-white drop-shadow-md" />
</div>
</div>
@endforeach
</div>
</div>
<flux:separator class="mb-6" />
@endif
<flux:file-upload wire:model="newShowroomPhotos" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
<flux:file-upload.dropzone heading="{{ __('Showroom-Bilder hochladen') }}" text="{{ __('JPEG oder PNG max. 200 MB') }}" with-progress />
</flux:file-upload>
@if (count($newShowroomPhotos) > 0)
<div class="mt-4 flex flex-wrap items-center gap-3">
@foreach ($newShowroomPhotos as $index => $photo)
<flux:file-item
:heading="$photo->getClientOriginalName()"
:image="(str_starts_with($photo->getMimeType() ?? '', 'image/') && $photo->isPreviewable()) ? $photo->temporaryUrl() : null"
:size="$photo->getSize()"
>
<x-slot name="actions">
<flux:file-item.remove wire:click="removeQueuedPhoto({{ $index }}, 'showroom')" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="newShowroomPhotos.*" />
</flux:card>
@endif
@if ($isManufacturer)
{{-- ── Marken-Bilder ── --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Marken-Bilder') }}</flux:heading>
<flux:subheading>{{ __('Bilder für Ihre Marken-Präsentation (Atmosphäre, Brand-Story etc.) nur JPG/PNG, max. 200 MB') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
@if (count($existingBrandImages) > 0)
<div class="mb-6">
<flux:heading size="sm" class="mb-3">{{ __('Vorhandene Bilder') }}</flux:heading>
<flux:text size="sm" class="mb-3 text-zinc-500">{{ __('Per Drag & Drop sortieren.') }}</flux:text>
<div
x-data="{
dragging: null,
dragOver: null,
items: @js(collect($existingBrandImages)->pluck('id')->toArray()),
onDragStart(e, id) {
this.dragging = id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', id);
},
onDragOver(e, id) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
this.dragOver = id;
},
onDrop(e, targetId) {
e.preventDefault();
if (this.dragging === targetId) { this.dragOver = null; return; }
const fromIdx = this.items.indexOf(this.dragging);
const toIdx = this.items.indexOf(targetId);
this.items.splice(fromIdx, 1);
this.items.splice(toIdx, 0, this.dragging);
this.dragging = null;
this.dragOver = null;
$wire.updatePhotoOrder(this.items, 'brand_image');
},
onDragEnd() { this.dragging = null; this.dragOver = null; }
}"
class="flex flex-wrap items-start gap-3"
>
@foreach ($existingBrandImages as $photo)
<div
wire:key="brand-image-{{ $photo['id'] }}"
draggable="true"
x-on:dragstart="onDragStart($event, {{ $photo['id'] }})"
x-on:dragover="onDragOver($event, {{ $photo['id'] }})"
x-on:drop="onDrop($event, {{ $photo['id'] }})"
x-on:dragend="onDragEnd()"
:class="{
'opacity-50 scale-95': dragging === {{ $photo['id'] }},
'ring-2 ring-blue-400 ring-offset-2 dark:ring-offset-zinc-800': dragOver === {{ $photo['id'] }} && dragging !== {{ $photo['id'] }}
}"
class="group relative cursor-grab transition-all duration-150 active:cursor-grabbing"
>
<img
src="{{ Storage::url($photo['file_path']) }}"
alt="{{ $photo['alt_text'] }}"
class="h-24 w-24 rounded-lg border border-zinc-200 object-cover dark:border-zinc-700"
/>
<flux:button
wire:click="removeExistingPhoto({{ $photo['id'] }}, 'brand_image')"
wire:confirm="{{ __('Foto wirklich löschen?') }}"
variant="filled" size="xs" icon="trash"
class="absolute -right-2 -top-2 !bg-red-500 !text-white hover:!bg-red-600"
/>
<div class="absolute bottom-1 left-1/2 -translate-x-1/2 opacity-0 transition-opacity group-hover:opacity-100">
<flux:icon.arrows-up-down class="h-4 w-4 text-white drop-shadow-md" />
</div>
</div>
@endforeach
</div>
</div>
<flux:separator class="mb-6" />
@endif
<flux:file-upload wire:model="newBrandImages" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
<flux:file-upload.dropzone heading="{{ __('Marken-Bilder hochladen') }}" text="{{ __('JPEG oder PNG max. 200 MB') }}" with-progress />
</flux:file-upload>
@if (count($newBrandImages) > 0)
<div class="mt-4 flex flex-wrap items-center gap-3">
@foreach ($newBrandImages as $index => $photo)
<flux:file-item
:heading="$photo->getClientOriginalName()"
:image="(str_starts_with($photo->getMimeType() ?? '', 'image/') && $photo->isPreviewable()) ? $photo->temporaryUrl() : null"
:size="$photo->getSize()"
>
<x-slot name="actions">
<flux:file-item.remove wire:click="removeQueuedPhoto({{ $index }}, 'brand_image')" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="newBrandImages.*" />
</flux:card>
@endif
</div>
@endif
{{-- Speichern-Button --}}
<div class="flex items-center justify-between">
<x-error-alert />
<flux:button type="submit" variant="primary" icon="check-circle" wire:loading.attr="disabled" class="ml-auto">
<span wire:loading.remove>{{ __('Änderungen speichern') }}</span>
<span wire:loading>{{ __('Wird gespeichert...') }}</span>
</flux:button>
</div>
</form>
</div>