1105 lines
56 KiB
PHP
1105 lines
56 KiB
PHP
<?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:10240';
|
||
$rules['newShowroomPhotos'] = 'nullable|array|max:20';
|
||
$rules['newShowroomPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:10240';
|
||
}
|
||
|
||
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:10240';
|
||
}
|
||
|
||
$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 10 MB groß sein.'),
|
||
'newShowroomPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
||
'newShowroomPhotos.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
|
||
'newBrandImages.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
||
'newBrandImages.*.max' => __('Bilder dürfen maximal 10 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. 10 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. 10 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. 10 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. 10 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. 10 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. 10 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>
|