b2in/resources/views/livewire/admin/hubs/manage.blade.php
Kevin Adametz 6c6d683b9a Display CMS Optimierungen 29-05-2026
- Mediathek: Video-Vorschaubilder statt Icons (FFmpeg-Thumbnails + Backfill-Command), Kategorie "Sonstiges"
- B2in Media-Picker zeigt alle Medientypen, Typ wird automatisch erkannt; Thumbnail-Preview vor allen Medien-URL-Feldern
- B2in Marke/Footer: Footer ein/aus, Logo+Claim frei positionierbar (Ecken) mit Constraints, separate Anzeige-Schalter
- Angebote-Modul dynamisch: kein Slide-Typ mehr, einheitliches Detail-Layout mit ein-/ausblendbaren Bloecken, Logo/Brand pro Slide, Streichpreis-Option
- Player: leere Module stoppen Endlosschleife, dynamische Layout-Anpassung bei verstecktem Footer/Header
- Fix: Script-Ladereihenfolge (Livewire vor Flux), entfernte stale public/flux/flux.js, Modal-Crash beim Aktualisieren behoben

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:57:33 +00:00

478 lines
23 KiB
PHP

<?php
use function Livewire\Volt\{state, mount, computed};
state([
'hubId' => null,
'activeTab' => 'identity',
'importMethod' => 'single',
// Identität
'name' => '',
'slug' => '',
'keyvisual' => null,
'emblem' => null,
'is_active' => false,
// PLZ-Management
'zipSearch' => '',
'newZipCode' => '',
'newCityName' => '',
'rangeStart' => '',
'rangeEnd' => '',
'csvFile' => null,
]);
mount(function ($hubId = null) {
if ($hubId) {
$hub = \App\Models\Hub::findOrFail($hubId);
$this->hubId = $hub->id;
$this->name = $hub->name;
$this->slug = $hub->slug;
$this->is_active = $hub->is_active;
}
});
// Auto-generate slug from name
$updatedName = function ($value) {
if (!$this->hubId) {
// Only auto-generate for new hubs
$this->slug = \Illuminate\Support\Str::slug($value);
}
};
$locations = computed(function () {
if (!$this->hubId) {
return collect();
}
return \App\Models\HubLocation::where('hub_id', $this->hubId)->when($this->zipSearch, fn($q) => $q->where('zip_code', 'like', "%{$this->zipSearch}%")->orWhere('city_name', 'like', "%{$this->zipSearch}%"))->orderBy('zip_code')->paginate(50);
});
$partners = computed(function () {
if (!$this->hubId) {
return collect();
}
return \App\Models\Partner::where('hub_id', $this->hubId)->get();
});
// Dummy save function
$save = function () {
// In production: Validation und Speicherung
session()->flash('message', __('Hub gespeichert (Dummy-Funktion)'));
};
// Dummy functions für PLZ-Management
$addSingleZip = function () {
session()->flash('message', __('PLZ hinzugefügt (Dummy-Funktion)'));
};
$addZipRange = function () {
session()->flash('message', __('PLZ-Bereich importiert (Dummy-Funktion)'));
};
$importCsv = function () {
session()->flash('message', __('CSV importiert (Dummy-Funktion)'));
};
$deleteLocation = function ($id) {
session()->flash('message', __('PLZ gelöscht (Dummy-Funktion)'));
};
?>
<div class="space-y-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">
{{ $hubId ? __('Hub bearbeiten') : __('Neuer Hub') }} (in Entwicklung)
</flux:heading>
<flux:subheading>{{ $name ?: __('Regionalen Marktplatz konfigurieren') }}</flux:subheading>
</div>
<div class="flex gap-2">
<flux:button variant="ghost" :href="route('admin.hubs.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
<flux:button variant="primary" icon="check" wire:click="save">
{{ __('Speichern') }}
</flux:button>
</div>
</div>
{{-- Flash Message --}}
@if (session()->has('message'))
<flux:card class="p-4 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<div class="flex items-center gap-3">
<flux:icon.check-circle class="h-5 w-5 text-green-600 dark:text-green-400" />
<span class="text-sm text-green-900 dark:text-green-100">{{ session('message') }}</span>
</div>
</flux:card>
@endif
{{-- Tabs --}}
<flux:tabs wire:model.live="activeTab" variant="segmented">
<flux:tab name="identity" icon="identification">{{ __('Identität & Design') }}</flux:tab>
<flux:tab name="geography" icon="map">{{ __('Geografie & PLZ') }}</flux:tab>
<flux:tab name="partners" icon="user-group">{{ __('Partner-Monitor') }}</flux:tab>
</flux:tabs>
{{-- TAB 1: Identität & Design --}}
@if ($activeTab === 'identity')
<flux:card class="p-6 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- Basis-Informationen --}}
<div class="space-y-6">
<flux:field>
<flux:label>{{ __('Hub-Name') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model.live="name" placeholder="Ostwestfalen-Lippe" />
<flux:description>{{ __('Wird dem Kunden angezeigt, z.B. "Region Ostwestfalen-Lippe"') }}
</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('URL-Slug') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="slug" placeholder="owl" />
<flux:description>{{ __('Für saubere URLs, z.B. b2in.eu/region/owl') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:checkbox wire:model="is_active">
{{ __('Hub ist aktiv und für Kunden sichtbar') }}
</flux:checkbox>
<flux:description>
{{ __('Inaktive Hubs sind im "Coming Soon"-Modus') }}
</flux:description>
</flux:field>
</div>
{{-- Vorschau --}}
<div class="p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg">
<h4 class="font-semibold mb-3 text-zinc-900 dark:text-zinc-100">
{{ __('Vorschau: Kunden-Landingpage') }}</h4>
<div class="relative h-48 rounded-lg overflow-hidden bg-zinc-200 dark:bg-zinc-700 shadow-lg">
@if ($keyvisual)
<img src="{{ $keyvisual }}" class="w-full h-full object-cover" alt="Keyvisual" />
@else
<div class="flex items-center justify-center h-full">
<div class="text-center">
<flux:icon.photo class="w-12 h-12 text-zinc-400 mx-auto mb-2" />
<p class="text-sm text-zinc-500">{{ __('Keyvisual hochladen') }}</p>
</div>
</div>
@endif
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 to-transparent">
<h3 class="text-xl font-bold text-white">
{{ $name ?: __('Ihr Hub-Name') }}
</h3>
<p class="text-sm text-zinc-200">
{{ __('Die besten Marken Europas, von Händlern aus Ihrer Nachbarschaft') }}
</p>
</div>
@if ($emblem)
<div class="absolute top-3 right-3 w-12 h-12 bg-white rounded-full p-2 shadow-lg">
<img src="{{ $emblem }}" alt="Emblem" class="w-full h-full object-contain" />
</div>
@endif
</div>
</div>
</div>
<flux:separator />
{{-- Keyvisual Upload --}}
<flux:field>
<flux:label>{{ __('Keyvisual (Hintergrundbild)') }}</flux:label>
{{-- <flux:input type="file" wire:model="keyvisual" accept="image/*" /> --}}
<flux:description>
{{ __('Atmosphärisches Regionalbild für emotionalen Einstieg') }}
<br>
<span class="text-xs">
{{ __('Beispiele: Hermannsdenkmal (OWL), Skyline (Frankfurt), Hafen (Hamburg)') }}
• {{ __('Empfohlen: 1920x800px, max. 2MB') }}
</span>
</flux:description>
</flux:field>
{{-- Wappen Upload --}}
<flux:field>
<flux:label>{{ __('Wappen / Emblem') }}</flux:label>
{{-- <flux:input type="file" wire:model="emblem" accept="image/*" /> --}}
<flux:description>
{{ __('Offizielles Wappen oder Logo der Region für Vertrauen & Offizialität') }}
<br>
<span class="text-xs">
{{ __('Beispiele: Sparrenburg-Logo, Landeswappen') }}
• {{ __('Empfohlen: 256x256px, PNG mit transparentem Hintergrund') }}
</span>
</flux:description>
</flux:field>
</flux:card>
@endif
{{-- TAB 2: Geografie & PLZ --}}
@if ($activeTab === 'geography')
<div class="space-y-6">
{{-- Info-Box --}}
<flux:card class="p-4 bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
<div class="flex items-start gap-3">
<flux:icon.light-bulb class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div class="flex-1">
<div class="text-sm font-semibold text-yellow-900 dark:text-yellow-100">
{{ __('Die Mapping-Engine') }}
</div>
<div class="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
{{ __('Hier ordnen Sie Postleitzahlen diesem Hub zu. Gibt ein Kunde seine PLZ ein, wird er automatisch diesem regionalen Marktplatz zugewiesen und sieht lokale Händler.') }}
</div>
</div>
</div>
</flux:card>
{{-- PLZ-Import Tools --}}
<flux:card class="p-6">
<flux:heading size="lg" class="mb-4">{{ __('Postleitzahlen hinzufügen') }}</flux:heading>
<flux:tabs wire:model.live="importMethod" variant="segmented">
<flux:tab name="single" icon="plus">{{ __('Einzeln') }}</flux:tab>
<flux:tab name="range" icon="arrows-right-left">{{ __('Bereich') }}</flux:tab>
<flux:tab name="csv" icon="document">{{ __('CSV-Import') }}</flux:tab>
</flux:tabs>
<div class="mt-6">
{{-- Einzelne PLZ --}}
@if ($importMethod === 'single')
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<flux:field>
<flux:label>{{ __('Postleitzahl') }} <span class="text-red-500">*</span>
</flux:label>
<flux:input wire:model="newZipCode" placeholder="33602" />
</flux:field>
<flux:field>
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="newCityName" placeholder="Bielefeld" />
</flux:field>
</div>
<flux:button wire:click="addSingleZip" icon="plus">
{{ __('PLZ hinzufügen') }}
</flux:button>
</div>
@endif
{{-- PLZ-Bereich --}}
@if ($importMethod === 'range')
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<flux:field>
<flux:label>{{ __('Von PLZ') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="rangeStart" placeholder="33000" />
</flux:field>
<flux:field>
<flux:label>{{ __('Bis PLZ') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="rangeEnd" placeholder="33999" />
</flux:field>
</div>
<flux:button wire:click="addZipRange" icon="arrows-right-left">
{{ __('Bereich importieren') }}
</flux:button>
<flux:description>
⚠️
{{ __('Generiert automatisch alle PLZs im Bereich. Kann bei großen Bereichen mehrere Minuten dauern!') }}
</flux:description>
</div>
@endif
{{-- CSV-Import --}}
@if ($importMethod === 'csv')
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('CSV-Datei') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="file" wire:model="csvFile" accept=".csv" />
<flux:description>
{{ __('Format: PLZ,Stadt (eine Zeile pro Eintrag)') }}
<br>
<span class="text-xs">{{ __('Beispiel:') }}</span>
<code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded">
33602,Bielefeld<br>33603,Bielefeld<br>33604,Bielefeld
</code>
</flux:description>
</flux:field>
<flux:button wire:click="importCsv" icon="arrow-up-tray">
{{ __('CSV importieren') }}
</flux:button>
</div>
@endif
</div>
</flux:card>
{{-- PLZ-Liste --}}
<flux:card>
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<flux:heading size="lg">
{{ __('Zugeordnete Postleitzahlen') }}
@if ($hubId)
<span class="text-sm font-normal text-zinc-600 dark:text-zinc-400">
({{ $this->locations->total() }} {{ __('gesamt') }})
</span>
@endif
</flux:heading>
<flux:input wire:model.live.debounce="zipSearch"
placeholder="{{ __('PLZ oder Stadt suchen...') }}" icon="magnifying-glass"
class="w-64" />
</div>
@if ($hubId)
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('PLZ') }}</flux:table.column>
<flux:table.column>{{ __('Stadt') }}</flux:table.column>
<flux:table.column class="text-right w-32">{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($this->locations as $location)
<flux:table.row :key="$location->id">
<flux:table.cell>
<span class="font-mono">{{ $location->zip_code }}</span>
</flux:table.cell>
<flux:table.cell>{{ $location->city_name }}</flux:table.cell>
<flux:table.cell class="text-right">
<flux:button variant="ghost" size="sm" icon="trash"
wire:click="deleteLocation({{ $location->id }})" />
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="3" class="text-center py-8">
<flux:icon.map-pin class="w-12 h-12 text-zinc-400 mx-auto mb-2" />
<p class="text-zinc-600 dark:text-zinc-400">
{{ __('Noch keine PLZs zugeordnet') }}
</p>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
{{-- Pagination --}}
@if ($this->locations->hasPages())
<div class="mt-4 border-t border-zinc-200 dark:border-zinc-700 pt-4">
{{ $this->locations->links() }}
</div>
@endif
@else
<div class="text-center py-8 text-zinc-600 dark:text-zinc-400">
{{ __('Speichern Sie zuerst den Hub, um PLZs hinzuzufügen') }}
</div>
@endif
</div>
</flux:card>
</div>
@endif
{{-- TAB 3: Partner-Monitor --}}
@if ($activeTab === 'partners')
<flux:card class="p-6">
<flux:heading size="lg" class="mb-4">
{{ __('Partner in diesem Hub') }}
@if ($hubId)
<span class="text-sm font-normal text-zinc-600 dark:text-zinc-400">
({{ $this->partners->count() }} {{ __('Partner') }})
</span>
@endif
</flux:heading>
{{-- Info --}}
<flux:card class="p-4 mb-4 bg-zinc-50 dark:bg-zinc-800 border-0">
<div class="text-sm text-zinc-600 dark:text-zinc-400">
<strong>{{ __('Logik:') }}</strong>
<ul class="list-disc ml-5 mt-2 space-y-1">
<li>{{ __('Händler (Retailer) → Werden diesem Hub fest zugeordnet') }}</li>
<li>{{ __('Hersteller (Manufacturer) → Sind "Hub-agnostisch", in allen Hubs sichtbar') }}</li>
<li>{{ __('Makler (Estate-Agent) → Werden Hub zugeordnet für regionale Kundenakquise') }}</li>
</ul>
</div>
</flux:card>
@if ($hubId)
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Name') }}</flux:table.column>
<flux:table.column>{{ __('Typ') }}</flux:table.column>
<flux:table.column>{{ __('Stadt') }}</flux:table.column>
<flux:table.column>{{ __('Lieferradius') }}</flux:table.column>
<flux:table.column class="text-center">{{ __('Status') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($this->partners as $partner)
<flux:table.row :key="$partner->id">
<flux:table.cell>
<div class="font-semibold text-zinc-900 dark:text-zinc-100">
{{ $partner->company_name }}
</div>
@if ($partner->display_name && $partner->display_name !== $partner->company_name)
<div class="text-xs text-zinc-500">{{ $partner->display_name }}</div>
@endif
</flux:table.cell>
<flux:table.cell>
@php
$typeColors = [
'Retailer' => 'blue',
'Manufacturer' => 'purple',
'Estate-Agent' => 'green',
];
@endphp
<flux:badge :color="$typeColors[$partner->type] ?? 'zinc'" size="sm">
{{ $partner->type }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
{{ $partner->city ?? '-' }}
</flux:table.cell>
<flux:table.cell>
@if ($partner->delivery_radius_km)
<span class="text-sm">{{ $partner->delivery_radius_km }} km</span>
@else
<span class="text-sm text-zinc-400">-</span>
@endif
</flux:table.cell>
<flux:table.cell class="text-center">
<flux:badge :color="$partner->is_active ? 'green' : 'zinc'" size="sm">
{{ $partner->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
</flux:table.cell>
<flux:table.cell class="text-right">
<flux:button variant="ghost" size="sm" icon="eye">
{{ __('Details') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6" class="text-center py-8">
<flux:icon.user-group class="w-12 h-12 text-zinc-400 mx-auto mb-2" />
<p class="text-zinc-600 dark:text-zinc-400">
{{ __('Noch keine Partner in diesem Hub') }}
</p>
<p class="text-xs text-zinc-500 mt-2">
{{ __('Partner werden automatisch beim Onboarding zugeordnet (basierend auf PLZ)') }}
</p>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@else
<div class="text-center py-8 text-zinc-600 dark:text-zinc-400">
{{ __('Speichern Sie zuerst den Hub, um Partner-Zuordnungen zu sehen') }}
</div>
@endif
</flux:card>
@endif
</div>