23-01-2026
This commit is contained in:
parent
07959c0ba2
commit
854ce02bf6
166 changed files with 32909 additions and 1262 deletions
307
resources/views/livewire/admin/c-m-s/cabinet-display.blade.php
Normal file
307
resources/views/livewire/admin/c-m-s/cabinet-display.blade.php
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
<div>
|
||||
<flux:header class="mb-6">
|
||||
<flux:heading size="xl">{{ __('Cabinet Display - CMS Verwaltung') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Verwalten Sie die Inhalte der Display-Seite') }}</flux:subheading>
|
||||
</flux:header>
|
||||
|
||||
{{-- Hilfe-Banner --}}
|
||||
<flux:card class="mb-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-start gap-4">
|
||||
<flux:icon.information-circle class="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">{{ __('Schnellanleitung') }}</h3>
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200 space-y-1">
|
||||
<p>• <strong>Videos:</strong> Videos müssen aufgrund der Dateigrößer vorab per SFTP hochgeladen werden. Die Position (0-100%) bestimmt den vertikalen Bildausschnitt.</p>
|
||||
<p>• <strong>Footer-Inhalte:</strong> Werden alle 30 Sekunden gewechselt. URLs werden automatisch als QR-Code angezeigt.</p>
|
||||
<p>• <strong>Footer-Inhalte:</strong> Sind alle Inhalte ausgeblendet, wird der Footer ausgeblendet und das Video auf 100% der Höhe angezeigt.</p>
|
||||
<p>• <strong>Display-URL:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded">https://cabinet.b2in.eu</code></p>
|
||||
<p>• <strong>API-Endpunkt:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded">{{ url('/api/display/config') }}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Success-Meldungen --}}
|
||||
@if (session()->has('success'))
|
||||
<x-success-alert>
|
||||
{{ session('success') }}
|
||||
</x-success-alert>
|
||||
@endif
|
||||
|
||||
{{-- Video-Verwaltung --}}
|
||||
<flux:card class="mb-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Video-Playlist') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Videos werden in der angegebenen Reihenfolge abgespielt') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button wire:click="openVideoModal" icon="plus">
|
||||
{{ __('Video hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if($videos->isEmpty())
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.film class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>{{ __('Noch keine Videos vorhanden. Fügen Sie Ihr erstes Video hinzu!') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($videos as $index => $video)
|
||||
<div wire:key="video-{{ $video->id }}"
|
||||
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($index > 0)
|
||||
<flux:button wire:click="moveVideo({{ $video->id }}, 'up')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-up"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($index < count($videos) - 1)
|
||||
<flux:button wire:click="moveVideo({{ $video->id }}, 'down')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-down"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$video->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $video->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
<span class="font-semibold text-sm">{{ $video->title ?: $video->filename }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
|
||||
<span>📁 {{ $video->filename }}</span>
|
||||
<span>📍 Position: {{ $video->position }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button wire:click="toggleVideoStatus({{ $video->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
:icon="$video->is_active ? 'eye-slash' : 'eye'">
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="openVideoModal({{ $video->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="pencil">
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="deleteVideo({{ $video->id }})"
|
||||
wire:confirm="Möchten Sie dieses Video wirklich löschen?"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="trash"
|
||||
class="text-red-600 hover:text-red-700">
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Footer-Content-Verwaltung --}}
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Footer-Inhalte') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Inhalte werden alle 30 Sekunden im Footer gewechselt') }}</flux:subheading>
|
||||
@if($footerContents->isNotEmpty())
|
||||
<div class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
📊 Gesamt-Klicks: <strong>{{ $footerContents->sum('clicks') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<flux:button wire:click="openFooterModal" icon="plus">
|
||||
{{ __('Inhalt hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if($footerContents->isEmpty())
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.document-text class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>{{ __('Noch keine Footer-Inhalte vorhanden. Fügen Sie den ersten Inhalt hinzu!') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($footerContents as $index => $footer)
|
||||
<div wire:key="footer-{{ $footer->id }}"
|
||||
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($index > 0)
|
||||
<flux:button wire:click="moveFooter({{ $footer->id }}, 'up')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-up"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($index < count($footerContents) - 1)
|
||||
<flux:button wire:click="moveFooter({{ $footer->id }}, 'down')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-down"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$footer->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $footer->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
<span class="font-semibold text-sm">{{ $footer->headline }}</span>
|
||||
<flux:badge color="blue" size="sm">
|
||||
<flux:icon.cursor-arrow-rays class="w-3 h-3" />
|
||||
{{ $footer->clicks }} {{ __('Klicks') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-y-1">
|
||||
<div>{{ $footer->subline }}</div>
|
||||
|
||||
@if($footer->url)
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.link class="w-3 h-3" />
|
||||
<span class="font-mono bg-zinc-100 dark:bg-zinc-700 px-2 py-0.5 rounded">
|
||||
{{ $footer->short_code }}
|
||||
</span>
|
||||
<button
|
||||
onclick="navigator.clipboard.writeText('{{ $footer->short_url }}'); alert('Short-Link kopiert!');"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 text-xs"
|
||||
title="Short-Link kopieren">
|
||||
📋 Short-Link
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs">
|
||||
<flux:icon.arrow-top-right-on-square class="w-3 h-3" />
|
||||
<a href="{{ $footer->url }}" target="_blank" class="hover:underline">
|
||||
{{ Str::limit($footer->url, 50) }}
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-500">
|
||||
<flux:icon.x-circle class="w-3 h-3" />
|
||||
<span>Kein QR-Code (Keine URL angegeben)</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button wire:click="toggleFooterStatus({{ $footer->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
:icon="$footer->is_active ? 'eye-slash' : 'eye'"
|
||||
title="{{ $footer->is_active ? 'Deaktivieren' : 'Aktivieren' }}">
|
||||
</flux:button>
|
||||
|
||||
<flux:dropdown>
|
||||
<flux:button size="sm" variant="ghost" icon="ellipsis-vertical"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item wire:click="openFooterModal({{ $footer->id }})" icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item wire:click="regenerateShortCode({{ $footer->id }})" icon="arrow-path">
|
||||
{{ __('Short-Code neu generieren') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item wire:click="resetClicks({{ $footer->id }})"
|
||||
wire:confirm="Möchten Sie den Klick-Zähler wirklich zurücksetzen?"
|
||||
icon="arrow-path-rounded-square">
|
||||
{{ __('Klicks zurücksetzen') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item wire:click="deleteFooter({{ $footer->id }})"
|
||||
wire:confirm="Möchten Sie diesen Footer-Inhalt wirklich löschen?"
|
||||
icon="trash"
|
||||
class="text-red-600">
|
||||
{{ __('Löschen') }}
|
||||
</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Video Modal --}}
|
||||
<flux:modal :open="$showVideoModal" wire:model="showVideoModal">
|
||||
<form wire:submit.prevent="saveVideo">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $videoId ? __('Video bearbeiten') : __('Video hinzufügen') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:select wire:model="videoFilename" label="Video-Datei" placeholder="Wählen Sie ein Video...">
|
||||
@foreach($availableVideos as $videoFile)
|
||||
<option value="{{ $videoFile }}">{{ $videoFile }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
@error('videoFilename') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:input wire:model="videoTitle" label="Titel (optional)" placeholder="z.B. Herbst Kollektion 2025" />
|
||||
|
||||
<flux:input wire:model="videoPosition" type="number" min="0" max="100" label="Position (%)"
|
||||
description="Vertikale Position im Video (0 = oben, 100 = unten)" />
|
||||
@error('videoPosition') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:checkbox wire:model="videoIsActive" label="Video aktiv anzeigen" />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<flux:button type="button" wire:click="closeVideoModal" variant="ghost">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ $videoId ? __('Aktualisieren') : __('Hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Footer Modal --}}
|
||||
<flux:modal :open="$showFooterModal" wire:model="showFooterModal">
|
||||
<form wire:submit.prevent="saveFooter">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $footerId ? __('Footer-Inhalt bearbeiten') : __('Footer-Inhalt hinzufügen') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="footerHeadline" label="Überschrift" placeholder="z.B. Beratung & Termin" />
|
||||
@error('footerHeadline') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:input wire:model="footerSubline" label="Unterzeile" placeholder="z.B. Jetzt Termin vereinbaren." />
|
||||
@error('footerSubline') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:input wire:model="footerUrl" label="URL (optional)" placeholder="https://www.cabinet.de/bielefeld..."
|
||||
description="Leer lassen = Kein QR-Code wird angezeigt. Mit URL = QR-Code mit Short-Link wird generiert." />
|
||||
@error('footerUrl') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:checkbox wire:model="footerIsActive" label="Footer-Inhalt aktiv anzeigen" />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<flux:button type="button" wire:click="closeFooterModal" variant="ghost">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ $footerId ? __('Aktualisieren') : __('Hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
508
resources/views/livewire/admin/documentation.blade.php
Normal file
508
resources/views/livewire/admin/documentation.blade.php
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, computed};
|
||||
use Illuminate\Support\Str;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
|
||||
state(['showToc' => false]);
|
||||
|
||||
$content = computed(function () {
|
||||
$mdPath = base_path('dev/entwicklung.md');
|
||||
|
||||
if (!file_exists($mdPath)) {
|
||||
return '<p class="text-red-600">Dokumentation nicht gefunden.</p>';
|
||||
}
|
||||
|
||||
$markdown = file_get_contents($mdPath);
|
||||
|
||||
// Configure CommonMark with GitHub Flavored Markdown
|
||||
$environment = new Environment([
|
||||
'html_input' => 'allow',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new GithubFlavoredMarkdownExtension());
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
|
||||
return $converter->convert($markdown)->getContent();
|
||||
});
|
||||
|
||||
// Extract Table of Contents from markdown
|
||||
$tableOfContents = computed(function () {
|
||||
$mdPath = base_path('dev/entwicklung.md');
|
||||
|
||||
if (!file_exists($mdPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$markdown = file_get_contents($mdPath);
|
||||
$toc = [];
|
||||
|
||||
// Extract headings (## and ###)
|
||||
preg_match_all('/^(#{2,3})\s+(.+)$/m', $markdown, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$level = strlen($match[1]);
|
||||
$title = trim($match[2]);
|
||||
$slug = Str::slug($title);
|
||||
|
||||
$toc[] = [
|
||||
'level' => $level,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
];
|
||||
}
|
||||
|
||||
return $toc;
|
||||
});
|
||||
|
||||
$fileInfo = computed(function () {
|
||||
$mdPath = base_path('dev/entwicklung.md');
|
||||
|
||||
if (!file_exists($mdPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'size' => round(filesize($mdPath) / 1024, 1) . ' KB',
|
||||
'modified' => \Carbon\Carbon::parse(filemtime($mdPath))->format('d.m.Y H:i'),
|
||||
'lines' => count(file($mdPath)),
|
||||
];
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
<div class="space-y-6 pb-12">
|
||||
{{-- Sticky Header --}}
|
||||
<div class="sticky top-0 z-10 bg-zinc-50 dark:bg-zinc-800 pb-4 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Projekt-Dokumentation') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Entwicklungsstand & Technische Übersicht') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="list-bullet"
|
||||
wire:click="$toggle('showToc')"
|
||||
>
|
||||
{{ __('Inhaltsverzeichnis') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="arrow-down-tray"
|
||||
href="{{ route('admin.documentation.download') }}"
|
||||
>
|
||||
{{ __('Download') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- File Info Bar --}}
|
||||
@if($this->fileInfo)
|
||||
<div class="flex items-center gap-4 mt-3 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.document-text class="w-3 h-3" />
|
||||
<span>{{ $this->fileInfo['size'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.clock class="w-3 h-3" />
|
||||
<span>{{ __('Aktualisiert:') }} {{ $this->fileInfo['modified'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.bars-3 class="w-3 h-3" />
|
||||
<span>{{ $this->fileInfo['lines'] }} {{ __('Zeilen') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Table of Contents Sidebar (Collapsible) --}}
|
||||
@if($showToc)
|
||||
<flux:card class="p-6 bg-gradient-to-br from-accent-50 to-blue-50 dark:from-accent-900/20 dark:to-blue-900/20 border-accent-200 dark:border-accent-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<flux:heading size="lg">{{ __('Inhaltsverzeichnis') }}</flux:heading>
|
||||
<flux:button variant="ghost" size="sm" icon="x-mark" wire:click="$set('showToc', false)" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@foreach($this->tableOfContents as $item)
|
||||
<a href="#{{ $item['slug'] }}"
|
||||
class="block py-1.5 px-3 rounded-lg hover:bg-white dark:hover:bg-zinc-800 transition-colors
|
||||
{{ $item['level'] === 2 ? 'font-semibold text-zinc-900 dark:text-zinc-100' : 'ml-4 text-sm text-zinc-700 dark:text-zinc-300' }}"
|
||||
wire:click="$set('showToc', false)">
|
||||
{{ $item['level'] === 3 ? '• ' : '' }}{{ $item['title'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Markdown Content with Enhanced Styling --}}
|
||||
<div class="relative">
|
||||
<flux:card class="p-6 lg:p-8 shadow-lg">
|
||||
<style>
|
||||
/* Force headings visibility and styling - reduced by 30% */
|
||||
.prose h1 {
|
||||
display: block !important;
|
||||
font-size: 2.1rem !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-top: 2.1rem !important;
|
||||
margin-bottom: 1.4rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
border-bottom: 2px solid #e5e7eb !important;
|
||||
background: linear-gradient(to right, #3b82f6, #2563eb) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
display: block !important;
|
||||
font-size: 1.3125rem !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3 !important;
|
||||
margin-top: 2.1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
border-bottom: 1px solid #e5e7eb !important;
|
||||
scroll-margin-top: 5rem !important;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
display: block !important;
|
||||
font-size: 1.05rem !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.4 !important;
|
||||
margin-top: 1.4rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
scroll-margin-top: 5rem !important;
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
display: block !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.5 !important;
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Dark mode headings */
|
||||
.dark .prose h1 {
|
||||
border-bottom-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .prose h2 {
|
||||
border-bottom-color: #374151 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.dark .prose h3 {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .prose h4 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Force list indentation and visibility - reduced by 30% */
|
||||
.prose ul {
|
||||
list-style-type: disc !important;
|
||||
margin-left: 1.75rem !important;
|
||||
padding-left: 0.7rem !important;
|
||||
margin-top: 0.7rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal !important;
|
||||
margin-left: 1.75rem !important;
|
||||
padding-left: 0.7rem !important;
|
||||
margin-top: 0.7rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
display: list-item !important;
|
||||
margin-bottom: 0.35rem !important;
|
||||
padding-left: 0.35rem !important;
|
||||
font-size: 0.95rem !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
.prose ul ul, .prose ol ol,
|
||||
.prose ul ol, .prose ol ul {
|
||||
margin-left: 1.4rem !important;
|
||||
margin-top: 0.35rem !important;
|
||||
margin-bottom: 0.35rem !important;
|
||||
}
|
||||
|
||||
/* Paragraphs - reduced by 30% */
|
||||
.prose p {
|
||||
margin-top: 0.7rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
line-height: 1.6 !important;
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
/* Strong/Bold */
|
||||
.prose strong {
|
||||
font-weight: 700 !important;
|
||||
color: #111827 !important;
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
.dark .prose strong {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* Code - reduced by 30% */
|
||||
.prose code {
|
||||
background-color: #f3f4f6 !important;
|
||||
padding: 0.175rem 0.35rem !important;
|
||||
border-radius: 0.25rem !important;
|
||||
font-size: 0.8rem !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.dark .prose code {
|
||||
background-color: #1f2937 !important;
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
/* HR - reduced by 30% */
|
||||
.prose hr {
|
||||
margin-top: 2.1rem !important;
|
||||
margin-bottom: 2.1rem !important;
|
||||
border-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .prose hr {
|
||||
border-color: #374151 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none
|
||||
{{-- Headings --}}
|
||||
prose-headings:font-bold prose-headings:tracking-tight
|
||||
prose-h1:text-5xl prose-h1:mb-8 prose-h1:mt-12 prose-h1:pb-6
|
||||
prose-h1:border-b-2 prose-h1:border-accent-300 dark:prose-h1:border-accent-700
|
||||
prose-h1:text-transparent prose-h1:bg-clip-text prose-h1:bg-gradient-to-r prose-h1:from-accent-600 prose-h1:to-blue-600 dark:prose-h1:from-accent-400 dark:prose-h1:to-blue-400
|
||||
|
||||
prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h2:pb-3
|
||||
prose-h2:border-b prose-h2:border-zinc-200 dark:prose-h2:border-zinc-700
|
||||
prose-h2:text-zinc-900 dark:prose-h2:text-zinc-100
|
||||
prose-h2:scroll-mt-20
|
||||
|
||||
prose-h3:text-2xl prose-h3:mb-4 prose-h3:mt-8
|
||||
prose-h3:text-zinc-800 dark:prose-h3:text-zinc-200
|
||||
prose-h3:scroll-mt-20
|
||||
|
||||
prose-h4:text-xl prose-h4:mb-3 prose-h4:mt-6
|
||||
prose-h4:text-zinc-700 dark:prose-h4:text-zinc-300
|
||||
|
||||
{{-- Paragraphs & Text --}}
|
||||
prose-p:mb-6 prose-p:leading-relaxed prose-p:text-zinc-700 dark:prose-p:text-zinc-300
|
||||
|
||||
{{-- Links --}}
|
||||
prose-a:text-accent-600 dark:prose-a:text-accent-400
|
||||
prose-a:font-medium prose-a:no-underline
|
||||
prose-a:transition-all prose-a:duration-200
|
||||
hover:prose-a:text-accent-700 dark:hover:prose-a:text-accent-300
|
||||
hover:prose-a:underline hover:prose-a:decoration-2 hover:prose-a:underline-offset-4
|
||||
|
||||
{{-- Strong & Emphasis --}}
|
||||
prose-strong:text-zinc-900 dark:prose-strong:text-zinc-100
|
||||
prose-strong:font-bold
|
||||
prose-em:text-zinc-800 dark:prose-em:text-zinc-200
|
||||
|
||||
{{-- Code --}}
|
||||
prose-code:bg-accent-100 dark:prose-code:bg-accent-900/30
|
||||
prose-code:text-accent-900 dark:prose-code:text-accent-100
|
||||
prose-code:px-2 prose-code:py-1 prose-code:rounded-md
|
||||
prose-code:text-sm prose-code:font-mono prose-code:font-semibold
|
||||
prose-code:before:content-none prose-code:after:content-none
|
||||
prose-code:border prose-code:border-accent-200 dark:prose-code:border-accent-800
|
||||
|
||||
{{-- Code Blocks --}}
|
||||
prose-pre:bg-gradient-to-br prose-pre:from-zinc-900 prose-pre:to-zinc-800
|
||||
dark:prose-pre:from-zinc-950 dark:prose-pre:to-zinc-900
|
||||
prose-pre:p-6 prose-pre:rounded-xl prose-pre:overflow-x-auto
|
||||
prose-pre:shadow-xl prose-pre:border prose-pre:border-zinc-700 dark:prose-pre:border-zinc-800
|
||||
prose-pre:my-8
|
||||
|
||||
{{-- Lists --}}
|
||||
prose-ul:list-disc prose-ul:ml-8 prose-ul:pl-6 prose-ul:mb-6 prose-ul:space-y-2
|
||||
prose-ol:list-decimal prose-ol:ml-8 prose-ol:pl-6 prose-ol:mb-6 prose-ol:space-y-2
|
||||
prose-li:text-zinc-700 dark:prose-li:text-zinc-300
|
||||
prose-li:leading-relaxed prose-li:pl-3
|
||||
prose-li:marker:text-accent-500 dark:prose-li:marker:text-accent-400
|
||||
prose-li:marker:font-bold
|
||||
|
||||
{{-- Nested Lists --}}
|
||||
prose-ul prose-ul:ml-8 prose-ul prose-ul:space-y-1
|
||||
prose-ol prose-ol:ml-8 prose-ol prose-ol:space-y-1
|
||||
|
||||
{{-- Blockquotes --}}
|
||||
prose-blockquote:border-l-4 prose-blockquote:border-accent-500 dark:prose-blockquote:border-accent-400
|
||||
prose-blockquote:bg-accent-50 dark:prose-blockquote:bg-accent-900/10
|
||||
prose-blockquote:pl-6 prose-blockquote:pr-4 prose-blockquote:py-4
|
||||
prose-blockquote:italic prose-blockquote:text-zinc-700 dark:prose-blockquote:text-zinc-300
|
||||
prose-blockquote:rounded-r-lg prose-blockquote:my-8
|
||||
prose-blockquote:shadow-sm
|
||||
|
||||
{{-- HR --}}
|
||||
prose-hr:border-zinc-300 dark:prose-hr:border-zinc-600
|
||||
prose-hr:my-12 prose-hr:border-t-2
|
||||
|
||||
{{-- Tables --}}
|
||||
prose-table:border-collapse prose-table:w-full
|
||||
prose-table:shadow-lg prose-table:rounded-lg prose-table:overflow-hidden
|
||||
prose-table:my-8
|
||||
|
||||
prose-thead:bg-gradient-to-r prose-thead:from-accent-100 prose-thead:to-blue-100
|
||||
dark:prose-thead:from-accent-900/40 dark:prose-thead:to-blue-900/40
|
||||
|
||||
prose-th:p-4 prose-th:text-left prose-th:font-bold
|
||||
prose-th:text-zinc-900 dark:prose-th:text-zinc-100
|
||||
prose-th:border-b-2 prose-th:border-accent-300 dark:prose-th:border-accent-700
|
||||
|
||||
prose-td:p-4 prose-td:border-b prose-td:border-zinc-200 dark:prose-td:border-zinc-700
|
||||
prose-td:text-zinc-700 dark:prose-td:text-zinc-300
|
||||
|
||||
prose-tr:transition-colors
|
||||
hover:prose-tr:bg-zinc-50 dark:hover:prose-tr:bg-zinc-800/50
|
||||
|
||||
{{-- Images --}}
|
||||
prose-img:rounded-xl prose-img:shadow-2xl prose-img:my-8
|
||||
prose-img:border prose-img:border-zinc-200 dark:prose-img:border-zinc-700
|
||||
">
|
||||
{!! $this->content !!}
|
||||
</div>
|
||||
|
||||
{{-- Scroll to top button --}}
|
||||
<div class="flex justify-center mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
icon="arrow-up"
|
||||
onclick="window.scrollTo({top: 0, behavior: 'smooth'})"
|
||||
>
|
||||
{{ __('Zurück nach oben') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Quick Reference Grid --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<flux:card class="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-blue-500 rounded-lg">
|
||||
<flux:icon.cube class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<flux:heading size="sm" class="text-blue-900 dark:text-blue-100">{{ __('Module') }}</flux:heading>
|
||||
</div>
|
||||
<ul class="space-y-2 text-sm text-blue-800 dark:text-blue-200">
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Multi-Domain-System
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Benutzer-Management
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Partner-Verwaltung
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Hub-System
|
||||
</li>
|
||||
</ul>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-6 bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-purple-500 rounded-lg">
|
||||
<flux:icon.code-bracket class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<flux:heading size="sm" class="text-purple-900 dark:text-purple-100">{{ __('Technologien') }}</flux:heading>
|
||||
</div>
|
||||
<ul class="space-y-2 text-sm text-purple-800 dark:text-purple-200">
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Laravel 12
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Livewire 3 + Volt
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Flux UI Pro
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Spatie Permissions
|
||||
</li>
|
||||
</ul>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-6 bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-green-500 rounded-lg">
|
||||
<flux:icon.chart-bar class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<flux:heading size="sm" class="text-green-900 dark:text-green-100">{{ __('Status-Legende') }}</flux:heading>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="green" size="sm">✓</flux:badge>
|
||||
<span class="text-green-800 dark:text-green-200">{{ __('Vollständig implementiert') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="yellow" size="sm">●</flux:badge>
|
||||
<span class="text-green-800 dark:text-green-200">{{ __('In Entwicklung') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="zinc" size="sm">○</flux:badge>
|
||||
<span class="text-green-800 dark:text-green-200">{{ __('Geplant') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Footer Metadata --}}
|
||||
<flux:card class="p-6 bg-gradient-to-r from-zinc-50 to-zinc-100 dark:from-zinc-900 dark:to-zinc-800 border-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-6 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.document-text class="w-4 h-4" />
|
||||
<span class="font-mono">entwicklung.md</span>
|
||||
</div>
|
||||
@if($this->fileInfo)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.clock class="w-4 h-4" />
|
||||
<span>{{ __('Zuletzt aktualisiert:') }} {{ $this->fileInfo['modified'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<flux:badge color="accent" size="sm">
|
||||
{{ __('Live-Dokumentation') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
208
resources/views/livewire/admin/hubs/index.blade.php
Normal file
208
resources/views/livewire/admin/hubs/index.blade.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, computed};
|
||||
|
||||
state(['search' => '']);
|
||||
|
||||
$hubs = computed(function () {
|
||||
return \App\Models\Hub::with(['locations', 'partners'])
|
||||
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
|
||||
->get()
|
||||
->map(function ($hub) {
|
||||
$retailers = $hub->partners()->where('type', 'Retailer')->count();
|
||||
$brokers = $hub->partners()->where('type', 'Estate-Agent')->count();
|
||||
|
||||
return [
|
||||
'id' => $hub->id,
|
||||
'name' => $hub->name,
|
||||
'slug' => $hub->slug,
|
||||
'keyvisual' => $hub->keyvisual_url ?? '/images/default-keyvisual.jpg',
|
||||
'emblem' => $hub->emblem_url,
|
||||
'is_active' => $hub->is_active,
|
||||
'locations_count' => $hub->locations->count(),
|
||||
'retailers_count' => $retailers,
|
||||
'brokers_count' => $brokers,
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header mit Suche --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Hub-Verwaltung') }} (in Entwicklung)</flux:heading>
|
||||
<flux:subheading>{{ __('Regionale Marktplätze & Postleitzahlen-Zuordnung') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button variant="primary" icon="plus" :href="route('admin.hubs.create')" wire:navigate>
|
||||
{{ __('Neuer Hub') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- Info-Banner --}}
|
||||
<flux:card class="p-4 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:icon.information-circle class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-semibold text-blue-900 dark:text-blue-100">
|
||||
{{ __('Konzept: Heimatgefühl + Weltmarkt') }}
|
||||
</div>
|
||||
<div class="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
{{ __('Jeder Hub filtert lokale Händler heraus, behält aber globale Hersteller bei. Kunden fühlen sich "zuhause" mit Zugriff auf das volle Sortiment.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Suchfeld --}}
|
||||
<flux:input
|
||||
wire:model.live.debounce="search"
|
||||
placeholder="{{ __('Hub suchen...') }}"
|
||||
icon="magnifying-glass"
|
||||
/>
|
||||
|
||||
{{-- Hubs als Karten-Grid --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@forelse($this->hubs as $hub)
|
||||
<a href="{{ route('admin.hubs.edit', $hub['id']) }}"
|
||||
wire:navigate
|
||||
class="block relative overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700 hover:shadow-lg hover:border-accent-300 dark:hover:border-accent-700 transition-all group">
|
||||
|
||||
{{-- Keyvisual Hintergrund --}}
|
||||
<div class="relative h-48 bg-cover bg-center"
|
||||
style="background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.6)), url('{{ $hub['keyvisual'] }}')">
|
||||
|
||||
{{-- Wappen oben rechts --}}
|
||||
@if($hub['emblem'])
|
||||
<div class="absolute top-3 right-3 w-12 h-12 bg-white rounded-full p-2 shadow-lg">
|
||||
<img src="{{ $hub['emblem'] }}" alt="Wappen" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Status Badge oben links --}}
|
||||
<div class="absolute top-3 left-3">
|
||||
<flux:badge :color="$hub['is_active'] ? 'green' : 'zinc'" size="sm">
|
||||
{{ $hub['is_active'] ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
{{-- Hub Name --}}
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4">
|
||||
<h3 class="text-2xl font-bold text-white group-hover:text-accent-200 transition-colors">
|
||||
{{ $hub['name'] }}
|
||||
</h3>
|
||||
<p class="text-sm text-zinc-200">{{ $hub['slug'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Metriken --}}
|
||||
<div class="p-4 bg-white dark:bg-zinc-900">
|
||||
<div class="grid grid-cols-3 gap-2 text-center">
|
||||
<div class="p-2 bg-zinc-50 dark:bg-zinc-800 rounded group-hover:bg-accent-50 dark:group-hover:bg-accent-900/20 transition-colors">
|
||||
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $hub['locations_count'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('PLZs') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-zinc-50 dark:bg-zinc-800 rounded group-hover:bg-accent-50 dark:group-hover:bg-accent-900/20 transition-colors">
|
||||
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $hub['retailers_count'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Händler') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-zinc-50 dark:bg-zinc-800 rounded group-hover:bg-accent-50 dark:group-hover:bg-accent-900/20 transition-colors">
|
||||
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $hub['brokers_count'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Makler') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="col-span-3">
|
||||
<flux:card class="p-12">
|
||||
<div class="text-center">
|
||||
<flux:icon.map class="w-16 h-16 text-zinc-400 mx-auto mb-4" />
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Noch keine Hubs angelegt') }}</flux:heading>
|
||||
<flux:subheading class="mb-4">
|
||||
{{ __('Erstellen Sie Ihren ersten regionalen Marktplatz') }}
|
||||
</flux:subheading>
|
||||
<flux:button variant="primary" icon="plus" :href="route('admin.hubs.create')" wire:navigate>
|
||||
{{ __('Ersten Hub erstellen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- Statistik-Übersicht --}}
|
||||
@if($this->hubs->isNotEmpty())
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-8">
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||
<flux:icon.map class="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->count() }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Gesamt Hubs') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-green-100 dark:bg-green-900/20 rounded-lg">
|
||||
<flux:icon.check-circle class="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->where('is_active', true)->count() }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Aktive Hubs') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
|
||||
<flux:icon.map-pin class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->sum('locations_count') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('PLZ-Gebiete') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-orange-100 dark:bg-orange-900/20 rounded-lg">
|
||||
<flux:icon.building-storefront class="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->sum('retailers_count') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Partner gesamt') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
482
resources/views/livewire/admin/hubs/manage.blade.php
Normal file
482
resources/views/livewire/admin/hubs/manage.blade.php
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
<?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>
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ new class extends Component {
|
|||
public string $email = '';
|
||||
public bool $showSuccessMessage = false;
|
||||
public ?PartnerInvitation $lastInvitation = null;
|
||||
public int $expiryWeeks = 1;
|
||||
|
||||
public function getPartnerRoles()
|
||||
{
|
||||
|
|
@ -44,6 +45,7 @@ new class extends Component {
|
|||
'contactLastName' => 'nullable|string|max:255',
|
||||
'roleId' => 'required|exists:roles,id|in:' . implode(',', $availableRoleIds),
|
||||
'email' => 'required|email|max:255|unique:users,email',
|
||||
'expiryWeeks' => 'required|integer|in:1,2,3,4',
|
||||
], [
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'companyName.max' => __('Der Firmenname darf maximal 255 Zeichen lang sein.'),
|
||||
|
|
@ -55,6 +57,8 @@ new class extends Component {
|
|||
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
|
||||
'email.max' => __('Die E-Mail-Adresse darf maximal 255 Zeichen lang sein.'),
|
||||
'email.unique' => __('Diese E-Mail-Adresse ist bereits als Benutzer registriert.'),
|
||||
'expiryWeeks.required' => __('Bitte wählen Sie eine Gültigkeitsdauer aus.'),
|
||||
'expiryWeeks.in' => __('Die Gültigkeitsdauer muss zwischen 1 und 4 Wochen liegen.'),
|
||||
]);
|
||||
|
||||
// Prüfe ob bereits eine aktive Einladung existiert
|
||||
|
|
@ -76,7 +80,7 @@ new class extends Component {
|
|||
'email' => $this->email,
|
||||
'token' => PartnerInvitation::generateToken(),
|
||||
'status' => 'pending',
|
||||
'expires_at' => now()->addDays(7), // 7 Tage gültig
|
||||
'expires_at' => now()->addWeeks($this->expiryWeeks),
|
||||
'invited_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
|
|
@ -91,11 +95,12 @@ new class extends Component {
|
|||
$this->showSuccessMessage = true;
|
||||
|
||||
// Reset Form
|
||||
$this->reset(['companyName', 'contactFirstName', 'contactLastName', 'roleId', 'email']);
|
||||
$this->reset(['companyName', 'contactFirstName', 'contactLastName', 'roleId', 'email', 'expiryWeeks']);
|
||||
|
||||
// Setze default Rolle wieder
|
||||
$firstRole = $this->getPartnerRoles()->first();
|
||||
$this->roleId = $firstRole?->id;
|
||||
$this->expiryWeeks = 1;
|
||||
|
||||
session()->flash('message', __('Einladung erfolgreich versendet!'));
|
||||
} catch (\Exception $e) {
|
||||
|
|
@ -226,6 +231,19 @@ new class extends Component {
|
|||
@error('roleId') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Gültigkeit der Einladung') }}</flux:label>
|
||||
<flux:description>{{ __('Wählen Sie zwischen 1 und 4 Wochen') }}</flux:description>
|
||||
<flux:select wire:model="expiryWeeks">
|
||||
@for($i = 1; $i <= 4; $i++)
|
||||
<flux:select.option :value="$i">
|
||||
{{ $i }} {{ $i === 1 ? __('Woche') : __('Wochen') }}
|
||||
</flux:select.option>
|
||||
@endfor
|
||||
</flux:select>
|
||||
@error('expiryWeeks') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('E-Mail Adresse') }}</flux:label>
|
||||
<flux:description>{{ __('E-Mail des Ansprechpartners') }}</flux:description>
|
||||
|
|
|
|||
1254
resources/views/livewire/admin/partners/registration-codes.blade.php
Normal file
1254
resources/views/livewire/admin/partners/registration-codes.blade.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,412 @@
|
|||
<?php
|
||||
|
||||
use App\Models\RegistrationCode;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Livewire\Volt\Component;
|
||||
use function Livewire\Volt\{layout, title};
|
||||
|
||||
layout('components.layouts.app');
|
||||
title('Registrierung testen');
|
||||
|
||||
new class extends Component {
|
||||
public array $roleOptions = [];
|
||||
public string $selectedRole = 'broker';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadRoleOptions();
|
||||
}
|
||||
|
||||
private function loadRoleOptions(): void
|
||||
{
|
||||
$roles = Role::whereNotNull('reg_prefix')->where('can_be_invited', true)->orderBy('id', 'asc')->get();
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$key = strtolower(str_replace('-', '', $role->name));
|
||||
$slug = strtolower($role->reg_prefix);
|
||||
|
||||
$this->roleOptions[$key] = [
|
||||
'label' => $role->display_name ?? $role->name,
|
||||
'prefix' => $role->reg_prefix,
|
||||
'slug' => $slug,
|
||||
'color' => $role->color ?? 'zinc',
|
||||
'icon' => $role->icon ?? 'key',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($this->roleOptions)) {
|
||||
$this->selectedRole = array_key_first($this->roleOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
// Lade verfügbare Codes für ausgewählte Rolle
|
||||
$availableCodes = RegistrationCode::where('role', $this->selectedRole)
|
||||
->where('status', RegistrationCode::STATUS_AVAILABLE)
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
// Lade letzte verwendete Codes
|
||||
$recentUsedCodes = RegistrationCode::where('status', RegistrationCode::STATUS_USED)
|
||||
->with('usedBy')
|
||||
->orderBy('used_at', 'desc')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'roleOptions' => $this->roleOptions,
|
||||
'availableCodes' => $availableCodes,
|
||||
'recentUsedCodes' => $recentUsedCodes,
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6 p-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">{{ __('Registrierung testen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Teste den kompletten Registrierungsprozess mit echten Codes') }}</flux:subheading>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Info Box --}}
|
||||
<flux:card class="bg-accent-50 dark:bg-accent-900/20 border-accent-200 dark:border-accent-800">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="h-12 w-12 rounded-lg bg-accent-100 dark:bg-accent-900/40 flex items-center justify-center">
|
||||
@svg('heroicon-o-information-circle', 'h-6 w-6 text-accent-600 dark:text-accent-400')
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<flux:heading size="md" class="mb-2">{{ __('Wie funktioniert das Testing?') }}</flux:heading>
|
||||
<flux:subheading class="mb-3">
|
||||
{{ __('Wähle eine Rolle aus, kopiere einen verfügbaren Code und teste den gesamten Prozess:') }}
|
||||
</flux:subheading>
|
||||
<ol class="list-decimal list-inside space-y-1 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
<li>{{ __('Code-Eingabe auf der Landing-Page') }}</li>
|
||||
<li>{{ __('Account-Erstellung mit persönlichen Daten') }}</li>
|
||||
<li>{{ __('Setup-Wizard für Partner-Profil') }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Rollen-Auswahl --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Rolle wählen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Wähle die Rolle, für die du den Registrierungsprozess testen möchtest') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
@foreach($roleOptions as $key => $meta)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('selectedRole', '{{ $key }}')"
|
||||
class="p-4 rounded-lg border-2 transition-all {{ $selectedRole === $key ? 'border-accent-500 bg-accent-50 dark:bg-accent-900/20' : 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600' }}"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="h-12 w-12 rounded-lg flex items-center justify-center {{ $selectedRole === $key ? 'bg-accent-100 dark:bg-accent-900/40' : 'bg-zinc-100 dark:bg-zinc-800' }}">
|
||||
@svg('heroicon-o-'.$meta['icon'], 'h-6 w-6 ' . ($selectedRole === $key ? 'text-accent-600 dark:text-accent-400' : 'text-zinc-600 dark:text-zinc-400'))
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-semibold text-sm">{{ $meta['label'] }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ __('Prefix:') }} {{ $meta['prefix'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Landing Page Link --}}
|
||||
@if($selectedRole && isset($roleOptions[$selectedRole]))
|
||||
<flux:card class="shadow-elegant bg-gradient-to-r from-accent-50 to-blue-50 dark:from-accent-900/20 dark:to-blue-900/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-arrow-top-right-on-square', 'inline-block h-5 w-5')
|
||||
{{ __('Landing-Page aufrufen') }}
|
||||
</flux:heading>
|
||||
<flux:subheading class="mb-4">
|
||||
{{ __('Starte den Registrierungsprozess für :role', ['role' => $roleOptions[$selectedRole]['label']]) }}
|
||||
</flux:subheading>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="arrow-right"
|
||||
href="{{ config('domains.domain_b2in_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('B2In') }}
|
||||
</flux:button>
|
||||
@if($selectedRole === 'customer')
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="sparkles"
|
||||
href="{{ config('domains.domain_style2own_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Style2Own') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="home-modern"
|
||||
href="{{ config('domains.domain_stileigentum_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Stileigentum') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
@if($selectedRole === 'customer')
|
||||
<div class="mt-3 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
<flux:icon.information-circle class="inline-block h-4 w-4" />
|
||||
{{ __('Für Kunden stehen drei Landing-Page-Varianten zur Verfügung') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="h-20 w-20 rounded-2xl bg-white dark:bg-zinc-800 flex items-center justify-center shadow-lg">
|
||||
@svg('heroicon-o-'.$roleOptions[$selectedRole]['icon'], 'h-10 w-10 text-accent-600 dark:text-accent-400')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Verfügbare Codes --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-key', 'inline-block h-5 w-5')
|
||||
{{ __('Verfügbare Codes für :role', ['role' => $roleOptions[$selectedRole]['label'] ?? 'diese Rolle']) }}
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ __('Kopiere einen dieser Codes für deinen Test') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
@if($availableCodes->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<flux:icon.exclamation-triangle class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<flux:heading size="md" class="mt-4">{{ __('Keine verfügbaren Codes') }}</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
{{ __('Erstelle zuerst Codes in der Registrierungscode-Verwaltung') }}
|
||||
</flux:subheading>
|
||||
<div class="mt-4">
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="plus"
|
||||
href="{{ route('admin.partners.registration-codes') }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Zur Code-Verwaltung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($availableCodes as $code)
|
||||
<div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="font-mono text-xl font-bold text-zinc-900 dark:text-white">
|
||||
{{ $code->code }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick="navigator.clipboard.writeText('{{ $code->code }}');
|
||||
alert('Code kopiert: {{ $code->code }}');"
|
||||
class="p-2 rounded-lg bg-accent-100 dark:bg-accent-900/40 hover:bg-accent-200 dark:hover:bg-accent-900/60 transition-colors"
|
||||
title="{{ __('Code kopieren') }}"
|
||||
>
|
||||
@svg('heroicon-o-clipboard-document', 'h-5 w-5 text-accent-600 dark:text-accent-400')
|
||||
</button>
|
||||
</div>
|
||||
@if($code->name)
|
||||
<div class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
|
||||
{{ $code->name }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.calendar class="h-4 w-4" />
|
||||
{{ __('Erstellt:') }} {{ $code->created_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
@if($code->expires_at)
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.clock class="h-4 w-4" />
|
||||
{{ __('Gültig bis:') }} {{ $code->expires_at->format('d.m.Y') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Test-Prozess Steps --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-clipboard-document-check', 'inline-block h-5 w-5')
|
||||
{{ __('Test-Prozess') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ __('Folge diesen Schritten für einen vollständigen Test') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold">
|
||||
1
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Code kopieren') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Wähle einen verfügbaren Code aus der Liste oben und kopiere ihn') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-green-500 text-white flex items-center justify-center font-bold">
|
||||
2
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Landing-Page öffnen') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400 mb-2">
|
||||
{{ __('Öffne die Landing-Page in einem neuen Tab (Private/Inkognito-Modus empfohlen)') }}
|
||||
</div>
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
icon="arrow-top-right-on-square"
|
||||
href="{{ route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug'] ?? 'e']) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Landing-Page öffnen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-purple-500 text-white flex items-center justify-center font-bold">
|
||||
3
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Code eingeben') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Gib den kopierten Code auf der Landing-Page ein und klicke auf "Code prüfen"') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-orange-500 text-white flex items-center justify-center font-bold">
|
||||
4
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Account erstellen') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Fülle das Registrierungsformular mit Test-Daten aus (verwende eine Test-Email)') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-teal-500 text-white flex items-center justify-center font-bold">
|
||||
5
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Setup-Wizard durchlaufen') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Vervollständige das Partner-Profil im Setup-Wizard') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Letzte verwendete Codes --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-check-circle', 'inline-block h-5 w-5')
|
||||
{{ __('Kürzlich getestete Codes') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ __('Diese Codes wurden bereits verwendet') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
@if($recentUsedCodes->isEmpty())
|
||||
<div class="text-center py-8 text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Noch keine Codes verwendet') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($recentUsedCodes as $code)
|
||||
<div class="flex items-center justify-between p-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="font-mono font-semibold text-zinc-900 dark:text-white">
|
||||
{{ $code->code }}
|
||||
</div>
|
||||
<flux:badge size="sm" color="zinc">
|
||||
{{ $roleOptions[$code->role]['label'] ?? $code->role }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
@if($code->usedBy)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.user class="h-4 w-4" />
|
||||
{{ $code->usedBy->name }}
|
||||
@if($code->used_at)
|
||||
<span class="text-xs">• {{ $code->used_at->diffForHumans() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Quick Actions --}}
|
||||
<flux:card class="shadow-elegant bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="md" class="mb-2">{{ __('Weitere Aktionen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Nützliche Links für das Testing') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
icon="key"
|
||||
href="{{ route('admin.partners.registration-codes') }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Code-Verwaltung') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
icon="users"
|
||||
href="{{ route('admin.users') }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Benutzer-Verwaltung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
use function Livewire\Volt\{layout, title, state};
|
||||
|
|
@ -13,17 +14,32 @@ new class extends Component {
|
|||
|
||||
public string $search = '';
|
||||
public string $roleFilter = '';
|
||||
public string $sortField = 'name';
|
||||
public string $sortDirection = 'asc';
|
||||
public string $parentPartnerFilter = '';
|
||||
public string $brandFilter = '';
|
||||
public string $setupStatusFilter = '';
|
||||
public string $sortField = 'created_at';
|
||||
public string $sortDirection = 'desc';
|
||||
|
||||
// Modal state
|
||||
// Modal state for roles
|
||||
public bool $showRoleModal = false;
|
||||
public ?int $selectedUserId = null;
|
||||
public array $selectedRoles = [];
|
||||
|
||||
// Modal state for editing user
|
||||
public bool $showEditModal = false;
|
||||
public ?int $editUserId = null;
|
||||
public string $userName = '';
|
||||
public string $displayName = '';
|
||||
public string $userEmail = '';
|
||||
public bool $emailVerified = false;
|
||||
|
||||
// Modal state for viewing partner data
|
||||
public bool $showViewModal = false;
|
||||
public ?int $viewUserId = null;
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$query = User::with('roles')
|
||||
$query = User::with(['roles', 'registrationCode', 'partner.parentPartner'])
|
||||
->when($this->search, fn($q, $search) =>
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
|
|
@ -32,14 +48,62 @@ new class extends Component {
|
|||
$q->whereHas('roles', fn($roleQuery) =>
|
||||
$roleQuery->where('name', $role)
|
||||
)
|
||||
)
|
||||
->when($this->parentPartnerFilter, fn($q, $parentPartnerId) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->where('parent_partner_id', $parentPartnerId)
|
||||
)
|
||||
)
|
||||
->when($this->brandFilter, fn($q, $brand) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->where('brand', $brand)
|
||||
)
|
||||
)
|
||||
->when($this->setupStatusFilter === 'completed', fn($q) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->whereNotNull('setup_completed_at')
|
||||
)
|
||||
)
|
||||
->when($this->setupStatusFilter === 'pending', fn($q) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->whereNull('setup_completed_at')
|
||||
)
|
||||
)
|
||||
->when($this->setupStatusFilter === 'no_partner', fn($q) =>
|
||||
$q->whereNull('partner_id')
|
||||
);
|
||||
|
||||
// Finde alle Partner, die als parent_partner_id verwendet werden
|
||||
$parentPartnerIds = \App\Models\Partner::whereNotNull('parent_partner_id')
|
||||
->distinct()
|
||||
->pluck('parent_partner_id');
|
||||
|
||||
$availableParentPartners = \App\Models\Partner::whereIn('id', $parentPartnerIds)
|
||||
->orderBy('company_name')
|
||||
->get();
|
||||
|
||||
// Finde alle verfügbaren Brands
|
||||
$availableBrands = \App\Models\Partner::whereNotNull('brand')
|
||||
->distinct()
|
||||
->pluck('brand')
|
||||
->sort();
|
||||
|
||||
// Zähle User mit abgeschlossenem Setup
|
||||
$setupCompletedCount = User::whereHas('partner', function($q) {
|
||||
$q->whereNotNull('setup_completed_at');
|
||||
})->count();
|
||||
|
||||
return [
|
||||
'users' => $query->orderBy($this->sortField, $this->sortDirection)->paginate(15),
|
||||
'totalUsers' => User::count(),
|
||||
'verifiedUsers' => User::whereNotNull('email_verified_at')->count(),
|
||||
'setupCompletedUsers' => $setupCompletedCount,
|
||||
'availableRoles' => \Spatie\Permission\Models\Role::orderBy('name')->get(),
|
||||
'availableParentPartners' => $availableParentPartners,
|
||||
'availableBrands' => $availableBrands,
|
||||
'selectedUser' => $this->selectedUserId ? User::find($this->selectedUserId) : null,
|
||||
'editUser' => $this->editUserId ? User::find($this->editUserId) : null,
|
||||
'viewUser' => $this->viewUserId ? User::with(['partner.brand', 'roles'])->find($this->viewUserId) : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +127,21 @@ new class extends Component {
|
|||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingParentPartnerFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingBrandFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingSetupStatusFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function openRoleModal(int $userId): void
|
||||
{
|
||||
$user = User::with('roles')->findOrFail($userId);
|
||||
|
|
@ -94,22 +173,123 @@ new class extends Component {
|
|||
$this->selectedUserId = null;
|
||||
$this->selectedRoles = [];
|
||||
}
|
||||
|
||||
public function openEditModal(int $userId): void
|
||||
{
|
||||
$user = User::findOrFail($userId);
|
||||
$this->editUserId = $userId;
|
||||
$this->userName = $user->name;
|
||||
$this->displayName = $user->display_name ?? '';
|
||||
$this->userEmail = $user->email;
|
||||
$this->emailVerified = !is_null($user->email_verified_at);
|
||||
$this->showEditModal = true;
|
||||
}
|
||||
|
||||
public function saveUser(): void
|
||||
{
|
||||
if (!$this->editUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::with('roles')->findOrFail($this->editUserId);
|
||||
|
||||
// Prüfe ob User eine Rolle hat, die display_name erfordert
|
||||
$requiresDisplayName = $user->roles->whereIn('name', ['Broker', 'Retailer', 'Manufacturer'])->isNotEmpty();
|
||||
|
||||
$rules = [
|
||||
'userName' => 'required|string|max:255',
|
||||
'displayName' => $requiresDisplayName ? 'required|string|max:255' : 'nullable|string|max:255',
|
||||
'userEmail' => 'required|email|max:255|unique:users,email,' . $this->editUserId,
|
||||
];
|
||||
|
||||
$messages = [
|
||||
'displayName.required' => __('Name für die Zuordnung von Kunden ist für Makler, Händler und Hersteller erforderlich.'),
|
||||
];
|
||||
|
||||
$this->validate($rules, $messages);
|
||||
|
||||
$user->update([
|
||||
'name' => $this->userName,
|
||||
'display_name' => $this->displayName ?: null,
|
||||
'email' => $this->userEmail,
|
||||
'email_verified_at' => $this->emailVerified ? ($user->email_verified_at ?? now()) : null,
|
||||
]);
|
||||
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['editUserId', 'userName', 'displayName', 'userEmail', 'emailVerified']);
|
||||
|
||||
session()->flash('message', __('User updated successfully!'));
|
||||
}
|
||||
|
||||
public function closeEditModal(): void
|
||||
{
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['editUserId', 'userName', 'displayName', 'userEmail', 'emailVerified']);
|
||||
}
|
||||
|
||||
public function openViewModal(int $userId): void
|
||||
{
|
||||
$this->viewUserId = $userId;
|
||||
$this->showViewModal = true;
|
||||
}
|
||||
|
||||
public function closeViewModal(): void
|
||||
{
|
||||
$this->showViewModal = false;
|
||||
$this->viewUserId = null;
|
||||
}
|
||||
|
||||
public function deleteUser(int $userId): void
|
||||
{
|
||||
$user = User::findOrFail($userId);
|
||||
|
||||
// Prüfe auf Verknüpfungen
|
||||
if ($user->hasDependencies()) {
|
||||
// Anonymisiere statt zu löschen
|
||||
$user->anonymize();
|
||||
session()->flash('message', __('User wurde anonymisiert, da Verknüpfungen existieren.'));
|
||||
} else {
|
||||
// Soft Delete
|
||||
$user->delete();
|
||||
session()->flash('message', __('User deleted successfully!'));
|
||||
}
|
||||
|
||||
// Schließe ggf. offene Modals
|
||||
$this->showRoleModal = false;
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['selectedUserId', 'editUserId']);
|
||||
}
|
||||
|
||||
public function loginAsUser(int $userId): void
|
||||
{
|
||||
$currentUser = Auth::user();
|
||||
$targetUser = User::findOrFail($userId);
|
||||
|
||||
// Speichere den aktuellen Admin-User in der Session
|
||||
session(['impersonate_from' => $currentUser->id]);
|
||||
|
||||
// Logge als Ziel-User ein
|
||||
Auth::login($targetUser);
|
||||
|
||||
// Weiterleitung zum Dashboard
|
||||
$this->redirect(route('dashboard'), navigate: false);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6 p-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">{{ __('Users Management') }}</flux:heading>
|
||||
<flux:heading size="xl" class="mb-2">{{ __('Benutzerverwaltung') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Manage users and their roles in your application') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{{-- <div class="flex gap-2">
|
||||
<flux:button variant="primary" icon="plus">{{ __('Create User') }}</flux:button>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
|
||||
{{-- Statistics --}}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -128,8 +308,20 @@ new class extends Component {
|
|||
<flux:subheading>{{ __('Verified Users') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">{{ $verifiedUsers }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
||||
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/20">
|
||||
<flux:icon.shield-check class="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:subheading>{{ __('Setup abgeschlossen') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">{{ $setupCompletedUsers }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/20">
|
||||
<flux:icon.check-circle class="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -140,8 +332,8 @@ new class extends Component {
|
|||
<flux:subheading>{{ __('Active Roles') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">{{ $availableRoles->count() }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
||||
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/20">
|
||||
<flux:icon.user-group class="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -149,7 +341,7 @@ new class extends Component {
|
|||
|
||||
{{-- Filters --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
||||
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Search users...') }}" />
|
||||
|
||||
<flux:select wire:model.live="roleFilter" placeholder="{{ __('All Roles') }}">
|
||||
|
|
@ -159,8 +351,31 @@ new class extends Component {
|
|||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
@if($search || $roleFilter)
|
||||
<flux:button wire:click="$set('search', ''); $set('roleFilter', '')" variant="ghost" icon="x-mark">
|
||||
<flux:select wire:model.live="brandFilter" placeholder="{{ __('Alle Brands') }}">
|
||||
<flux:select.option value="">{{ __('Alle Brands') }}</flux:select.option>
|
||||
@foreach($availableBrands as $brand)
|
||||
<flux:select.option :value="$brand">{{ strtoupper($brand) }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select variant="listbox" searchable wire:model.live="parentPartnerFilter" placeholder="{{ __('Alle Partner-Zuordnungen') }}">
|
||||
<flux:select.option value="">{{ __('Alle Partner-Zuordnungen') }}</flux:select.option>
|
||||
@foreach($availableParentPartners as $partner)
|
||||
<flux:select.option :value="$partner->id">
|
||||
{{ $partner->company_name }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="setupStatusFilter" placeholder="{{ __('Setup Status') }}">
|
||||
<flux:select.option value="">{{ __('Alle Setup-Status') }}</flux:select.option>
|
||||
<flux:select.option value="completed">{{ __('Setup abgeschlossen') }}</flux:select.option>
|
||||
<flux:select.option value="pending">{{ __('Setup ausstehend') }}</flux:select.option>
|
||||
<flux:select.option value="no_partner">{{ __('Kein Partner') }}</flux:select.option>
|
||||
</flux:select>
|
||||
|
||||
@if($search || $roleFilter || $parentPartnerFilter || $brandFilter || $setupStatusFilter)
|
||||
<flux:button wire:click="$set('search', ''); $set('roleFilter', ''); $set('parentPartnerFilter', ''); $set('brandFilter', ''); $set('setupStatusFilter', '')" variant="ghost" icon="x-mark">
|
||||
{{ __('Clear Filters') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
|
|
@ -187,9 +402,17 @@ new class extends Component {
|
|||
@endif
|
||||
</div>
|
||||
</flux:table.column>
|
||||
<flux:table.column>{{ __('Roles') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('roles.role') }}</flux:table.column>
|
||||
<flux:table.column class="w-48" wire:click="sortBy('created_at')" class="cursor-pointer">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ __('Registrierung & Setup') }}
|
||||
@if($sortField === 'created_at')
|
||||
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.column>
|
||||
<flux:table.column class="w-32">{{ __('Status') }}</flux:table.column>
|
||||
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
|
||||
<flux:table.column class="w-40">{{ __('Actions') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
|
|
@ -202,8 +425,32 @@ new class extends Component {
|
|||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-zinc-900 dark:text-white">{{ $user->name }}</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('ID:') }} {{ $user->id }}
|
||||
@if($user->display_name)
|
||||
<div class="text-xs font-medium text-accent-600 dark:text-accent-400">
|
||||
{{ $user->display_name }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
@if($user->registrationCode)
|
||||
<span class="font-mono">{{ $user->registrationCode->code }}</span>
|
||||
@else
|
||||
<span class="text-zinc-400">{{ __('Kein Code') }}</span>
|
||||
@endif
|
||||
@if($user->partner && $user->partner->brand)
|
||||
<span class="text-zinc-300 dark:text-zinc-600">•</span>
|
||||
@php
|
||||
$brandColors = [
|
||||
'b2in' => 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
'style2own' => 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
||||
'stileigentum' => 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
'b2a' => 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400',
|
||||
];
|
||||
$brandColor = $brandColors[$user->partner->brand] ?? 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-400';
|
||||
@endphp
|
||||
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium {{ $brandColor }}">
|
||||
{{ strtoupper($user->partner->brand) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -234,6 +481,45 @@ new class extends Component {
|
|||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="flex flex-col space-y-1 text-sm">
|
||||
{{-- Registrierungsdatum --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.user-plus variant="micro" class="text-zinc-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-zinc-900 dark:text-white">
|
||||
{{ $user->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Registriert') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Setup-Abschluss --}}
|
||||
@if($user->partner && $user->partner->setup_completed_at)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.check-circle variant="micro" class="text-green-500" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">
|
||||
{{ $user->partner->setup_completed_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Setup abgeschlossen') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.clock variant="micro" class="text-orange-400" />
|
||||
<span class="text-xs text-orange-600 dark:text-orange-400">
|
||||
{{ __('Setup ausstehend') }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@if($user->email_verified_at)
|
||||
<flux:badge size="sm" color="green" icon="check-circle">
|
||||
|
|
@ -248,21 +534,37 @@ new class extends Component {
|
|||
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2">
|
||||
@if($user->partner_id)
|
||||
<flux:button size="sm" variant="ghost" icon="eye"
|
||||
wire:click="openViewModal({{ $user->id }})"
|
||||
tooltip="{{ __('Partner-Daten ansehen') }}"></flux:button>
|
||||
@endif
|
||||
@if($user->id !== Auth::id())
|
||||
<flux:button size="sm" variant="ghost" icon="arrow-right-start-on-rectangle"
|
||||
wire:click="loginAsUser({{ $user->id }})"
|
||||
wire:confirm="{{ __('Als :name einloggen? Sie können später zurück zum Admin wechseln.', ['name' => $user->name]) }}"
|
||||
tooltip="{{ __('Login als User') }}"></flux:button>
|
||||
@endif
|
||||
<flux:button size="sm" variant="ghost" icon="pencil"
|
||||
wire:click="openEditModal({{ $user->id }})"
|
||||
tooltip="{{ __('Edit User') }}"></flux:button>
|
||||
<flux:button size="sm" variant="ghost" icon="trash"
|
||||
tooltip="{{ __('Delete User') }}"></flux:button>
|
||||
@if($user->id !== Auth::id())
|
||||
<flux:button size="sm" variant="ghost" icon="trash"
|
||||
wire:click="deleteUser({{ $user->id }})"
|
||||
wire:confirm="{{ __('Möchten Sie diesen Benutzer wirklich löschen? Falls Verknüpfungen existieren, wird der Benutzer anonymisiert.') }}"
|
||||
tooltip="{{ __('Delete User') }}"></flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="5">
|
||||
<flux:table.cell colspan="6">
|
||||
<div class="py-12 text-center">
|
||||
<flux:icon.users variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<flux:heading size="lg" class="mt-4">{{ __('No users found') }}</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
@if($search || $roleFilter)
|
||||
@if($search || $roleFilter || $parentPartnerFilter || $brandFilter || $setupStatusFilter)
|
||||
{{ __('Try adjusting your filters.') }}
|
||||
@else
|
||||
{{ __('Get started by creating a new user.') }}
|
||||
|
|
@ -299,7 +601,7 @@ new class extends Component {
|
|||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Roles') }}</flux:label>
|
||||
<flux:label>{{ __('roles.role') }}</flux:label>
|
||||
<flux:description>{{ __('Select one or multiple roles for this user') }}</flux:description>
|
||||
|
||||
<div class="space-y-2 mt-3">
|
||||
|
|
@ -343,6 +645,363 @@ new class extends Component {
|
|||
</form>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Edit User Modal --}}
|
||||
<flux:modal name="edit-user-modal" :variant="'flyout'" wire:model="showEditModal">
|
||||
<form wire:submit="saveUser" class="space-y-6 max-w-2xl">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Edit User') }}</flux:heading>
|
||||
<flux:subheading>
|
||||
@if($editUser)
|
||||
{{ __('Editing user') }}: <strong>{{ $editUser->name }}</strong>
|
||||
@endif
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }}</flux:label>
|
||||
<flux:input wire:model="userName" placeholder="{{ __('Enter user name') }}" />
|
||||
@error('userName') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>
|
||||
{{ __('Name für die Zuordnung von Kunden') }}
|
||||
@if($editUser && $editUser->roles->whereIn('name', ['Broker', 'Retailer', 'Manufacturer'])->isNotEmpty())
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
</flux:label>
|
||||
<flux:description>
|
||||
{{ __('Pflichtfeld für Makler, Händler und Hersteller') }}
|
||||
</flux:description>
|
||||
<flux:input wire:model="displayName" placeholder="{{ __('z.B. Max Mustermann Immobilien') }}" />
|
||||
@error('displayName') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Email') }}</flux:label>
|
||||
<flux:input type="email" wire:model="userEmail" placeholder="{{ __('Enter email address') }}" />
|
||||
@error('userEmail') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Email Verification') }}</flux:label>
|
||||
<flux:description>{{ __('Set whether the email address is verified') }}</flux:description>
|
||||
<flux:checkbox wire:model="emailVerified" :label="__('Email verified')" />
|
||||
</flux:field>
|
||||
|
||||
@if($editUser && $editUser->registrationCode)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Registration Code') }}</flux:label>
|
||||
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
||||
<span class="font-mono font-semibold text-lg">{{ $editUser->registrationCode->code }}</span>
|
||||
<div class="text-sm text-zinc-500 mt-1">
|
||||
{{ __('Registered with this code') }}
|
||||
</div>
|
||||
</div>
|
||||
</flux:field>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="flex justify-between gap-2">
|
||||
<flux:button type="button" variant="ghost" wire:click="closeEditModal">
|
||||
{{ __('Cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ __('Save User') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
|
||||
{{-- View Partner Data Modal --}}
|
||||
<flux:modal name="view-partner-modal" :variant="'flyout'" wire:model="showViewModal" class="md:w-2xl">
|
||||
@if($viewUser && $viewUser->partner)
|
||||
@php
|
||||
$partner = $viewUser->partner;
|
||||
$normalizedType = strtolower(str_replace('-', '', $partner->type));
|
||||
$isCustomer = $normalizedType === 'customer';
|
||||
$isBroker = $normalizedType === 'broker' || $normalizedType === 'estateagent';
|
||||
$isRetailer = $normalizedType === 'retailer';
|
||||
$isManufacturer = $normalizedType === 'manufacturer';
|
||||
|
||||
$role = $viewUser->roles->first();
|
||||
$roleIcon = $role?->icon ?? 'shield-check';
|
||||
$roleName = $role?->display_name ?? $role?->name ?? '-';
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">{{ __('Partner-Daten') }}</flux:heading>
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5 text-accent-600 dark:text-accent-400')
|
||||
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $roleName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<flux:subheading class="mt-2">
|
||||
{{ __('Benutzer') }}: <strong>{{ $viewUser->name }}</strong>
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Firmeninformationen --}}
|
||||
@if (!$isCustomer)
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Firmeninformationen') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Firmenname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->company_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($isBroker)
|
||||
<div>
|
||||
<flux:label>{{ __('Anzeigename') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->display_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($partner->description)
|
||||
<div>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
{{ $partner->description }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
@endif
|
||||
|
||||
{{-- Persönliche Daten --}}
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Persönliche Daten') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Anrede') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->salutation ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Vorname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->first_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Nachname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->last_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Adresse --}}
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Adresse') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="col-span-3">
|
||||
<flux:label>{{ __('Straße') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->street ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Hausnummer') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->house_number ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Postleitzahl') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->zip ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<flux:label>{{ __('Ort') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->city ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Land') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->country ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Kontaktdaten --}}
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Kontaktdaten') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Telefon') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->phone ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!$isCustomer)
|
||||
<div>
|
||||
<flux:label>{{ __('Website') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
@if($partner->website)
|
||||
<a href="{{ $partner->website }}" target="_blank" class="text-accent-600 hover:text-accent-700 dark:text-accent-400 dark:hover:text-accent-300">
|
||||
{{ $partner->website }}
|
||||
</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Liefergebiete für Händler --}}
|
||||
@if ($isRetailer)
|
||||
<flux:separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Liefergebiete') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Lieferradius') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->delivery_radius_km ? $partner->delivery_radius_km . ' km' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Montageradius') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->assembly_radius_km ? $partner->assembly_radius_km . ' km' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Marke für Hersteller --}}
|
||||
@if ($isManufacturer && $partner->brand)
|
||||
<flux:separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Markeninformationen') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Markenname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
@if(isset($partner->brand->name))
|
||||
{{ $partner->brand->name }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(isset($partner->brand->description))
|
||||
<div>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
{{ $partner->brand->description }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Status') }}</flux:label>
|
||||
<div class="mt-1">
|
||||
@if(isset($partner->brand->is_active) && $partner->brand->is_active === true)
|
||||
<flux:badge color="green" size="sm" icon="check-circle">{{ __('Aktiv') }}</flux:badge>
|
||||
@else
|
||||
<flux:badge color="zinc" size="sm" icon="x-circle">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Close Button --}}
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="button" variant="primary" wire:click="closeViewModal">
|
||||
{{ __('Schließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Partner-Daten') }}</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
{{ __('Dieser Benutzer hat keine Partner-Daten') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="button" variant="primary" wire:click="closeViewModal">
|
||||
{{ __('Schließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</flux:modal>
|
||||
|
||||
{{-- Success Message --}}
|
||||
@if (session()->has('message'))
|
||||
<flux:toast :variant="'success'">
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ new class extends Component {
|
|||
public string $roleIcon = 'shield-check';
|
||||
public string $roleColor = 'zinc';
|
||||
public array $rolePermissions = [];
|
||||
public ?string $regPrefix = null;
|
||||
public ?string $regDescription = null;
|
||||
public ?int $regStartNumber = null;
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'roles' => Role::with('permissions')->orderBy('name')->get(),
|
||||
'roles' => Role::with('permissions')->orderBy('id', 'asc')->get(),
|
||||
'permissions' => Permission::with('roles')->orderBy('name')->get(),
|
||||
'allPermissions' => Permission::orderBy('name')->get(),
|
||||
'selectedRole' => $this->selectedRoleId ? Role::find($this->selectedRoleId) : null,
|
||||
|
|
@ -46,6 +49,9 @@ new class extends Component {
|
|||
$this->roleColor = $role->color ?? 'zinc';
|
||||
$this->rolePermissions = $role->permissions->pluck('name')->toArray();
|
||||
$this->roleIcon = $role->icon ?? 'shield-check';
|
||||
$this->regPrefix = $role->reg_prefix;
|
||||
$this->regDescription = $role->reg_description;
|
||||
$this->regStartNumber = $role->reg_start_number;
|
||||
$this->showEditModal = true;
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +64,8 @@ new class extends Component {
|
|||
$this->validate([
|
||||
'roleName' => 'required|string|max:255',
|
||||
'roleColor' => 'required|string|max:50',
|
||||
'regPrefix' => 'nullable|string|size:1',
|
||||
'regStartNumber' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
$role = Role::findOrFail($this->selectedRoleId);
|
||||
|
|
@ -66,12 +74,15 @@ new class extends Component {
|
|||
'display_name' => $this->roleDisplayName,
|
||||
'icon' => $this->roleIcon,
|
||||
'color' => $this->roleColor,
|
||||
'reg_prefix' => $this->regPrefix,
|
||||
'reg_description' => $this->regDescription,
|
||||
'reg_start_number' => $this->regStartNumber,
|
||||
]);
|
||||
|
||||
$role->syncPermissions($this->rolePermissions);
|
||||
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions', 'regPrefix', 'regDescription', 'regStartNumber']);
|
||||
|
||||
session()->flash('message', __('Role updated successfully!'));
|
||||
}
|
||||
|
|
@ -79,7 +90,7 @@ new class extends Component {
|
|||
public function closeEditModal(): void
|
||||
{
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions', 'regPrefix', 'regDescription', 'regStartNumber']);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
|
|
@ -90,10 +101,10 @@ new class extends Component {
|
|||
<flux:heading size="xl" class="mb-2">{{ __('Permissions & Roles Management') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Manage roles and permissions for your application') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{{-- <div class="flex gap-2">
|
||||
<flux:button variant="primary" icon="plus">{{ __('Create Role') }}</flux:button>
|
||||
<flux:button variant="ghost" icon="shield-check">{{ __('Create Permission') }}</flux:button>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
|
||||
{{-- Tabs --}}
|
||||
|
|
@ -185,8 +196,7 @@ new class extends Component {
|
|||
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2">
|
||||
<flux:button size="sm" variant="ghost" icon="eye"
|
||||
tooltip="{{ __('View Details') }}"></flux:button>
|
||||
|
||||
<flux:button size="sm" variant="ghost" icon="pencil"
|
||||
wire:click="openEditModal({{ $role->id }})"
|
||||
tooltip="{{ __('Edit Role') }}"></flux:button>
|
||||
|
|
@ -211,7 +221,7 @@ new class extends Component {
|
|||
</flux:card>
|
||||
|
||||
{{-- Role Statistics --}}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -236,19 +246,7 @@ new class extends Component {
|
|||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:subheading>{{ __('Avg. Permissions/Role') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">
|
||||
{{ $roles->count() > 0 ? number_format($roles->sum(fn($r) => $r->permissions->count()) / $roles->count(), 1) : 0 }}
|
||||
</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
||||
<flux:icon.chart-bar class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -414,6 +412,34 @@ new class extends Component {
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:subheading>{{ __('Registrierungscode-Einstellungen') }}</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Code-Prefix') }}</flux:label>
|
||||
<flux:input wire:model="regPrefix" maxlength="1" placeholder="z.B. K, M, H" />
|
||||
<flux:description>{{ __('Einzelner Buchstabe für Registrierungscodes') }}</flux:description>
|
||||
@error('regPrefix') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Startnummer') }}</flux:label>
|
||||
<flux:input wire:model="regStartNumber" type="number" placeholder="10000000" />
|
||||
<flux:description>{{ __('Startnummer für Code-Generierung') }}</flux:description>
|
||||
@error('regStartNumber') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||
<flux:input wire:model="regDescription" placeholder="Code-Beschreibung" />
|
||||
<flux:description>{{ __('Optionale Beschreibung') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Permissions') }}</flux:label>
|
||||
<flux:description>{{ __('Select permissions for this role') }}</flux:description>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue