482 lines
21 KiB
PHP
482 lines
21 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.de/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>
|
|
|