20-02-2026

This commit is contained in:
Kevin Adametz 2026-02-20 17:57:50 +01:00
parent 854ce02bf6
commit 4d6b4930b2
128 changed files with 18247 additions and 2093 deletions

View file

@ -1,6 +1,8 @@
<?php
use App\Enums\ProductStatus;
use App\Models\Partner;
use App\Models\Product;
use App\Models\User;
use App\Models\RegistrationCode;
use App\Helpers\ThemeHelper;
@ -189,8 +191,32 @@ new class extends Component {
}
}
// Top-Angebote laden (Platzhalter - später aus Product-Tabelle)
$this->topOffers = [
// Top-Angebote aus dem Hub des Kunden laden
$hubId = $partner->hub_id ?? $user->hub_id;
$this->topOffers = Product::query()
->where('status', ProductStatus::Active)
->where('is_curated', true)
->where('is_available', true)
->when($hubId, fn ($q) => $q->where('hub_id', $hubId))
->with(['categories', 'media'])
->latest()
->take(3)
->get()
->map(fn (Product $p) => [
'id' => $p->id,
'name' => $p->name,
'description' => $p->description_short ?? '',
'price' => $p->price ?? 0,
'original_price' => $p->price ?? 0,
'discount' => 0,
'image' => $p->media->first()?->url ?? null,
'category' => $p->categories->first()?->name ?? '',
])
->toArray();
// Fallback: Dummy-Daten wenn noch keine Produkte vorhanden
if (empty($this->topOffers)) {
$this->topOffers = [
[
'id' => 1,
'name' => 'Designer Sofa "Luna"',
@ -198,7 +224,7 @@ new class extends Component {
'price' => 1899.00,
'original_price' => 2499.00,
'discount' => 24,
'image' => 'https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=400&h=300&fit=crop',
'image' => null,
'category' => 'Wohnzimmer',
],
[
@ -208,7 +234,7 @@ new class extends Component {
'price' => 899.00,
'original_price' => 1299.00,
'discount' => 31,
'image' => 'https://images.unsplash.com/photo-1617806118233-18e1de247200?w=400&h=300&fit=crop',
'image' => null,
'category' => 'Esszimmer',
],
[
@ -218,10 +244,11 @@ new class extends Component {
'price' => 1599.00,
'original_price' => 2199.00,
'discount' => 27,
'image' => 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=400&h=300&fit=crop',
'image' => null,
'category' => 'Schlafzimmer',
],
];
}
}
}
}

View file

@ -1,136 +1,204 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-zinc-50 dark:bg-zinc-800 antialiased">
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ config('domains.domain_main_url') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse">
<x-app-logo />
</a>
@if(session('impersonate_from'))
<div class="mb-4 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 p-3">
<div class="flex items-start gap-3 mb-2">
<flux:icon.exclamation-triangle class="h-5 w-5 text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" />
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-orange-900 dark:text-orange-100">
{{ __('Als :name eingeloggt', ['name' => Auth::user()->name]) }}
</div>
<div class="text-xs text-orange-700 dark:text-orange-300 mt-0.5">
{{ __('Sie sind temporär als dieser User angemeldet') }}
</div>
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-zinc-50 dark:bg-zinc-800 antialiased">
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ config('domains.domain_main_url') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse">
<x-app-logo />
</a>
@if (session('impersonate_from'))
<div
class="mb-4 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 p-3">
<div class="flex items-start gap-3 mb-2">
<flux:icon.exclamation-triangle
class="h-5 w-5 text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" />
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-orange-900 dark:text-orange-100">
{{ __('Als :name eingeloggt', ['name' => Auth::user()->name]) }}
</div>
<div class="text-xs text-orange-700 dark:text-orange-300 mt-0.5">
{{ __('Sie sind temporär als dieser User angemeldet') }}
</div>
</div>
<form method="POST" action="{{ route('admin.impersonate.leave') }}" class="w-full">
@csrf
<flux:button type="submit" variant="primary" size="sm" icon="arrow-left-start-on-rectangle" class="w-full">
{{ __('Zurück zum Admin') }}
</flux:button>
</form>
</div>
@endif
<form method="POST" action="{{ route('admin.impersonate.leave') }}" class="w-full">
@csrf
<flux:button type="submit" variant="primary" size="sm" icon="arrow-left-start-on-rectangle"
class="w-full">
{{ __('Zurück zum Admin') }}
</flux:button>
</form>
</div>
@endif
<flux:navlist variant="outline">
@hasrole('Customer')
<flux:navlist variant="outline">
@hasrole('Customer')
<flux:navlist.group :heading="__('Customer')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')"
wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
<flux:navlist.item icon="building-office" :href="route('partner.my-data')"
:current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Retailer')
@endhasrole
@hasrole('Retailer')
<flux:navlist.group :heading="__('Retailer')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')"
wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
<flux:navlist.item icon="cube" :href="route('products.index')" :current="request()->routeIs('products.index')" wire:navigate>{{ __('Produktliste') }}</flux:navlist.item>
<flux:navlist.item icon="plus-circle" :href="route('products.create')" :current="request()->routeIs('products.create')" wire:navigate>{{ __('Neues Produkt') }}</flux:navlist.item>
<flux:navlist.item icon="cube" :href="route('products.index')"
:current="request()->routeIs('products.index')" wire:navigate>{{ __('Produktliste') }}
</flux:navlist.item>
<flux:navlist.item icon="plus-circle" :href="route('products.create.teaser')"
:current="request()->routeIs('products.create.teaser')" wire:navigate>
{{ __('Neues Teaser-Produkt') }}
</flux:navlist.item>
<flux:navlist.item icon="plus-circle" :href="route('products.create.standard')"
:current="request()->routeIs('products.create.standard')" wire:navigate>
{{ __('Neues Standard-Produkt') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
<flux:navlist.item icon="building-office" :href="route('partner.my-data')"
:current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Manufacturer')
@endhasrole
@hasrole('Manufacturer')
<flux:navlist.group :heading="__('Manufacturer')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
<flux:navlist.item icon="home" :href="route('dashboard')"
:current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
<flux:navlist.item icon="cube" :href="route('products.index')" :current="request()->routeIs('products.index')" wire:navigate>{{ __('Produktliste') }}</flux:navlist.item>
<flux:navlist.item icon="plus-circle" :href="route('products.create')" :current="request()->routeIs('products.create')" wire:navigate>{{ __('Neues Produkt') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Broker')
<flux:navlist.group :heading="__('Broker')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Admin|Super-Admin')
<flux:navlist.group :heading="__('Info')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
<flux:navlist.item icon="cube" :href="route('products.index')"
:current="request()->routeIs('products.index')" wire:navigate>{{ __('Produktliste') }}
</flux:navlist.item>
@hasrole('Super-Admin|Admin')
<flux:navlist.item icon="plus-circle" :href="route('products.create.teaser')"
:current="request()->routeIs('products.create.teaser')" wire:navigate>
{{ __('Neues Teaser-Produkt') }}
</flux:navlist.item>
<flux:navlist.item icon="plus-circle" :href="route('products.create.standard')"
:current="request()->routeIs('products.create.standard')" wire:navigate>
{{ __('Neues Standard-Produkt') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
<flux:navlist.item icon="building-office" :href="route('partner.my-data')"
:current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Broker')
<flux:navlist.group :heading="__('Broker')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')"
:current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
<flux:navlist.item icon="building-office" :href="route('partner.my-data')"
:current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Admin|Super-Admin')
<flux:navlist.group :heading="__('Info')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')"
:current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Super-Admin|Admin')
<flux:navlist.group :heading="__('Admin')" class="grid mb-4">
<flux:navlist.group expandable :expanded="request()->routeIs(['admin.users', 'admin.users.permissions', 'admin.partners.invite', 'admin.partners.registration-codes'])" heading="Partner" class="grid">
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Benutzer') }}</flux:navlist.item>
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')" :current="request()->routeIs('admin.partners.invite')" wire:navigate>{{ __('Partner einladen') }}</flux:navlist.item>
<flux:navlist.item icon="key" :href="route('admin.partners.registration-codes')" :current="request()->routeIs('admin.partners.registration-codes')" wire:navigate>{{ __('Registrierungscodes') }}</flux:navlist.item>
<flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')" :current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}</flux:navlist.item>
<flux:navlist.group expandable
:expanded="request()->routeIs(['admin.users', 'admin.users.permissions', 'admin.partners.invite', 'admin.partners.registration-codes'])"
heading="Partner" class="grid">
<flux:navlist.item icon="user-group" :href="route('admin.users')"
:current="request()->routeIs('admin.users')" wire:navigate>{{ __('Benutzer') }}
</flux:navlist.item>
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')"
:current="request()->routeIs('admin.partners.invite')" wire:navigate>
{{ __('Partner einladen') }}</flux:navlist.item>
<flux:navlist.item icon="key" :href="route('admin.partners.registration-codes')"
:current="request()->routeIs('admin.partners.registration-codes')" wire:navigate>
{{ __('Registrierungscodes') }}</flux:navlist.item>
<flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')"
:current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group expandable :expanded="request()->routeIs('testing')" heading="Testing" class="hidden lg:grid mt-2">
<flux:navlist.item icon="user-group" :href="route('testing.landing')" :current="request()->routeIs('testing.landing')" wire:navigate>{{ __('Register') }}</flux:navlist.item>
<flux:navlist.group expandable :expanded="request()->routeIs('testing')" heading="Testing"
class="hidden lg:grid mt-2">
<flux:navlist.item icon="user-group" :href="route('testing.landing')"
:current="request()->routeIs('testing.landing')" wire:navigate>{{ __('Register') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
<flux:navlist.item icon="clipboard-document-check" :href="route('admin.products.index')"
:current="request()->routeIs('admin.products.*')" wire:navigate>
{{ __('Produkt-Verwaltung') }}
</flux:navlist.item>
<flux:navlist.item icon="cube" :href="route('products.index')"
:current="request()->routeIs('products.index')" wire:navigate>
{{ __('Alle Produkte') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Regionen')" class="grid mb-4">
<flux:navlist.item icon="map" :href="route('admin.hubs.index')" :current="request()->routeIs('admin.hubs.*')" wire:navigate>{{ __('Hub-Verwaltung') }}</flux:navlist.item>
<flux:navlist.item icon="map" :href="route('admin.hubs.index')"
:current="request()->routeIs('admin.hubs.*')" wire:navigate>{{ __('Hub-Verwaltung') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('CMS')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('admin.cms.cabinet')" :current="request()->routeIs('admin.cms.cabinet')" wire:navigate>{{ __('Cabinet') }}</flux:navlist.item>
<flux:navlist.item icon="home" :href="route('admin.cms.cabinet')"
:current="request()->routeIs('admin.cms.cabinet')" wire:navigate>{{ __('Cabinet') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Super-Admin')
<flux:navlist.group :heading="__('Superadmin')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
</flux:navlist>
<flux:spacer />
@hasrole('Super-Admin|Admin')
<flux:navlist.group :heading="__('Dokumentation')" class="grid mb-4">
<flux:navlist.item icon="document-text" :href="route('admin.documentation')" :current="request()->routeIs('admin.documentation')" wire:navigate>{{ __('Projekt-Dokumentation') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Super-Admin')
<flux:navlist.group :heading="__('Superadmin')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')"
:current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
</flux:navlist>
<flux:spacer />
@hasrole('Super-Admin|Admin')
<flux:navlist.group :heading="__('Dokumentation')" class="grid mb-4">
<flux:navlist.item icon="document-text" :href="route('admin.documentation')"
:current="request()->routeIs('admin.documentation')" wire:navigate>{{ __('Projekt-Dokumentation') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
@hasrole('Super-Admin')
<flux:navlist variant="outline">
<flux:navlist.group :heading="__('Resources')">
<flux:navlist.item icon="pencil" href="https://tailwindcss.com/docs/installation/using-vite" target="_blank">
{{ __('Tailwind CSS') }}
<flux:navlist.item icon="pencil" href="https://tailwindcss.com/docs/installation/using-vite"
target="_blank">
{{ __('Tailwind CSS') }}
</flux:navlist.item>
<flux:navlist.item icon="shield-check" href="https://heroicons.com" target="_blank">
{{ __('Hero Icons') }}
{{ __('Hero Icons') }}
</flux:navlist.item>
<flux:navlist.item icon="bolt" href="https://fluxui.dev/docs/installation" target="_blank">
{{ __('Flux UI') }}
{{ __('Flux UI') }}
</flux:navlist.item>
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit"
target="_blank">
{{ __('Repository') }}
</flux:navlist.item>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits" target="_blank">
@ -138,159 +206,143 @@
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
@endhasrole
@endhasrole
<!-- Desktop User Menu -->
<flux:dropdown position="bottom" align="start">
<flux:profile
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
icon-trailing="chevrons-up-down"
/>
<!-- Desktop User Menu -->
<flux:dropdown position="bottom" align="start">
<flux:profile :name="auth()->user()->name" :initials="auth()->user()->initials()"
icon-trailing="chevrons-up-down" />
<flux:menu class="w-[220px]">
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
</span>
<flux:menu class="w-[220px]">
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</flux:menu.radio.group>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>
{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<div x-data="{
toggle() {
$flux.appearance = $flux.appearance === 'dark' ? 'light' : 'dark';
}
}">
<flux:menu.item
x-show="$flux.appearance !== 'dark'"
@click="toggle()"
icon="moon"
>
{{ __('Dunkel') }}
</flux:menu.item>
<flux:menu.item
x-show="$flux.appearance === 'dark'"
x-cloak
@click="toggle()"
icon="sun"
>
{{ __('Hell') }}
</flux:menu.item>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }}
<flux:menu.radio.group>
<div x-data="{
toggle() {
$flux.appearance = $flux.appearance === 'dark' ? 'light' : 'dark';
}
}">
<flux:menu.item x-show="$flux.appearance !== 'dark'" @click="toggle()" icon="moon">
{{ __('Dunkel') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:sidebar>
<flux:menu.item x-show="$flux.appearance === 'dark'" x-cloak @click="toggle()"
icon="sun">
{{ __('Hell') }}
</flux:menu.item>
</div>
</flux:menu.radio.group>
<!-- Mobile User Menu -->
<flux:header class="lg:hidden">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<flux:menu.separator />
<a href="{{ config('domains.domain_main_url') }}" class="me-5 ml-2 flex items-center space-x-2 rtl:space-x-reverse">
<x-app-logo />
</a>
<flux:spacer />
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle"
class="w-full">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:sidebar>
<flux:dropdown position="top" align="end">
<flux:profile
:initials="auth()->user()->initials()"
icon-trailing="chevron-down"
/>
<!-- Mobile User Menu -->
<flux:header class="lg:hidden">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
</span>
<a href="{{ config('domains.domain_main_url') }}"
class="me-5 ml-2 flex items-center space-x-2 rtl:space-x-reverse">
<x-app-logo />
</a>
<flux:spacer />
<flux:dropdown position="top" align="end">
<flux:profile :initials="auth()->user()->initials()" icon-trailing="chevron-down" />
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</flux:menu.radio.group>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>
{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<div x-data="{
toggle() {
$flux.appearance = $flux.appearance === 'dark' ? 'light' : 'dark';
}
}">
<flux:menu.item
x-show="$flux.appearance !== 'dark'"
@click="toggle()"
icon="moon"
>
{{ __('Dunkel') }}
</flux:menu.item>
<flux:menu.item
x-show="$flux.appearance === 'dark'"
x-cloak
@click="toggle()"
icon="sun"
>
{{ __('Hell') }}
</flux:menu.item>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }}
<flux:menu.radio.group>
<div x-data="{
toggle() {
$flux.appearance = $flux.appearance === 'dark' ? 'light' : 'dark';
}
}">
<flux:menu.item x-show="$flux.appearance !== 'dark'" @click="toggle()" icon="moon">
{{ __('Dunkel') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
<flux:menu.item x-show="$flux.appearance === 'dark'" x-cloak @click="toggle()"
icon="sun">
{{ __('Hell') }}
</flux:menu.item>
</div>
</flux:menu.radio.group>
{{ $slot }}
<flux:menu.separator />
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle"
class="w-full">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
{{ $slot }}
<flux:toast />
@fluxScripts
</body>
@fluxScripts
</body>
</html>

View file

@ -0,0 +1,355 @@
<?php
use App\Models\Partner;
use App\Models\Hub;
use Livewire\Volt\Component;
use function Livewire\Volt\{layout, title};
layout('components.layouts.app');
title('Partner bearbeiten');
new class extends Component {
public Partner $partner;
// Basis-Felder
public string $companyName = '';
public string $displayName = '';
public string $street = '';
public string $houseNumber = '';
public string $zip = '';
public string $city = '';
public string $phone = '';
public string $website = '';
public ?int $hubId = null;
public bool $isActive = true;
// Profil-Felder (Phase 1 Migrationen)
public string $storyText = '';
public int|string $foundedYear = '';
public string $specialtiesInput = '';
/**
* Öffnungszeiten als strukturiertes Array.
*
* @var array<string, array{open: string, close: string, closed: bool}>
*/
public array $openingHours = [
'monday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'tuesday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'wednesday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'thursday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'friday' => ['open' => '09:00', 'close' => '18:00', 'closed' => false],
'saturday' => ['open' => '10:00', 'close' => '16:00', 'closed' => false],
'sunday' => ['open' => '', 'close' => '', 'closed' => true],
];
public function mount(int $partnerId): void
{
$this->partner = Partner::findOrFail($partnerId);
$this->authorize('update', $this->partner);
$this->companyName = $this->partner->company_name ?? '';
$this->displayName = $this->partner->display_name ?? '';
$this->street = $this->partner->street ?? '';
$this->houseNumber = $this->partner->house_number ?? '';
$this->zip = $this->partner->zip ?? '';
$this->city = $this->partner->city ?? '';
$this->phone = $this->partner->phone ?? '';
$this->website = $this->partner->website ?? '';
$this->hubId = $this->partner->hub_id;
$this->isActive = $this->partner->is_active;
$this->storyText = $this->partner->story_text ?? '';
$this->foundedYear = $this->partner->founded_year ?? '';
$this->specialtiesInput = $this->partner->specialties
? implode(', ', $this->partner->specialties)
: '';
if ($this->partner->opening_hours) {
$this->openingHours = array_merge($this->openingHours, $this->partner->opening_hours);
}
}
public function save(): void
{
$this->authorize('update', $this->partner);
$this->validate([
'companyName' => 'required|string|max:255',
'displayName' => 'nullable|string|max:255',
'street' => 'nullable|string|max:255',
'houseNumber' => 'nullable|string|max:20',
'zip' => 'nullable|string|max:10',
'city' => 'nullable|string|max:100',
'phone' => 'nullable|string|max:50',
'website' => 'nullable|url|max:255',
'hubId' => 'nullable|exists:hubs,id',
'storyText' => 'nullable|string|max:2000',
'foundedYear' => 'nullable|integer|min:1800|max:' . now()->year,
'specialtiesInput' => 'nullable|string|max:500',
], [
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
'website.url' => __('Bitte geben Sie eine gültige URL ein (z.B. https://example.de).'),
'foundedYear.integer' => __('Bitte geben Sie eine gültige Jahreszahl ein.'),
'foundedYear.min' => __('Das Gründungsjahr muss nach 1800 liegen.'),
'foundedYear.max' => __('Das Gründungsjahr darf nicht in der Zukunft liegen.'),
]);
$specialties = array_filter(
array_map('trim', explode(',', $this->specialtiesInput))
);
$this->partner->update([
'company_name' => $this->companyName,
'display_name' => $this->displayName ?: null,
'street' => $this->street ?: null,
'house_number' => $this->houseNumber ?: null,
'zip' => $this->zip ?: null,
'city' => $this->city ?: null,
'phone' => $this->phone ?: null,
'website' => $this->website ?: null,
'hub_id' => $this->hubId,
'is_active' => $this->isActive,
'story_text' => $this->storyText ?: null,
'founded_year' => $this->foundedYear ?: null,
'specialties' => array_values($specialties),
'opening_hours' => $this->openingHours,
]);
session()->flash('message', __('Partner-Profil erfolgreich gespeichert.'));
}
/** @return array<string, string> */
protected function dayLabels(): array
{
return [
'monday' => __('Montag'),
'tuesday' => __('Dienstag'),
'wednesday' => __('Mittwoch'),
'thursday' => __('Donnerstag'),
'friday' => __('Freitag'),
'saturday' => __('Samstag'),
'sunday' => __('Sonntag'),
];
}
public function with(): array
{
return [
'hubs' => Hub::orderBy('name')->get(['id', 'name']),
'dayLabels' => $this->dayLabels(),
];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center gap-4">
<flux:button href="{{ route('admin.partners.index') }}" variant="ghost" icon="arrow-left" />
<div>
<flux:heading size="xl" class="mb-1">{{ $partner->company_name }}</flux:heading>
<flux:subheading>{{ __('Partner-Profil bearbeiten') }}</flux:subheading>
</div>
</div>
@if (session()->has('message'))
<flux:callout variant="success" icon="check-circle">
{{ session('message') }}
</flux:callout>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Basisdaten --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Basisdaten') }}</flux:heading>
</div>
<flux:separator class="mb-6" />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Firmenname') }}</flux:label>
<flux:input wire:model="companyName" icon="building-office" />
@error('companyName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Anzeigename (optional)') }}</flux:label>
<flux:description>{{ __('Öffentlich sichtbarer Name, falls abweichend') }}</flux:description>
<flux:input wire:model="displayName" />
@error('displayName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:field class="md:col-span-2">
<flux:label>{{ __('Straße') }}</flux:label>
<flux:input wire:model="street" />
@error('street') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Hausnummer') }}</flux:label>
<flux:input wire:model="houseNumber" />
@error('houseNumber') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:field>
<flux:label>{{ __('PLZ') }}</flux:label>
<flux:input wire:model="zip" />
@error('zip') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field class="md:col-span-2">
<flux:label>{{ __('Stadt') }}</flux:label>
<flux:input wire:model="city" />
@error('city') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="phone" type="tel" icon="phone" />
@error('phone') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Website') }}</flux:label>
<flux:input wire:model="website" type="url" placeholder="https://example.de" icon="globe-alt" />
@error('website') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Hub / Region') }}</flux:label>
<flux:select wire:model="hubId">
<flux:select.option :value="null">{{ __(' Kein Hub ') }}</flux:select.option>
@foreach ($hubs as $hub)
<flux:select.option :value="$hub->id">{{ $hub->name }}</flux:select.option>
@endforeach
</flux:select>
@error('hubId') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:switch wire:model="isActive" label="{{ __('Partner ist aktiv') }}" />
</flux:field>
</div>
</flux:card>
{{-- Story & Profil --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Story & Profil') }}</flux:heading>
<flux:subheading>{{ __('Erzählen Sie die Geschichte des Partners') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Story-Text') }}</flux:label>
<flux:description>{{ __('Kurze Geschichte des Unternehmens max. 2.000 Zeichen') }}</flux:description>
<flux:textarea
wire:model="storyText"
placeholder="{{ __('Seit 1950 steht unser Haus für...') }}"
rows="5"
/>
<div class="mt-1 text-right text-xs text-zinc-400">
{{ strlen($storyText) }} / 2000
</div>
@error('storyText') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:field>
<flux:label>{{ __('Gründungsjahr') }}</flux:label>
<flux:input
wire:model="foundedYear"
type="number"
min="1800"
:max="date('Y')"
placeholder="{{ __('z.B. 1985') }}"
icon="calendar"
/>
@error('foundedYear') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Fachgebiete / Spezialisierungen') }}</flux:label>
<flux:description>{{ __('Kommagetrennt, z.B. Polstermöbel, Küchen, Matratzen') }}</flux:description>
<flux:input
wire:model="specialtiesInput"
placeholder="{{ __('Polstermöbel, Küchen, Matratzen') }}"
/>
@error('specialtiesInput') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
</div>
</flux:card>
{{-- Öffnungszeiten --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Öffnungszeiten') }}</flux:heading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-3">
@foreach ($dayLabels as $dayKey => $dayLabel)
<div class="flex items-center gap-4">
<div class="w-28 text-sm font-medium text-zinc-700 dark:text-zinc-300">
{{ $dayLabel }}
</div>
<flux:switch
wire:model.live="openingHours.{{ $dayKey }}.closed"
label="{{ __('Geschlossen') }}"
/>
@unless ($openingHours[$dayKey]['closed'] ?? false)
<div class="flex items-center gap-2">
<flux:input
wire:model="openingHours.{{ $dayKey }}.open"
type="time"
class="w-28"
/>
<span class="text-zinc-500"></span>
<flux:input
wire:model="openingHours.{{ $dayKey }}.close"
type="time"
class="w-28"
/>
</div>
@endunless
</div>
@endforeach
</div>
</flux:card>
{{-- Aktionen --}}
<div class="flex justify-end gap-3">
<flux:button href="{{ route('admin.partners.index') }}" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button
type="submit"
variant="primary"
icon="check"
wire:loading.attr="disabled"
wire:target="save"
>
<span wire:loading.remove wire:target="save">{{ __('Speichern') }}</span>
<span wire:loading wire:target="save">
<flux:icon.arrow-path class="animate-spin inline-block mr-1 h-4 w-4" />
{{ __('Wird gespeichert...') }}
</span>
</flux:button>
</div>
</form>
</div>

View file

@ -0,0 +1,220 @@
<?php
use App\Models\Partner;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use function Livewire\Volt\{layout, title};
layout('components.layouts.app');
title('Partner-Verwaltung');
new class extends Component {
use WithPagination;
public string $search = '';
public string $typeFilter = '';
public bool $onlyActive = false;
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedTypeFilter(): void
{
$this->resetPage();
}
public function updatedOnlyActive(): void
{
$this->resetPage();
}
public function with(): array
{
$this->authorize('viewAny', Partner::class);
$partners = Partner::query()
->with(['hub', 'users'])
->when($this->search, fn ($q) => $q->where(function ($q) {
$q->where('company_name', 'like', "%{$this->search}%")
->orWhere('city', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
}))
->when($this->typeFilter, fn ($q) => $q->where('type', $this->typeFilter))
->when($this->onlyActive, fn ($q) => $q->where('is_active', true))
->orderBy('company_name')
->paginate(20);
return [
'partners' => $partners,
'totalCount' => Partner::count(),
'activeCount' => Partner::where('is_active', true)->count(),
];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Partner-Verwaltung') }}</flux:heading>
<flux:subheading>{{ __('Alle registrierten Partner auf der Plattform') }}</flux:subheading>
</div>
<flux:button
href="{{ route('admin.partners.invite') }}"
variant="primary"
icon="plus"
>
{{ __('Partner einladen') }}
</flux:button>
</div>
{{-- Statistics --}}
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Gesamt') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $totalCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/20">
<flux:icon.building-office 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>{{ __('Aktiv') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $activeCount }}</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>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Inaktiv') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $totalCount - $activeCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
<flux:icon.x-circle class="h-6 w-6 text-zinc-600 dark:text-zinc-400" />
</div>
</div>
</flux:card>
</div>
{{-- Filter --}}
<flux:card class="shadow-elegant">
<div class="flex flex-wrap items-end gap-4">
<flux:field class="flex-1">
<flux:label>{{ __('Suche') }}</flux:label>
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Firmenname, Stadt oder E-Mail...') }}"
icon="magnifying-glass"
/>
</flux:field>
<flux:field>
<flux:label>{{ __('Partner-Typ') }}</flux:label>
<flux:select wire:model.live="typeFilter">
<flux:select.option value="">{{ __('Alle Typen') }}</flux:select.option>
<flux:select.option value="retailer">{{ __('Händler') }}</flux:select.option>
<flux:select.option value="manufacturer">{{ __('Hersteller') }}</flux:select.option>
<flux:select.option value="estate_agent">{{ __('Makler') }}</flux:select.option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:switch wire:model.live="onlyActive" label="{{ __('Nur aktive') }}" />
</flux:field>
</div>
</flux:card>
{{-- Partner-Tabelle --}}
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Partner') }}</flux:table.column>
<flux:table.column>{{ __('Typ') }}</flux:table.column>
<flux:table.column>{{ __('Hub') }}</flux:table.column>
<flux:table.column>{{ __('Benutzer') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($partners as $partner)
<flux:table.row>
<flux:table.cell>
<div class="font-medium text-zinc-900 dark:text-white">
{{ $partner->company_name }}
</div>
@if ($partner->city)
<div class="text-sm text-zinc-500">{{ $partner->zip }} {{ $partner->city }}</div>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="{{ match($partner->type?->value ?? $partner->type) {
'retailer' => 'blue',
'manufacturer' => 'purple',
'estate_agent' => 'amber',
default => 'zinc'
} }}">
{{ $partner->type?->label() ?? ucfirst($partner->type ?? '') }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
{{ $partner->hub?->name ?? '' }}
</flux:table.cell>
<flux:table.cell>
{{ $partner->users->count() }}
</flux:table.cell>
<flux:table.cell>
@if ($partner->is_active)
<flux:badge size="sm" color="green">{{ __('Aktiv') }}</flux:badge>
@else
<flux:badge size="sm" color="zinc">{{ __('Inaktiv') }}</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:button
href="{{ route('admin.partners.edit', $partner->id) }}"
size="sm"
variant="ghost"
icon="pencil"
>
{{ __('Bearbeiten') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6" class="py-12 text-center text-zinc-500">
<flux:icon.building-office class="mx-auto mb-2 h-12 w-12 text-zinc-400" />
<div>{{ __('Keine Partner gefunden') }}</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@if ($partners->hasPages())
<div class="mt-4">
{{ $partners->links() }}
</div>
@endif
</flux:card>
</div>

View file

@ -0,0 +1,563 @@
<?php
use App\Enums\ProductStatus;
use App\Enums\ProductType;
use App\Models\Category;
use App\Models\Product;
use Flux\Flux;
use Illuminate\Support\Facades\Storage;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use function Livewire\Volt\{layout, title};
layout('components.layouts.app');
title('Produkt-Verwaltung');
new class extends Component {
use WithPagination;
public string $search = '';
public string $statusFilter = '';
public string $productTypeFilter = '';
public string $categoryFilter = '';
public string $partnerFilter = '';
public string $curationNotes = '';
public string $rejectionReason = '';
public ?int $correctingProductId = null;
public ?int $rejectingProductId = null;
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function updatedProductTypeFilter(): void
{
$this->resetPage();
}
public function updatedCategoryFilter(): void
{
$this->resetPage();
}
public function updatedPartnerFilter(): void
{
$this->resetPage();
}
public function approve(int $productId): void
{
$this->authorize('curate', Product::class);
$product = Product::findOrFail($productId);
$product->update([
'status' => ProductStatus::Active,
'is_curated' => true,
'curated_at' => now(),
'curated_by' => auth()->id(),
'curation_notes' => null,
]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'approved',
'note' => null,
]);
Flux::toast(variant: 'success', text: __('Produkt ":name" wurde freigegeben.', ['name' => $product->name]), duration: 5000);
}
public function openCorrection(int $productId): void
{
$this->correctingProductId = $productId;
$this->curationNotes = '';
$this->rejectingProductId = null;
$this->rejectionReason = '';
}
public function cancelCorrection(): void
{
$this->correctingProductId = null;
$this->curationNotes = '';
}
public function sendCorrection(int $productId): void
{
$this->authorize('curate', Product::class);
$this->validate([
'curationNotes' => 'required|string|max:5000',
], [
'curationNotes.required' => __('Bitte geben Sie eine Korrekturanweisung ein.'),
]);
$product = Product::findOrFail($productId);
$product->update([
'status' => ProductStatus::Correction,
'is_curated' => false,
'curation_notes' => $this->curationNotes,
]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'correction',
'note' => $this->curationNotes,
]);
$this->correctingProductId = null;
$this->curationNotes = '';
Flux::toast(variant: 'success', text: __('Korrekturanweisung für ":name" wurde gesendet.', ['name' => $product->name]), duration: 5000);
}
public function openRejection(int $productId): void
{
$this->rejectingProductId = $productId;
$this->rejectionReason = '';
$this->correctingProductId = null;
$this->curationNotes = '';
}
public function cancelRejection(): void
{
$this->rejectingProductId = null;
$this->rejectionReason = '';
}
public function reject(int $productId): void
{
$this->authorize('curate', Product::class);
$this->validate([
'rejectionReason' => 'required|string|max:5000',
], [
'rejectionReason.required' => __('Bitte geben Sie einen Ablehnungsgrund ein.'),
]);
$product = Product::findOrFail($productId);
$product->update([
'status' => ProductStatus::Archived,
'is_curated' => false,
'curation_notes' => $this->rejectionReason,
]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'rejected',
'note' => $this->rejectionReason,
]);
$this->rejectingProductId = null;
$this->rejectionReason = '';
Flux::toast(variant: 'success', text: __('Produkt ":name" wurde abgelehnt.', ['name' => $product->name]), duration: 5000);
}
public function archiveProduct(int $productId): void
{
$this->authorize('curate', Product::class);
$product = Product::findOrFail($productId);
$product->update(['status' => ProductStatus::Archived]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'archived',
'note' => null,
]);
Flux::toast(variant: 'success', text: __('Produkt ":name" wurde archiviert.', ['name' => $product->name]), duration: 5000);
}
public function markAsSold(int $productId): void
{
$this->authorize('curate', Product::class);
$product = Product::findOrFail($productId);
$product->update(['status' => ProductStatus::Sold]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'sold',
'note' => null,
]);
Flux::toast(variant: 'success', text: __('Produkt ":name" als verkauft markiert.', ['name' => $product->name]), duration: 5000);
}
public function with(): array
{
$this->authorize('curate', Product::class);
$query = Product::query()
->with(['partner', 'categories', 'media'])
->when($this->search, fn($q) => $q->where(function ($q) {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('b2in_article_number', 'like', "%{$this->search}%")
->orWhere('partner_product_number', 'like', "%{$this->search}%");
}))
->when($this->statusFilter, fn($q) => $q->where('status', $this->statusFilter))
->when($this->productTypeFilter, fn($q) => $q->where('product_type', $this->productTypeFilter))
->when($this->categoryFilter, fn($q) => $q->whereHas('categories', fn($q) => $q->where('categories.id', $this->categoryFilter)))
->when($this->partnerFilter, fn($q) => $q->where('partner_id', $this->partnerFilter));
$products = $query->latest()->paginate(25);
$categories = Category::orderBy('name')->get();
$partners = \App\Models\Partner::orderBy('company_name')->get(['id', 'company_name']);
$statusCounts = Product::query()
->selectRaw("status, count(*) as count")
->groupBy('status')
->pluck('count', 'status');
return [
'products' => $products,
'categories' => $categories,
'partners' => $partners,
'totalCount' => Product::count(),
'pendingCount' => $statusCounts[ProductStatus::Pending->value] ?? 0,
'correctionCount' => $statusCounts[ProductStatus::Correction->value] ?? 0,
'activeCount' => $statusCounts[ProductStatus::Active->value] ?? 0,
];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Produkt-Verwaltung') }}</flux:heading>
<flux:subheading>{{ __('Alle Produkte verwalten, freigeben und bearbeiten') }}</flux:subheading>
</div>
<div class="flex items-center gap-2">
<flux:button variant="primary" icon="plus" href="{{ route('products.create.teaser') }}">
{{ __('Neues Teaser-Produkt') }}
</flux:button>
<flux:button variant="primary" icon="plus" href="{{ route('products.create.standard') }}">
{{ __('Neues Standard-Produkt') }}
</flux:button>
</div>
</div>
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Gesamt') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $totalCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/20">
<flux:icon.shopping-bag class="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant cursor-pointer" wire:click="$set('statusFilter', 'pending')">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Zur Freigabe') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $pendingCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-amber-100 dark:bg-amber-900/20">
<flux:icon.clock class="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant cursor-pointer" wire:click="$set('statusFilter', 'correction')">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('In Korrektur') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $correctionCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/20">
<flux:icon.arrow-uturn-left class="h-6 w-6 text-orange-600 dark:text-orange-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant cursor-pointer" wire:click="$set('statusFilter', 'active')">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Freigegeben') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $activeCount }}</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>
</div>
{{-- Filter --}}
<flux:card class="shadow-elegant">
<div class="flex flex-wrap items-end gap-4">
<flux:field class="flex-1">
<flux:label>{{ __('Suche') }}</flux:label>
<flux:input wire:model.live.debounce.300ms="search"
placeholder="{{ __('Name, Artikelnummer...') }}" icon="magnifying-glass" />
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:select wire:model.live="statusFilter">
<flux:select.option value="">{{ __('Alle Status') }}</flux:select.option>
@foreach (ProductStatus::cases() as $status)
<flux:select.option value="{{ $status->value }}">{{ $status->label() }}</flux:select.option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Produkttyp') }}</flux:label>
<flux:select wire:model.live="productTypeFilter">
<flux:select.option value="">{{ __('Alle Typen') }}</flux:select.option>
<flux:select.option value="local_stock">{{ __('Teaser (Local Express)') }}</flux:select.option>
<flux:select.option value="smart_order">{{ __('Standard (Smart Club)') }}</flux:select.option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Händler') }}</flux:label>
<flux:select wire:model.live="partnerFilter">
<flux:select.option value="">{{ __('Alle Händler') }}</flux:select.option>
@foreach ($partners as $partner)
<flux:select.option :value="$partner->id">{{ $partner->company_name }}</flux:select.option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Kategorie') }}</flux:label>
<flux:select wire:model.live="categoryFilter">
<flux:select.option value="">{{ __('Alle Kategorien') }}</flux:select.option>
@foreach ($categories as $category)
<flux:select.option :value="$category->id">{{ $category->name }}</flux:select.option>
@endforeach
</flux:select>
</flux:field>
</div>
</flux:card>
{{-- Produkttabelle --}}
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Produkt') }}</flux:table.column>
<flux:table.column>{{ __('Händler') }}</flux:table.column>
<flux:table.column>{{ __('Kategorie') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Kuration') }}</flux:table.column>
<flux:table.column>{{ __('Erstellt') }}</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($products as $product)
<flux:table.row wire:key="product-{{ $product->id }}">
{{-- Produkt --}}
<flux:table.cell>
<div class="flex items-center gap-3">
@php
$thumbnail = $product->media->sortBy('order_column')->first();
@endphp
@if ($thumbnail)
<img src="{{ Storage::url($thumbnail->file_path) }}"
alt="{{ $thumbnail->alt_text ?? $product->name }}"
class="h-12 w-12 shrink-0 rounded-lg border border-zinc-200 object-cover dark:border-zinc-700" />
@else
<div
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
<flux:icon.photo class="h-5 w-5 text-zinc-400" />
</div>
@endif
<div>
<div class="font-medium text-zinc-900 dark:text-white">
{{ $product->name }}
</div>
<div class="flex items-center gap-2 mt-1">
<flux:badge size="sm"
color="{{ $product->product_type?->value === 'local_stock' ? 'amber' : 'blue' }}">
{{ $product->product_type?->label() ?? '' }}
</flux:badge>
@if ($product->b2in_article_number)
<span class="text-xs text-zinc-400">{{ $product->b2in_article_number }}</span>
@endif
</div>
</div>
</div>
</flux:table.cell>
{{-- Händler --}}
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ $product->partner?->company_name ?? '' }}
</span>
</flux:table.cell>
{{-- Kategorie --}}
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ $product->categories->first()?->name ?? '' }}
</span>
</flux:table.cell>
{{-- Status --}}
<flux:table.cell>
<flux:badge size="sm" color="{{ $product->status?->color() ?? 'zinc' }}">
{{ $product->status?->label() ?? '' }}
</flux:badge>
</flux:table.cell>
{{-- Kuration --}}
<flux:table.cell>
@if ($product->status === ProductStatus::Pending)
<div class="flex items-center gap-1">
<flux:button wire:click="approve({{ $product->id }})"
wire:loading.attr="disabled"
size="xs" variant="primary" icon="check"
title="{{ __('Freigeben') }}" />
<flux:button wire:click="openCorrection({{ $product->id }})"
wire:loading.attr="disabled"
size="xs" variant="filled" icon="arrow-uturn-left"
title="{{ __('Korrektur') }}" />
<flux:button wire:click="openRejection({{ $product->id }})"
wire:loading.attr="disabled"
size="xs" variant="danger" icon="x-mark"
title="{{ __('Ablehnen') }}" />
</div>
@elseif ($product->status === ProductStatus::Correction)
<flux:badge size="sm" color="orange" icon="arrow-uturn-left">
{{ __('Warte auf Korrektur') }}
</flux:badge>
@elseif ($product->is_curated)
<flux:badge size="sm" color="green" icon="check-circle">
{{ __('Freigegeben') }}
</flux:badge>
@else
<span class="text-xs text-zinc-400"></span>
@endif
</flux:table.cell>
{{-- Erstellt --}}
<flux:table.cell>
<span class="text-sm text-zinc-500">
{{ $product->created_at?->format('d.m.Y') }}
</span>
</flux:table.cell>
{{-- Aktionen --}}
<flux:table.cell>
<div class="flex items-center gap-1">
<flux:button
href="{{ $product->product_type === ProductType::LocalStock ? route('products.edit.teaser', $product) : route('products.edit.standard', $product) }}"
size="sm" variant="ghost" icon="pencil"
title="{{ __('Bearbeiten') }}" />
@if (!in_array($product->status, [ProductStatus::Archived, ProductStatus::Sold]))
<flux:dropdown>
<flux:button size="sm" variant="ghost" icon="ellipsis-vertical" />
<flux:menu>
<flux:menu.item wire:click="markAsSold({{ $product->id }})"
wire:confirm="{{ __('Produkt wirklich als verkauft markieren?') }}"
icon="check-badge">
{{ __('Als verkauft') }}
</flux:menu.item>
<flux:menu.item wire:click="archiveProduct({{ $product->id }})"
wire:confirm="{{ __('Produkt wirklich archivieren?') }}"
icon="archive-box" variant="danger">
{{ __('Archivieren') }}
</flux:menu.item>
</flux:menu>
</flux:dropdown>
@endif
</div>
</flux:table.cell>
</flux:table.row>
{{-- Inline Korrektur-Formular --}}
@if ($correctingProductId === $product->id)
<flux:table.row wire:key="correction-{{ $product->id }}">
<flux:table.cell colspan="7">
<div class="rounded-lg border border-orange-200 bg-orange-50 p-4 dark:border-orange-800 dark:bg-orange-950/30">
<flux:field>
<flux:label>{{ __('Korrekturanweisung für ":name"', ['name' => $product->name]) }}</flux:label>
<flux:textarea wire:model="curationNotes" rows="3"
placeholder="{{ __('Bitte beschreiben Sie, was der Partner korrigieren soll...') }}" />
<flux:error name="curationNotes" />
</flux:field>
<div class="mt-3 flex gap-2">
<flux:button wire:click="sendCorrection({{ $product->id }})"
wire:loading.attr="disabled" variant="primary" size="sm"
icon="paper-airplane">
{{ __('Korrektur senden') }}
</flux:button>
<flux:button wire:click="cancelCorrection" variant="ghost" size="sm">
{{ __('Abbrechen') }}
</flux:button>
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endif
{{-- Inline Ablehnungs-Formular --}}
@if ($rejectingProductId === $product->id)
<flux:table.row wire:key="rejection-{{ $product->id }}">
<flux:table.cell colspan="7">
<div class="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-950/30">
<flux:field>
<flux:label>{{ __('Ablehnungsgrund für ":name"', ['name' => $product->name]) }}</flux:label>
<flux:textarea wire:model="rejectionReason" rows="3"
placeholder="{{ __('Bitte geben Sie den Grund für die Ablehnung an...') }}" />
<flux:error name="rejectionReason" />
</flux:field>
<div class="mt-3 flex gap-2">
<flux:button wire:click="reject({{ $product->id }})"
wire:loading.attr="disabled" variant="danger" size="sm"
icon="x-mark">
{{ __('Ablehnen') }}
</flux:button>
<flux:button wire:click="cancelRejection" variant="ghost" size="sm">
{{ __('Abbrechen') }}
</flux:button>
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endif
{{-- Korrekturhinweis anzeigen --}}
@if ($product->status === ProductStatus::Correction && $product->curation_notes)
<flux:table.row wire:key="correction-notes-{{ $product->id }}">
<flux:table.cell colspan="7">
<flux:callout variant="warning" icon="exclamation-triangle">
<flux:callout.heading>{{ __('Korrekturanweisung') }}</flux:callout.heading>
<flux:callout.text>{{ $product->curation_notes }}</flux:callout.text>
</flux:callout>
</flux:table.cell>
</flux:table.row>
@endif
@empty
<flux:table.row>
<flux:table.cell colspan="7" class="py-12 text-center text-zinc-500">
<flux:icon.shopping-bag class="mx-auto mb-2 h-12 w-12 text-zinc-400" />
<div>{{ __('Keine Produkte gefunden') }}</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@if ($products->hasPages())
<div class="mt-4">
{{ $products->links() }}
</div>
@endif
</flux:card>
</div>

View file

@ -39,7 +39,6 @@ new #[Layout('components.layouts.auth')] class extends Component {
RateLimiter::clear($this->throttleKey());
Session::regenerate();
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
}

View file

@ -54,7 +54,7 @@ new #[Layout('components.layouts.app'), Title('Meine Daten')] class extends Comp
}
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
$this->partnerType = $this->partner->type;
$this->partnerType = $this->partner->type?->value ?? '';
// Vorausfüllen: Partner-Daten
$this->companyName = $this->partner->company_name ?? '';

View file

@ -0,0 +1,216 @@
<?php
use App\Enums\ProductStatus;
use App\Models\Partner;
use App\Models\Product;
use Livewire\Volt\Component;
use function Livewire\Volt\{layout, title};
layout('components.layouts.app');
new class extends Component {
public Partner $partner;
public string $title = '';
public function mount(int $partnerId): void
{
$this->partner = Partner::with(['hub', 'products' => function ($q) {
$q->where('status', ProductStatus::Active)
->where('is_curated', true)
->where('is_available', true)
->with(['categories', 'media'])
->latest()
->limit(6);
}])->findOrFail($partnerId);
$this->title = $this->partner->display_name ?? $this->partner->company_name;
}
public function with(): array
{
return [
'partner' => $this->partner,
'products' => $this->partner->products,
];
}
}; ?>
<div class="space-y-8 p-6">
{{-- Partner-Header --}}
<flux:card class="shadow-elegant">
<div class="flex flex-col md:flex-row items-start gap-6">
{{-- Logo / Placeholder --}}
<div class="flex h-24 w-24 shrink-0 items-center justify-center rounded-xl bg-zinc-100 dark:bg-zinc-800">
<flux:icon.building-office class="h-12 w-12 text-zinc-400" />
</div>
<div class="flex-1">
<flux:heading size="2xl">{{ $partner->display_name ?? $partner->company_name }}</flux:heading>
@if($partner->display_name && $partner->display_name !== $partner->company_name)
<div class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ $partner->company_name }}</div>
@endif
<div class="mt-3 flex flex-wrap gap-3">
@if($partner->type)
<flux:badge color="blue">{{ $partner->type?->label() ?? $partner->type }}</flux:badge>
@endif
@if($partner->hub)
<flux:badge color="zinc" icon="map-pin">{{ $partner->hub->name }}</flux:badge>
@endif
@if($partner->is_active)
<flux:badge color="green">{{ __('Aktiv') }}</flux:badge>
@endif
</div>
{{-- Kontaktdaten --}}
<div class="mt-4 flex flex-wrap gap-x-6 gap-y-2 text-sm text-zinc-600 dark:text-zinc-400">
@if($partner->city)
<span class="flex items-center gap-1">
<flux:icon.map-pin class="h-4 w-4" />
{{ $partner->zip }} {{ $partner->city }}
</span>
@endif
@if($partner->phone)
<span class="flex items-center gap-1">
<flux:icon.phone class="h-4 w-4" />
{{ $partner->phone }}
</span>
@endif
@if($partner->website)
<a href="{{ $partner->website }}" target="_blank" rel="noopener noreferrer"
class="flex items-center gap-1 hover:text-zinc-900 dark:hover:text-zinc-100">
<flux:icon.globe-alt class="h-4 w-4" />
{{ parse_url($partner->website, PHP_URL_HOST) }}
</a>
@endif
@if($partner->founded_year)
<span class="flex items-center gap-1">
<flux:icon.calendar class="h-4 w-4" />
{{ __('Seit') }} {{ $partner->founded_year }}
</span>
@endif
</div>
</div>
</div>
</flux:card>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
{{-- Linke Spalte: Story + Spezialisierungen --}}
<div class="space-y-6 lg:col-span-2">
{{-- Story / Über uns --}}
@if($partner->story_text)
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Über uns') }}</flux:heading>
<div class="text-zinc-700 dark:text-zinc-300 leading-relaxed whitespace-pre-line">
{{ $partner->story_text }}
</div>
</flux:card>
@endif
{{-- Produkte --}}
@if($products->isNotEmpty())
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between mb-4">
<flux:heading size="lg">{{ __('Aktuelle Produkte') }}</flux:heading>
<flux:button variant="ghost" size="sm" href="{{ route('products.index') }}" icon="arrow-right">
{{ __('Alle ansehen') }}
</flux:button>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
@foreach($products as $product)
<div class="rounded-lg border border-zinc-200 dark:border-zinc-700 overflow-hidden">
<div class="flex h-32 items-center justify-center bg-zinc-100 dark:bg-zinc-800">
@if($product->media->first()?->url)
<img src="{{ $product->media->first()->url }}"
alt="{{ $product->name }}"
class="h-full w-full object-cover">
@else
<flux:icon.photo class="h-10 w-10 text-zinc-400" />
@endif
</div>
<div class="p-3">
<div class="font-medium text-sm text-zinc-900 dark:text-zinc-100">
{{ $product->name }}
</div>
@if($product->price_display_text)
<div class="text-sm text-zinc-500 mt-1">{{ $product->price_display_text }}</div>
@elseif($product->price)
<div class="text-sm font-semibold mt-1">{{ number_format($product->price, 2, ',', '.') }} </div>
@else
<div class="text-sm text-zinc-400 mt-1">{{ __('Auf Anfrage') }}</div>
@endif
</div>
</div>
@endforeach
</div>
</flux:card>
@endif
</div>
{{-- Rechte Spalte: Öffnungszeiten + Spezialisierungen --}}
<div class="space-y-6">
{{-- Öffnungszeiten --}}
@if($partner->opening_hours)
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Öffnungszeiten') }}</flux:heading>
@php
$days = [
'monday' => __('Montag'),
'tuesday' => __('Dienstag'),
'wednesday' => __('Mittwoch'),
'thursday' => __('Donnerstag'),
'friday' => __('Freitag'),
'saturday' => __('Samstag'),
'sunday' => __('Sonntag'),
];
@endphp
<div class="space-y-2">
@foreach($days as $key => $label)
@if(isset($partner->opening_hours[$key]))
@php $hours = $partner->opening_hours[$key]; @endphp
<div class="flex items-center justify-between text-sm">
<span class="text-zinc-600 dark:text-zinc-400">{{ $label }}</span>
@if(!empty($hours['closed']))
<span class="text-zinc-400">{{ __('Geschlossen') }}</span>
@elseif(!empty($hours['open']) && !empty($hours['close']))
<span class="font-medium">{{ $hours['open'] }} {{ $hours['close'] }}</span>
@else
<span class="text-zinc-400"></span>
@endif
</div>
@endif
@endforeach
</div>
</flux:card>
@endif
{{-- Spezialisierungen --}}
@if($partner->specialties && count($partner->specialties) > 0)
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Spezialisierungen') }}</flux:heading>
<div class="flex flex-wrap gap-2">
@foreach($partner->specialties as $specialty)
<flux:badge color="zinc">{{ $specialty }}</flux:badge>
@endforeach
</div>
</flux:card>
@endif
{{-- Adresse --}}
@if($partner->street || $partner->city)
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<address class="not-italic text-sm text-zinc-700 dark:text-zinc-300 space-y-1">
@if($partner->street)
<div>{{ $partner->street }} {{ $partner->house_number }}</div>
@endif
@if($partner->zip || $partner->city)
<div>{{ $partner->zip }} {{ $partner->city }}</div>
@endif
</address>
</flux:card>
@endif
</div>
</div>
</div>

View file

@ -1,794 +0,0 @@
<?php
use function Livewire\Volt\{state, mount};
state([
'product' => [],
'activeTab' => 'basis'
]);
mount(function () {
// Initialisierung der Dummy-Daten
});
?>
<div class="space-y-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Produkte') }} (in Entwicklung)</flux:heading>
<flux:subheading>{{ __('Erstellen Sie ein neues Produkt') }}</flux:subheading>
</div>
<div class="flex items-center gap-2">
@svg('heroicon-o-cube', 'w-6 h-6 text-accent-600 dark:text-accent-400')
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ __('Neues Produkt anlegen') }}</span>
</div>
</div>
<form wire:submit="save" class="space-y-6">
{{-- Tab Navigation --}}
<flux:tabs wire:model.live="activeTab" variant="segmented">
<flux:tab name="basis" icon="identification">{{ __('Basis') }}</flux:tab>
<flux:tab name="bilder" icon="photo">{{ __('Bilder') }}</flux:tab>
<flux:tab name="physisch" icon="cube">{{ __('Physisch') }}</flux:tab>
<flux:tab name="material" icon="beaker">{{ __('Material & Herkunft') }}</flux:tab>
<flux:tab name="kommerziell" icon="currency-euro">{{ __('Kommerziell') }}</flux:tab>
<flux:tab name="verwaltung" icon="cog">{{ __('Zuordnung & Verwaltung') }}</flux:tab>
</flux:tabs>
{{-- TAB 1: BASIS - Identität & Varianten --}}
@if($activeTab === 'basis')
<div class="space-y-8">
{{-- 1. Identität & Katalog --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('1. Identität & Katalog') }}</flux:heading>
<flux:field>
<flux:label>{{ __('B2in-Artikelnummer (intern)') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.b2in_article_number" placeholder="B2IN-000471" />
<flux:description>{{ __('Fortlaufende Nummer (vom System vergeben)') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Lieferanten-Artikelnummer') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.supplier_article_number" placeholder="SOFA-ALBA-3S-ANTHR" />
<flux:description>{{ __('Originalnummer des Herstellers') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Produktname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.product_name" placeholder="Sofa ALBA 3-Sitzer" />
<flux:description>{{ __('Anzeigename auf Website') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Marke / Hersteller') }}</flux:label>
<flux:input wire:model="product.brand" placeholder="Möbelwerk Nord" />
<flux:description>{{ __('Produzent oder Label') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Kategorie') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.category" placeholder="{{ __('Bitte wählen...') }}">
<option value="sofas">{{ __('Wohnzimmer > Sofas') }}</option>
<option value="chairs">{{ __('Esszimmer > Stühle') }}</option>
<option value="beds">{{ __('Schlafzimmer > Betten') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
<flux:textarea wire:model="product.short_description" rows="3" placeholder="Modernes Sofa mit Holzrahmen und Stoff"></flux:textarea>
<flux:description>{{ __('Max. 180 Zeichen für Snippets') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Langbeschreibung') }}</flux:label>
<flux:textarea wire:model="product.long_description" rows="6" placeholder="Das Sofa ALBA verbindet zeitloses Design..."></flux:textarea>
<flux:description>{{ __('Detaillierter Text für Produktseite') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.status">
<option value="active">{{ __('Aktiv') }}</option>
<option value="draft">{{ __('Entwurf') }}</option>
<option value="inactive">{{ __('Inaktiv') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Erstelldatum / Änderungsdatum') }}</flux:label>
<flux:input type="date" wire:model="product.created_at" value="2025-11-04" />
<flux:description>{{ __('ISO-Datum') }}</flux:description>
</flux:field>
</flux:card>
{{-- 2. Varianten & Attribute --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('2. Varianten & Attribute') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Variantenattribute (Stammdaten)') }}</flux:label>
<flux:input wire:model="product.variant_attributes" placeholder="Farbe, Bezug, Gestellfarbe" />
<flux:description>{{ __('Merkmale, die die SKUs definieren') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Varianten (Kombinationen)') }}</flux:label>
<flux:input wire:model="product.variants" placeholder="Anthrazit / Stoff A / Eiche hell" />
<flux:description>{{ __('Konkrete Ausprägungen') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Weitere Attribute') }}</flux:label>
<flux:input wire:model="product.additional_attributes" placeholder="Sitzhärte: mittel" />
<flux:description>{{ __('Zusatzinfos (z. B. Sitzhärte, Stil)') }}</flux:description>
</flux:field>
</flux:card>
</div>
@endif
{{-- TAB 2: BILDER - Upload & Verwaltung --}}
@if($activeTab === 'bilder')
<div class="space-y-8">
{{-- Hauptbild & Galerie --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('Produktbilder') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Hauptbild') }} <span class="text-red-500">*</span></flux:label>
{{-- <flux:input type="file" wire:model="product.main_image" accept="image/*" /> --}}
<flux:description>{{ __('Hauptansicht des Produkts (min. 1200x1200px, max. 5MB)') }}</flux:description>
</flux:field>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg p-4 flex items-center justify-center h-40">
<div class="text-center">
<flux:icon.photo class="h-12 w-12 mx-auto text-zinc-400 mb-2" />
<span class="text-sm text-zinc-500">{{ __('Hauptbild Vorschau') }}</span>
</div>
</div>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Produktgalerie') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.gallery_images" accept="image/*" multiple /> --}}
<flux:description>{{ __('Mehrere Bilder hochladen (max. 10 Bilder, je max. 5MB)') }}</flux:description>
</flux:field>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@for($i = 1; $i <= 8; $i++)
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg p-4 flex items-center justify-center h-40">
<div class="text-center">
<flux:icon.photo class="h-8 w-8 mx-auto text-zinc-400 mb-1" />
<span class="text-xs text-zinc-500">{{ __('Bild') }} {{ $i }}</span>
</div>
</div>
@endfor
</div>
</flux:card>
{{-- Detailbilder & Ansichten --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('Detailansichten & Perspektiven') }}</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:field>
<flux:label>{{ __('Vorderansicht') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.view_front" accept="image/*" /> --}}
<flux:description>{{ __('Frontale Produktansicht') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Rückansicht') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.view_back" accept="image/*" /> --}}
<flux:description>{{ __('Rückseite des Produkts') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Seitenansicht (links)') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.view_left" accept="image/*" /> --}}
<flux:description>{{ __('Linke Seite') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Seitenansicht (rechts)') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.view_right" accept="image/*" /> --}}
<flux:description>{{ __('Rechte Seite') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Detailaufnahme 1') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.detail_1" accept="image/*" /> --}}
<flux:description>{{ __('z.B. Material-Nahaufnahme') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Detailaufnahme 2') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.detail_2" accept="image/*" /> --}}
<flux:description>{{ __('z.B. Verarbeitung, Nähte') }}</flux:description>
</flux:field>
</div>
</flux:card>
{{-- Ambiente & Lifestyle --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('Ambiente & Lifestyle-Bilder') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Ambiente-Bilder') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.lifestyle_images" accept="image/*" multiple /> --}}
<flux:description>{{ __('Produkt in Wohnsituation (max. 5 Bilder)') }}</flux:description>
</flux:field>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
@for($i = 1; $i <= 3; $i++)
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg p-4 flex items-center justify-center h-48">
<div class="text-center">
<flux:icon.home class="h-10 w-10 mx-auto text-zinc-400 mb-2" />
<span class="text-sm text-zinc-500">{{ __('Ambiente') }} {{ $i }}</span>
</div>
</div>
@endfor
</div>
</flux:card>
{{-- 360° Ansicht & Video --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('360° Ansicht & Produktvideo') }}</flux:heading>
<flux:field>
<flux:label>{{ __('360° Bilder') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.rotation_images" accept="image/*" multiple /> --}}
<flux:description>{{ __('Bilder für 360° Rotation (min. 24 Bilder empfohlen)') }}</flux:description>
</flux:field>
<flux:separator />
<flux:field>
<flux:label>{{ __('Produktvideo') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.product_video" accept="video/*" /> --}}
<flux:description>{{ __('Kurzes Produktvideo (max. 50MB, MP4/WebM)') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Video-URL (alternativ)') }}</flux:label>
<flux:input wire:model="product.video_url" placeholder="https://youtube.com/watch?v=..." />
<flux:description>{{ __('YouTube, Vimeo oder andere Video-URL') }}</flux:description>
</flux:field>
</flux:card>
{{-- Technische Zeichnungen & Dokumente --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('Technische Zeichnungen & Dokumente') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Maßzeichnung') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.dimension_drawing" accept="image/*,application/pdf" /> --}}
<flux:description>{{ __('Technische Zeichnung mit Maßen (PNG, JPG oder PDF)') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Montageanleitung') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.assembly_manual" accept="application/pdf" /> --}}
<flux:description>{{ __('PDF mit Montageanleitung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Datenblatt / Broschüre') }}</flux:label>
{{-- <flux:input type="file" wire:model="product.datasheet" accept="application/pdf" /> --}}
<flux:description>{{ __('Produktdatenblatt als PDF') }}</flux:description>
</flux:field>
</flux:card>
{{-- Bild-Metadaten & Alt-Texte --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('Bild-Metadaten & SEO') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Alt-Text (Hauptbild)') }}</flux:label>
<flux:input wire:model="product.main_image_alt" placeholder="Sofa ALBA 3-Sitzer in Anthrazit" />
<flux:description>{{ __('Beschreibung für Suchmaschinen und Barrierefreiheit') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Bildnachweis / Copyright') }}</flux:label>
<flux:input wire:model="product.image_credits" placeholder="© Möbelwerk Nord 2024" />
<flux:description>{{ __('Fotografen-Nennung oder Copyright-Hinweis') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Bildoptimierung') }}</flux:label>
<flux:checkbox wire:model="product.auto_optimize_images">
{{ __('Bilder automatisch für Web optimieren (Komprimierung & Skalierung)') }}
</flux:checkbox>
</flux:field>
<flux:field>
<flux:label>{{ __('Wasserzeichen') }}</flux:label>
<flux:checkbox wire:model="product.add_watermark">
{{ __('B2in-Wasserzeichen auf Bilder anwenden') }}
</flux:checkbox>
</flux:field>
</flux:card>
</div>
@endif
{{-- TAB 3: PHYSISCH - Maße & Verpackung --}}
@if($activeTab === 'physisch')
<div class="space-y-8">
{{-- 3. Maße & Gewicht (Produkt) --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('3. Maße & Gewicht (Produkt)') }}</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<flux:field>
<flux:label>{{ __('Breite (mm)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" wire:model="product.width" placeholder="2200" />
<flux:description>{{ __('Gesamtbreite') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Tiefe (mm)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" wire:model="product.depth" placeholder="950" />
<flux:description>{{ __('Gesamttiefe') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Höhe (mm)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" wire:model="product.height" placeholder="830" />
<flux:description>{{ __('Gesamthöhe') }}</flux:description>
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Gewicht netto (kg)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" step="0.1" wire:model="product.weight" placeholder="68" />
<flux:description>{{ __('Möbel ohne Verpackung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Aufbauart') }}</flux:label>
<flux:select wire:model="product.assembly_type">
<option value="assembled">{{ __('montiert') }}</option>
<option value="partially">{{ __('teilmontiert') }}</option>
<option value="disassembled">{{ __('zerlegt') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Montagezeit (min)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" wire:model="product.assembly_time" placeholder="45" />
<flux:description>{{ __('Aufbauzeit') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Traglast (kg)') }}</flux:label>
<flux:input type="number" wire:model="product.load_capacity" placeholder="120" />
<flux:description>{{ __('Belastbarkeit') }}</flux:description>
</flux:field>
</flux:card>
{{-- 4. Verpackung & Logistik --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('4. Verpackung & Logistik') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Anzahl Packstücke') }}</flux:label>
<flux:input type="number" wire:model="product.package_count" placeholder="2" />
</flux:field>
<flux:field>
<flux:label>{{ __('Gesamtgewicht brutto (kg)') }}</flux:label>
<flux:input type="number" step="0.1" wire:model="product.gross_weight" placeholder="75" />
<flux:description>{{ __('inkl. Verpackung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Verpackungsart') }}</flux:label>
<flux:input wire:model="product.packaging_type" placeholder="Karton mit Kantenschutz" />
<flux:description>{{ __('Karton, Holzrahmen usw.') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Verpackung recyclingfähig (%)') }}</flux:label>
<flux:input type="number" wire:model="product.packaging_recyclable" placeholder="85" />
<flux:description>{{ __('Anteil recycelbarer Materialien der Verpackung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Kolli 1 Maße (mm)') }}</flux:label>
<flux:input wire:model="product.package_1_dimensions" placeholder="L × B × H: 1500 × 950 × 600" />
</flux:field>
<flux:field>
<flux:label>{{ __('Kolli 1 Gewicht (kg)') }}</flux:label>
<flux:input type="number" step="0.1" wire:model="product.package_1_weight" placeholder="45" />
</flux:field>
<flux:field>
<flux:label>{{ __('Palettenfähig') }}</flux:label>
<flux:select wire:model="product.palletizable">
<option value="yes">{{ __('Ja') }}</option>
<option value="no">{{ __('Nein') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('HS-Code (Zolltarifnummer)') }}</flux:label>
<flux:input wire:model="product.hs_code" placeholder="94016100" />
</flux:field>
</flux:card>
</div>
@endif
{{-- TAB 4: MATERIAL & HERKUNFT - Materialien & Holzherkunft --}}
@if($activeTab === 'material')
<div class="space-y-8">
{{-- 5. Materialien & Qualität --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('5. Materialien & Qualität') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Hauptmaterial') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.main_material" placeholder="Massivholz Buche" />
<flux:description>{{ __('Tragende Struktur') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Oberflächenmaterial') }}</flux:label>
<flux:input wire:model="product.surface_material" placeholder="Furnier Eiche geölt" />
<flux:description>{{ __('Sichtflächen') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Bezugsmaterial') }}</flux:label>
<flux:input wire:model="product.upholstery_material" placeholder="Stoff (Polyester)" />
<flux:description>{{ __('Stoff / Leder / Synthetik') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Farbton / Dekor') }}</flux:label>
<flux:input wire:model="product.color" placeholder="Eiche natur / Anthrazit" />
</flux:field>
<flux:field>
<flux:label>{{ __('Herkunftsland (Produktion)') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.country_of_origin">
<option value="DE">{{ __('Deutschland') }}</option>
<option value="PL">{{ __('Polen') }}</option>
<option value="IT">{{ __('Italien') }}</option>
<option value="AT">{{ __('Österreich') }}</option>
</flux:select>
<flux:description>{{ __('ISO-Land') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Pflegehinweise') }}</flux:label>
<flux:textarea wire:model="product.care_instructions" rows="3" placeholder="Reinigung & Pflege"></flux:textarea>
<flux:description>{{ __('Mit feuchtem Tuch abwischen.') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Zertifikate / Labels') }}</flux:label>
<flux:input wire:model="product.certificates" placeholder="FSC, OEKO-TEX, Blauer Engel etc." />
</flux:field>
</flux:card>
{{-- 6. Holzherkunft & EUDR --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('6. Holzherkunft & EUDR') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Holzart(en)') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.wood_type" placeholder="Quercus robur (Eiche)" />
<flux:description>{{ __('Botanische Bezeichnung (falls Holz enthalten)') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Herkunftsland des Holzes') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.wood_origin_country">
<option value="PL">{{ __('Polen') }}</option>
<option value="DE">{{ __('Deutschland') }}</option>
<option value="RO">{{ __('Rumänien') }}</option>
</flux:select>
<flux:description>{{ __('ISO-Code (falls Holz enthalten)') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Region / Provinz') }}</flux:label>
<flux:input wire:model="product.wood_region" placeholder="Masowien" />
<flux:description>{{ __('falls erforderlich für EUDR') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Erntejahr') }}</flux:label>
<flux:input type="number" wire:model="product.harvest_year" placeholder="2024" />
<flux:description>{{ __('Jahr der Holzgewinnung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Forstbetrieb / Lieferant') }}</flux:label>
<flux:input wire:model="product.forest_operator" placeholder="ForestPol Sp. z o.o." />
</flux:field>
<flux:field>
<flux:label>{{ __('Nachhaltigkeitszertifikat') }}</flux:label>
<flux:input wire:model="product.sustainability_certificate" placeholder="FSC C123456" />
<flux:description>{{ __('FSC / PEFC') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Sorgfaltserklärung (EUDR-DD-Referenz)') }}</flux:label>
<flux:input wire:model="product.eudr_reference" placeholder="EUDR-DD-2025-PL-03421" />
<flux:description>{{ __('offiziell Referenz') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Nachweis­datei (Upload)') }}</flux:label>
<flux:input type="file" wire:model="product.eudr_document" />
<flux:description>{{ __('PDF / Link zum Statement') }}</flux:description>
</flux:field>
</flux:card>
</div>
@endif
{{-- TAB 5: KOMMERZIELL - Preise, Verfügbarkeit & Lieferung --}}
@if($activeTab === 'kommerziell')
<div class="space-y-8">
{{-- 7. Preise & Konditionen --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('7. Preise & Konditionen') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Einkaufspreis (net)') }}</flux:label>
<flux:input type="number" step="0.01" wire:model="product.purchase_price" placeholder="680.00" />
</flux:field>
<flux:field>
<flux:label>{{ __('Verkaufspreis (net)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" step="0.01" wire:model="product.selling_price" placeholder="1,250.00" />
<flux:description>{{ __('Für B2in-Plattform') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Währung') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.currency">
<option value="EUR">{{ __('EUR') }}</option>
<option value="USD">{{ __('USD') }}</option>
<option value="CHF">{{ __('CHF') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Steuersatz (%)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" wire:model="product.tax_rate" placeholder="19" />
</flux:field>
<flux:field>
<flux:label>{{ __('UVP (Brutto)') }}</flux:label>
<flux:input type="number" step="0.01" wire:model="product.rrp" placeholder="1,499.00" />
<flux:description>{{ __('Unverbindliche Preisempfehlung') }}</flux:description>
</flux:field>
</flux:card>
{{-- 8. Verfügbarkeit & Lieferzeit --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('8. Verfügbarkeit & Lieferzeit') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Lagerstatus') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.stock_status">
<option value="in_stock">{{ __('Auf Lager') }}</option>
<option value="on_order">{{ __('Auf Bestellung') }}</option>
<option value="out_of_stock">{{ __('Nicht verfügbar') }}</option>
</flux:select>
<flux:description>{{ __('Auf Lager / Auf Bestellung / Nicht verfügbar') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Lieferzeit (Wochen)') }} <span class="text-red-500">*</span></flux:label>
<flux:input type="number" wire:model="product.delivery_time" placeholder="4-6" />
<flux:description>{{ __('MinMax-Spanne') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Produktionszeit (Tage)') }}</flux:label>
<flux:input type="number" wire:model="product.production_time" placeholder="21" />
<flux:description>{{ __('falls relevant') }}</flux:description>
</flux:field>
</flux:card>
{{-- 9. Lieferung, Montage & Service --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('9. Lieferung, Montage & Service') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Lieferart') }}</flux:label>
<flux:select wire:model="product.delivery_type">
<option value="pickup">{{ __('Abholung') }}</option>
<option value="delivery">{{ __('Lieferung') }}</option>
<option value="expedition">{{ __('Spedition') }}</option>
<option value="parcel">{{ __('Paket') }}</option>
</flux:select>
<flux:description>{{ __('Abholung / Lieferung / Spedition / Paket') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Montageservice') }}</flux:label>
<flux:select wire:model="product.assembly_service">
<option value="yes">{{ __('Ja') }}</option>
<option value="no">{{ __('Nein') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Service-Radius (km)') }}</flux:label>
<flux:input type="number" wire:model="product.service_radius" placeholder="50" />
<flux:description>{{ __('Für Montageservice') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Garantie (Monate)') }}</flux:label>
<flux:input type="number" wire:model="product.warranty" placeholder="24" />
</flux:field>
</flux:card>
</div>
@endif
{{-- TAB 6: VERWALTUNG - Händler, Nachhaltigkeit, Scoring & Verwaltung --}}
@if($activeTab === 'verwaltung')
<div class="space-y-8">
{{-- 10. Händler- / Herstellerzuordnung --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('10. Händler- / Herstellerzuordnung') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Verkäufertyp') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="product.seller_type">
<option value="retailer">{{ __('Händler') }}</option>
<option value="manufacturer">{{ __('Hersteller') }}</option>
<option value="broker">{{ __('Makler') }}</option>
</flux:select>
<flux:description>{{ __('Hersteller / Makler') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Verkäufername') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.seller_name" placeholder="WohnDesign Bielefeld" />
</flux:field>
<flux:field>
<flux:label>{{ __('Verkäufer-ID') }}</flux:label>
<flux:input wire:model="product.seller_id" placeholder="SELLER_xyz" />
</flux:field>
<flux:field>
<flux:label>{{ __('Region / Hub') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="product.region" placeholder="OWL" />
<flux:description>{{ __('Logistische Zuordnung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Ort / PLZ') }}</flux:label>
<flux:input wire:model="product.location" placeholder="33602 Bielefeld" />
<flux:description>{{ __('Standort des Verkäufers/Lagers') }}</flux:description>
</flux:field>
</flux:card>
{{-- 11. Nachhaltigkeit & Umwelt --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('11. Nachhaltigkeit & Umwelt') }}</flux:heading>
<flux:field>
<flux:label>{{ __('CO₂-Fußabdruck (kg CO₂e) pro Stück') }}</flux:label>
<flux:input type="number" step="0.1" wire:model="product.co2_footprint" placeholder="35" />
</flux:field>
<flux:field>
<flux:label>{{ __('Recyclinganteil (%)') }}</flux:label>
<flux:input type="number" wire:model="product.recycling_rate" placeholder="40" />
<flux:description>{{ __('Anteil recycelter Materialien im Produkt') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Regionale Produktion') }}</flux:label>
<flux:select wire:model="product.regional_production">
<option value="yes">{{ __('Ja') }}</option>
<option value="no">{{ __('Nein') }}</option>
</flux:select>
<flux:description>{{ __('Ja / Nein (Umkreis z. B. < 500 km)') }}</flux:description>
</flux:field>
</flux:card>
{{-- 12. Scoring-System (B2in Internal) --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('12. Scoring-System (B2in Internal)') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Stauraumvolumen (L)') }}</flux:label>
<flux:input type="number" wire:model="product.storage_volume" placeholder="280" />
<flux:description>{{ __('Innenvolumen') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Aufbauaufwand (15)') }}</flux:label>
<flux:select wire:model="product.assembly_effort">
<option value="1">1 - {{ __('Sehr einfach') }}</option>
<option value="2">2 - {{ __('Einfach') }}</option>
<option value="3">3 - {{ __('Mittel') }}</option>
<option value="4">4 - {{ __('Anspruchsvoll') }}</option>
<option value="5">5 - {{ __('Sehr anspruchsvoll') }}</option>
</flux:select>
<flux:description>{{ __('gering = 1') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Designpunkte (15)') }}</flux:label>
<flux:select wire:model="product.design_score">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</flux:select>
<flux:description>{{ __('interne Bewertung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Gesamt-Score') }}</flux:label>
<flux:input type="number" step="0.1" wire:model="product.total_score" placeholder="4.2" disabled />
<flux:description>{{ __('automatisch berechnet') }}</flux:description>
</flux:field>
</flux:card>
{{-- 13. Verwaltung & Lebenszyklus --}}
<flux:card class="space-y-6">
<flux:heading size="lg" class="border-b pb-3">{{ __('13. Verwaltung & Lebenszyklus') }}</flux:heading>
<flux:field>
<flux:label>{{ __('Sichtbar ab / bis (Datum)') }}</flux:label>
<flux:input type="date" wire:model="product.visible_from" placeholder="2025-01-01 / 2026-01-01" />
<flux:description>{{ __('Steuerung der Veröffentlichung') }}</flux:description>
</flux:field>
<flux:field>
<flux:label>{{ __('Freigabe durch B2in erforderlich') }}</flux:label>
<flux:select wire:model="product.requires_approval">
<option value="yes">{{ __('Ja') }}</option>
<option value="no">{{ __('Nein') }}</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Letzte Änderung') }}</flux:label>
<flux:input type="date" wire:model="product.last_modified" value="2025-11-04" disabled />
<flux:description>{{ __('Datum der letzten Aktualisierung') }}</flux:description>
</flux:field>
</flux:card>
</div>
@endif
{{-- Submit Button (außerhalb der Tabs, immer sichtbar) --}}
<div class="flex justify-end gap-4 pt-6 border-t">
<flux:button variant="ghost" href="{{ route('dashboard') }}">{{ __('Abbrechen') }}</flux:button>
<flux:button type="submit" variant="primary" icon="check">{{ __('Produkt speichern') }}</flux:button>
</div>
</form>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,714 @@
<?php
use App\Enums\PriceType;
use App\Enums\ProductStatus;
use App\Enums\ProductType;
use App\Models\Category;
use App\Models\Product;
use App\Models\ProductVariant;
use Flux\Flux;
use Illuminate\Support\Facades\Storage;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use function Livewire\Volt\{layout, title};
layout('components.layouts.app');
title('Teaser-Produkt');
new class extends Component {
use WithFileUploads;
public ?Product $product = null;
public bool $isEditing = false;
public array $existingMedia = [];
// Produkt-Felder (Typ A Teaser / Local Stock)
public string $name = '';
public string $descriptionShort = '';
public string $priceType = 'from_price';
public string $priceDisplayText = '';
public ?int $categoryId = null;
public string $status = 'active';
public string $partnerProductNumber = '';
// Bildupload
public array $mainImages = [];
public function mount(?Product $product = null): void
{
if ($product && $product->exists) {
$this->authorize('update', $product);
$this->product = $product;
$this->isEditing = true;
// Pre-fill from product
$this->name = $product->name;
$this->descriptionShort = $product->description_short;
$this->priceType = $product->price_type->value;
$this->priceDisplayText = $product->price_display_text ?? '';
$this->categoryId = $product->categories->first()?->id;
// Produktnummer pre-fill
$this->partnerProductNumber = $product->partner_product_number ?? '';
// Map status: Pending/Correction/Active → 'active' for UI, Draft stays 'draft'
$this->status = in_array($product->status, [ProductStatus::Pending, ProductStatus::Active, ProductStatus::Correction]) ? 'active' : 'draft';
// Existing media (sorted by order_column)
$this->existingMedia = $product->media
->sortBy('order_column')
->values()
->map(
fn($m) => [
'id' => $m->id,
'file_path' => $m->file_path,
'alt_text' => $m->alt_text,
'order_column' => $m->order_column,
],
)
->toArray();
} else {
$this->authorize('create', Product::class);
abort_unless(
auth()
->user()
->hasAnyRole(['Retailer', 'Manufacturer', 'Admin', 'Super-Admin']),
403,
__('Nur Händler und Hersteller können Teaser-Produkte anlegen.'),
);
$partner = auth()->user()->partner;
if ($partner) {
$nextNumber = $partner->products()->count() + 1;
$this->partnerProductNumber = sprintf('P%03d-%04d', $partner->id, $nextNumber);
}
}
}
public function removeExistingMedia(int $mediaId): void
{
if (!$this->isEditing) {
return;
}
$media = $this->product->media()->find($mediaId);
if ($media) {
Storage::disk('public')->delete($media->file_path);
$media->delete();
$this->existingMedia = collect($this->existingMedia)->reject(fn($m) => $m['id'] === $mediaId)->values()->toArray();
}
}
public function removePhoto(int $index): void
{
if (isset($this->mainImages[$index])) {
unset($this->mainImages[$index]);
$this->mainImages = array_values($this->mainImages);
}
}
/**
* Reihenfolge der vorhandenen Bilder aktualisieren.
*
* @param array<int> $orderedIds
*/
public function updateMediaOrder(array $orderedIds): void
{
if (!$this->isEditing) {
return;
}
foreach ($orderedIds as $position => $mediaId) {
$this->product
->media()
->where('id', $mediaId)
->update([
'order_column' => $position + 1,
]);
}
// Sync local state
$reordered = collect($orderedIds)
->map(function ($id, $index) {
$media = collect($this->existingMedia)->firstWhere('id', $id);
return $media ? array_merge($media, ['order_column' => $index + 1]) : null;
})
->filter()
->values()
->toArray();
$this->existingMedia = $reordered;
}
public function archiveProduct(): void
{
if (! $this->isEditing) {
return;
}
$this->authorize('delete', $this->product);
$this->product->update(['status' => ProductStatus::Archived]);
$this->product->activities()->create([
'user_id' => auth()->id(),
'action' => 'archived',
'note' => null,
]);
session()->flash('message', __('Produkt wurde archiviert.'));
$this->redirect(route('products.index'));
}
public function markAsSold(): void
{
if (! $this->isEditing) {
return;
}
$this->authorize('update', $this->product);
$this->product->update(['status' => ProductStatus::Sold]);
$this->product->activities()->create([
'user_id' => auth()->id(),
'action' => 'sold',
'note' => null,
]);
session()->flash('message', __('Produkt wurde als verkauft markiert.'));
$this->redirect(route('products.index'));
}
public function save(): void
{
if ($this->isEditing) {
$this->authorize('update', $this->product);
} else {
$this->authorize('create', Product::class);
}
$allowedPriceTypes = collect(ProductType::LocalStock->allowedPriceTypes())
->map(fn(PriceType $pt) => $pt->value)
->implode(',');
$this->validate(
[
'name' => 'required|string|max:255',
'descriptionShort' => 'required|string|max:180',
'priceType' => "required|in:{$allowedPriceTypes}",
'priceDisplayText' => 'required_if:priceType,from_price|nullable|string|max:100',
'categoryId' => 'required|exists:categories,id',
'status' => 'required|in:active,draft',
'partnerProductNumber' => 'nullable|string|max:100',
'mainImages' => 'nullable|array|min:0|max:10',
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
],
[
'name.required' => __('Bitte geben Sie einen Produktnamen ein.'),
'descriptionShort.required' => __('Bitte geben Sie eine Kurzbeschreibung ein.'),
'descriptionShort.max' => __('Die Kurzbeschreibung darf maximal 180 Zeichen lang sein.'),
'priceType.required' => __('Bitte wählen Sie eine Preisangabe aus.'),
'priceDisplayText.required_if' => __('Bei Ab-Preis ist eine Preisangabe erforderlich (z.B. "Ab 2.500 €").'),
'categoryId.required' => __('Bitte wählen Sie eine Kategorie aus.'),
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
'mainImages.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
],
);
if ($this->isEditing) {
$this->saveExisting();
$toastMessage = $this->status === 'active' ? __('Teaser-Produkt wurde zur Freigabe eingereicht.') : __('Teaser-Produkt wurde als Entwurf gespeichert.');
Flux::toast(variant: 'success', text: $toastMessage, duration: 5000);
// Refresh existing media after save (sorted by order_column)
$this->existingMedia = $this->product
->fresh()
->media->sortBy('order_column')
->values()
->map(
fn($m) => [
'id' => $m->id,
'file_path' => $m->file_path,
'alt_text' => $m->alt_text,
'order_column' => $m->order_column,
],
)
->toArray();
$this->mainImages = [];
} else {
$this->saveNew();
$flashMessage = $this->status === 'active' ? __('Teaser-Produkt wurde zur Freigabe eingereicht.') : __('Teaser-Produkt wurde als Entwurf gespeichert.');
session()->flash('message', $flashMessage);
$this->redirect(route('products.index'));
}
}
private function saveNew(): void
{
$user = auth()->user();
$partner = $user->partner;
// Status: 'active' im UI → Pending (zur Freigabe), 'draft' → Draft
$newStatus = $this->status === 'active' ? ProductStatus::Pending : ProductStatus::Draft;
// B2in-Artikelnummer automatisch generieren
$lastNumber = Product::whereNotNull('b2in_article_number')->orderByDesc('b2in_article_number')->value('b2in_article_number');
$nextSeq = $lastNumber ? ((int) str_replace('B2IN-', '', $lastNumber)) + 1 : 1;
$b2inArticleNumber = sprintf('B2IN-%06d', $nextSeq);
$product = Product::create([
'partner_id' => $partner->id,
'partner_product_number' => $this->partnerProductNumber ?: null,
'b2in_article_number' => $b2inArticleNumber,
'hub_id' => $partner->hub_id,
'name' => $this->name,
'slug' => str($this->name)
->slug()
->append('-' . uniqid()),
'product_type' => ProductType::LocalStock,
'status' => $newStatus,
'price_type' => PriceType::from($this->priceType),
'price_display_text' => $this->priceDisplayText ?: null,
'description_short' => $this->descriptionShort,
'is_available' => true,
'is_curated' => false,
]);
// Kategorie zuordnen
$product->categories()->attach($this->categoryId);
// Bilder speichern
$index = 1;
foreach ($this->mainImages as $image) {
$path = $image->store('products/' . $product->id, 'public');
$product->media()->create([
'file_path' => $path,
'type' => 'image',
'alt_text' => $this->name,
'order_column' => $index++,
]);
}
}
private function saveExisting(): void
{
// Status logic for editing
$newStatus = ProductStatus::Draft;
if ($this->status === 'active') {
if ($this->product->status === ProductStatus::Draft) {
$newStatus = ProductStatus::Pending;
} elseif (in_array($this->product->status, [ProductStatus::Pending, ProductStatus::Active, ProductStatus::Correction])) {
$newStatus = ProductStatus::Pending;
}
}
$this->product->update([
'name' => $this->name,
'description_short' => $this->descriptionShort,
'price_type' => PriceType::from($this->priceType),
'price_display_text' => $this->priceDisplayText ?: null,
'partner_product_number' => $this->partnerProductNumber ?: null,
'status' => $newStatus,
]);
// Kategorie aktualisieren
$this->product->categories()->sync([$this->categoryId]);
// Neue Bilder speichern
$maxOrder = $this->product->media()->max('order_column') ?? 0;
$index = $maxOrder + 1;
foreach ($this->mainImages as $image) {
$path = $image->store('products/' . $this->product->id, 'public');
$this->product->media()->create([
'file_path' => $path,
'type' => 'image',
'alt_text' => $this->name,
'order_column' => $index++,
]);
}
// Activity log
$this->product->activities()->create([
'user_id' => auth()->id(),
'action' => 'updated',
'note' => null,
]);
}
public function with(): array
{
$data = [
'categories' => Category::orderBy('name')->get(['id', 'name']),
'allowedPriceTypes' => ProductType::LocalStock->allowedPriceTypes(),
'isEditing' => $this->isEditing,
];
if ($this->isEditing) {
$data['existingMedia'] = $this->existingMedia;
$data['activities'] = $this->product->activities()->with('user')->latest()->take(10)->get();
}
return $data;
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center gap-4">
<flux:button href="{{ route('products.index') }}" variant="ghost" icon="arrow-left" />
<div>
<flux:heading size="xl" class="mb-1">
{{ $isEditing ? __('Teaser-Produkt bearbeiten') : __('Neues Produkt anlegen') }}
</flux:heading>
<flux:subheading>
<flux:badge :color="$isEditing ? 'amber' : 'blue'" size="sm" icon="tag">
{{ __('Teaser-Produkt') }}</flux:badge>
{{ __('Typ A Beratungspflicht, Ticket erforderlich') }}
</flux:subheading>
</div>
</div>
{{-- Kuration-Hinweis (Korrektur oder Ablehnung) --}}
@if ($isEditing && $product->curation_notes && in_array($product->status, [ProductStatus::Correction, ProductStatus::Archived]))
<flux:callout
variant="{{ $product->status === ProductStatus::Correction ? 'warning' : 'danger' }}"
icon="{{ $product->status === ProductStatus::Correction ? 'exclamation-triangle' : 'x-circle' }}">
<flux:callout.heading>
{{ $product->status === ProductStatus::Correction ? __('Korrektur erforderlich') : __('Produkt abgelehnt') }}
</flux:callout.heading>
<flux:callout.text>{{ $product->curation_notes }}</flux:callout.text>
</flux:callout>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Bild-Upload --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ $isEditing ? __('Produktbilder') : __('Produktbild') }}</flux:heading>
<flux:subheading>{{ __('Nur Bilder (JPG, JPEG, PNG) max. 10 MB pro Bild, max. 10 Bilder') }}
</flux:subheading>
</div>
<flux:separator class="mb-6" />
{{-- Existing images (only in edit mode) - sortable via drag & drop --}}
@if ($isEditing && count($existingMedia) > 0)
<div class="mb-6">
<flux:heading size="sm" class="mb-3">{{ __('Vorhandene Bilder') }}</flux:heading>
<flux:text size="sm" class="mb-3 text-zinc-500">
{{ __('Per Drag & Drop sortieren das erste Bild ist das Standardbild.') }}</flux:text>
<div x-data="{
dragging: null,
dragOver: null,
items: @js(collect($existingMedia)->pluck('id')->toArray()),
onDragStart(e, id) {
this.dragging = id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', id);
},
onDragOver(e, id) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
this.dragOver = id;
},
onDrop(e, targetId) {
e.preventDefault();
if (this.dragging === targetId) { this.dragOver = null; return; }
const fromIdx = this.items.indexOf(this.dragging);
const toIdx = this.items.indexOf(targetId);
this.items.splice(fromIdx, 1);
this.items.splice(toIdx, 0, this.dragging);
this.dragging = null;
this.dragOver = null;
$wire.updateMediaOrder(this.items);
},
onDragEnd() {
this.dragging = null;
this.dragOver = null;
}
}" class="flex flex-wrap items-start gap-3">
@foreach ($existingMedia as $mediaIndex => $media)
<div wire:key="existing-media-{{ $media['id'] }}" draggable="true"
x-on:dragstart="onDragStart($event, {{ $media['id'] }})"
x-on:dragover="onDragOver($event, {{ $media['id'] }})"
x-on:drop="onDrop($event, {{ $media['id'] }})" x-on:dragend="onDragEnd()"
:class="{
'opacity-50 scale-95': dragging === {{ $media['id'] }},
'ring-2 ring-blue-400 ring-offset-2 dark:ring-offset-zinc-800': dragOver ===
{{ $media['id'] }} && dragging !== {{ $media['id'] }}
}"
class="group relative cursor-grab transition-all duration-150 active:cursor-grabbing">
@if ($mediaIndex === 0)
<div
class="absolute -left-1 -top-1 z-10 rounded-md bg-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow">
{{ __('Standard') }}
</div>
@endif
<img src="{{ Storage::url($media['file_path']) }}"
alt="{{ $media['alt_text'] ?? '' }}"
class="h-24 w-24 rounded-lg border border-zinc-200 object-cover dark:border-zinc-700 {{ $mediaIndex === 0 ? 'ring-2 ring-blue-500' : '' }}" />
<flux:button wire:click="removeExistingMedia({{ $media['id'] }})"
wire:confirm="{{ __('Bild wirklich löschen?') }}" variant="filled" size="xs"
icon="trash"
class="absolute -right-2 -top-2 !bg-red-500 !text-white hover:!bg-red-600" />
<div
class="absolute bottom-1 left-1/2 -translate-x-1/2 opacity-0 transition-opacity group-hover:opacity-100">
<flux:icon.arrows-up-down class="h-4 w-4 text-white drop-shadow-md" />
</div>
</div>
@endforeach
</div>
</div>
<flux:separator class="mb-6" />
@endif
{{-- Upload area --}}
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
<flux:file-upload.dropzone heading="{{ __('Bilder hochladen') }}"
text="{{ __('Nur JPEG oder PNG max. 10 MB') }}" with-progress />
</flux:file-upload>
@if (isset($mainImages) && count($mainImages) > 0)
<div class="mt-4 flex flex-wrap items-center gap-3">
@foreach ($mainImages as $index => $image)
<flux:file-item :heading="$image->getClientOriginalName()"
:image="(str_starts_with($image->getMimeType() ?? '', 'image/') && $image->isPreviewable()) ? $image->temporaryUrl() : null"
:size="$image->getSize()">
<x-slot name="actions">
<flux:file-item.remove wire:click="removePhoto({{ $index }})"
aria-label="{{ 'Remove file: ' . $image->getClientOriginalName() }}" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="mainImages" />
</flux:card>
{{-- Produktinformationen --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Produktinformationen') }}</flux:heading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Produktname') }}</flux:label>
<flux:input wire:model="name" placeholder="{{ __('z.B. Sideboard Eiche massiv') }}"
icon="tag" />
<flux:error name="name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
<flux:description>{{ __('Max. 180 Zeichen wird im Feed und auf der Karte angezeigt') }}
</flux:description>
<flux:textarea wire:model.live="descriptionShort"
placeholder="{{ __('Massivholz-Sideboard aus OWL, individuell auf Maß gefertigbar...') }}"
rows="3" maxlength="180" />
<div class="mt-1 flex justify-between text-xs text-zinc-400">
<span>{{ strlen($descriptionShort) }} / 180</span>
</div>
<flux:error name="descriptionShort" />
</flux:field>
<flux:field>
<flux:label>{{ __('Kategorie') }}</flux:label>
<flux:select wire:model="categoryId">
<flux:select.option :value="null">{{ __(' Bitte wählen ') }}</flux:select.option>
@foreach ($categories as $category)
<flux:select.option :value="$category->id">{{ $category->name }}</flux:select.option>
@endforeach
</flux:select>
<flux:error name="categoryId" />
</flux:field>
</div>
</flux:card>
{{-- Produktnummern --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Produktnummern') }}</flux:heading>
<flux:subheading>{{ __('Interne und B2in-Artikelnummer') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-4">
@if ($isEditing && $product->b2in_article_number)
<flux:field>
<flux:label>{{ __('B2in-Artikelnummer') }}</flux:label>
<flux:badge color="blue" size="sm" icon="hashtag">
{{ $product->b2in_article_number }}</flux:badge>
</flux:field>
@endif
<flux:field>
<flux:label>{{ __('Partner-Produktnummer') }}</flux:label>
<flux:description>
{{ __('Eigene interne Artikelnummer (optional, wird automatisch vorgeschlagen)') }}
</flux:description>
<flux:input wire:model="partnerProductNumber" placeholder="P001-0001" icon="hashtag" />
<flux:error name="partnerProductNumber" />
</flux:field>
</div>
</flux:card>
{{-- Preisangabe (Typ A: kein Festpreis) --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Preisangabe') }}</flux:heading>
<flux:subheading>
{{ __('Teaser-Produkte haben keine fixen Online-Preise. Wählen Sie Art der Preisangabe.') }}
</flux:subheading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Preistyp') }}</flux:label>
<flux:select wire:model.live="priceType">
@foreach ($allowedPriceTypes as $type)
<flux:select.option :value="$type->value">{{ $type->label() }}</flux:select.option>
@endforeach
</flux:select>
<flux:error name="priceType" />
</flux:field>
@if ($priceType === 'from_price')
<flux:field>
<flux:label>{{ __('Preisangabe (Freitext)') }}</flux:label>
<flux:description>{{ __('z.B. "Ab 2.500 €" oder "Ab 1.800 € pro Laufmeter"') }}
</flux:description>
<flux:input wire:model="priceDisplayText" placeholder="{{ __('Ab 2.500 €') }}"
icon="currency-euro" />
<flux:error name="priceDisplayText" />
</flux:field>
@endif
<flux:callout variant="info" icon="information-circle">
<flux:callout.heading>{{ __('Warum kein Festpreis?') }}</flux:callout.heading>
<flux:callout.text>
{{ __('Teaser-Produkte sind bewusst ohne finale Online-Konfiguration. Der Kunde kommt in Ihren Laden dort erfolgt die finale Planung und Preisfestlegung.') }}
</flux:callout.text>
</flux:callout>
</div>
</flux:card>
{{-- Status & Verfügbarkeit --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Veröffentlichung') }}</flux:heading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-4">
@if ($isEditing)
<flux:field>
<flux:label>{{ __('Aktueller Status') }}</flux:label>
<div>
<flux:badge :color="$product->status->color()" size="sm">
{{ $product->status->label() }}</flux:badge>
</div>
</flux:field>
@if ($product->status === \App\Enums\ProductStatus::Correction && $product->curation_notes)
<flux:callout variant="warning" icon="exclamation-triangle">
<flux:callout.heading>{{ __('Korrektur erforderlich') }}</flux:callout.heading>
<flux:callout.text>{{ $product->curation_notes }}</flux:callout.text>
</flux:callout>
@endif
@endif
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:select wire:model="status">
<flux:select.option value="active">
{{ __('Zur Freigabe einreichen') }}
</flux:select.option>
<flux:select.option value="draft">{{ __('Entwurf erst speichern') }}</flux:select.option>
</flux:select>
</flux:field>
<flux:callout variant="info" icon="shield-check">
<flux:callout.heading>{{ __('Freigabe-Workflow') }}</flux:callout.heading>
<flux:callout.text>
{{ __('Eingereichte Produkte werden vom Admin geprüft. Nach Freigabe wird das Produkt veröffentlicht. Bei Korrekturbedarf erhalten Sie eine Rückmeldung.') }}
</flux:callout.text>
</flux:callout>
</div>
</flux:card>
{{-- Aktivitätsprotokoll (only in edit mode) --}}
@if ($isEditing && $activities->isNotEmpty())
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg">{{ __('Aktivitätsprotokoll') }}</flux:heading>
<flux:subheading>{{ __('Letzte Änderungen an diesem Produkt') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
<div class="space-y-2">
@foreach ($activities as $activity)
<div wire:key="activity-{{ $activity->id }}"
class="flex items-center justify-between rounded-lg border border-zinc-100 px-4 py-2 text-sm dark:border-zinc-700">
<div class="flex items-center gap-2">
<flux:icon.user-circle class="h-4 w-4 text-zinc-400" />
<span
class="font-medium text-zinc-700 dark:text-zinc-300">{{ $activity->user?->name ?? __('System') }}</span>
<span class="text-zinc-500 dark:text-zinc-400">{{ $activity->action }}</span>
@if ($activity->note)
<span class="text-zinc-400"> {{ $activity->note }}</span>
@endif
</div>
<span
class="text-xs text-zinc-400">{{ $activity->created_at->format('d.m.Y H:i') }}</span>
</div>
@endforeach
</div>
</flux:card>
@endif
{{-- Aktionen --}}
<div class="flex items-center justify-between">
@if ($isEditing)
<div class="flex gap-2">
<flux:button wire:click="markAsSold" wire:confirm="{{ __('Produkt wirklich als verkauft markieren?') }}"
variant="filled" size="sm" icon="check-badge">
{{ __('Als verkauft markieren') }}
</flux:button>
<flux:button wire:click="archiveProduct" wire:confirm="{{ __('Produkt wirklich archivieren? Diese Aktion kann nicht rückgängig gemacht werden.') }}"
variant="danger" size="sm" icon="archive-box">
{{ __('Archivieren') }}
</flux:button>
</div>
@else
<div></div>
@endif
<div class="flex gap-3">
<flux:button href="{{ route('products.index') }}" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" :icon="$isEditing ? 'check' : 'plus'"
wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">
{{ $isEditing ? __('Änderungen speichern') : __('Produkt anlegen') }}
</span>
<span wire:loading wire:target="save">
<flux:icon.arrow-path class="animate-spin inline-block mr-1 h-4 w-4" />
{{ __('Wird gespeichert...') }}
</span>
</flux:button>
</div>
</div>
</form>
</div>

View file

@ -1,398 +1,344 @@
<?php
use function Livewire\Volt\{state, mount, computed};
use App\Enums\ProductStatus;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use function Livewire\Volt\{layout, title};
state([
'search' => '',
'statusFilter' => 'all',
'categoryFilter' => 'all',
'sortBy' => 'created_at',
'sortDirection' => 'desc',
]);
layout('components.layouts.app');
title('Produkte');
// Dummy-Produkte für die Anzeige
$products = computed(function () {
return [
[
'id' => 1,
'b2in_number' => 'B2IN-000471',
'supplier_number' => 'SOFA-ALBA-3S-ANTHR',
'name' => 'Sofa ALBA 3-Sitzer',
'brand' => 'Möbelwerk Nord',
'category' => 'Wohnzimmer > Sofas',
'status' => 'active',
'price' => 1250.00,
'stock_status' => 'in_stock',
'created_at' => '2025-11-04',
'image' => null,
],
[
'id' => 2,
'b2in_number' => 'B2IN-000472',
'supplier_number' => 'CHAIR-NORDIC-OAK',
'name' => 'Stuhl Nordic Eiche',
'brand' => 'Design Studio',
'category' => 'Esszimmer > Stühle',
'status' => 'active',
'price' => 189.00,
'stock_status' => 'in_stock',
'created_at' => '2025-11-05',
'image' => null,
],
[
'id' => 3,
'b2in_number' => 'B2IN-000473',
'supplier_number' => 'BED-LUNA-180',
'name' => 'Bett Luna 180x200',
'brand' => 'Schlafwelt',
'category' => 'Schlafzimmer > Betten',
'status' => 'draft',
'price' => 899.00,
'stock_status' => 'on_order',
'created_at' => '2025-11-03',
'image' => null,
],
[
'id' => 4,
'b2in_number' => 'B2IN-000474',
'supplier_number' => 'TABLE-OAK-EXTEND',
'name' => 'Esstisch Eiche ausziehbar',
'brand' => 'Tischmanufaktur',
'category' => 'Esszimmer > Tische',
'status' => 'active',
'price' => 1450.00,
'stock_status' => 'in_stock',
'created_at' => '2025-10-28',
'image' => null,
],
[
'id' => 5,
'b2in_number' => 'B2IN-000475',
'supplier_number' => 'WARDROBE-CLASSIC',
'name' => 'Kleiderschrank Classic',
'brand' => 'Möbelwerk Nord',
'category' => 'Schlafzimmer > Schränke',
'status' => 'inactive',
'price' => 2100.00,
'stock_status' => 'out_of_stock',
'created_at' => '2025-10-15',
'image' => null,
],
[
'id' => 6,
'b2in_number' => 'B2IN-000476',
'supplier_number' => 'SIDEBOARD-MOD-120',
'name' => 'Sideboard Modern 120cm',
'brand' => 'Design Studio',
'category' => 'Wohnzimmer > Sideboards',
'status' => 'active',
'price' => 675.00,
'stock_status' => 'in_stock',
'created_at' => '2025-11-01',
'image' => null,
],
];
});
new class extends Component {
use WithPagination;
?>
public string $search = '';
public string $statusFilter = '';
public string $categoryFilter = '';
public string $productTypeFilter = '';
public string $sortBy = 'created_at';
public string $sortDirection = 'desc';
<div class="space-y-6">
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function updatedCategoryFilter(): void
{
$this->resetPage();
}
public function updatedProductTypeFilter(): void
{
$this->resetPage();
}
public function archiveProduct(int $productId): void
{
$product = Product::findOrFail($productId);
$this->authorize('delete', $product);
$product->update(['status' => ProductStatus::Archived]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'archived',
'note' => null,
]);
session()->flash('message', __('Produkt ":name" wurde archiviert.', ['name' => $product->name]));
}
public function markAsSold(int $productId): void
{
$product = Product::findOrFail($productId);
$this->authorize('update', $product);
$product->update(['status' => ProductStatus::Sold]);
$product->activities()->create([
'user_id' => auth()->id(),
'action' => 'sold',
'note' => null,
]);
session()->flash('message', __('Produkt ":name" wurde als verkauft markiert.', ['name' => $product->name]));
}
public function with(): array
{
$user = Auth::user();
$isAdmin = $user->hasAnyRole(['Admin', 'Super-Admin']);
$isCustomer = $user->hasRole('Customer');
$query = Product::query()
->with(['brand', 'categories', 'partner', 'media'])
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
->when($this->statusFilter, fn($q) => $q->where('status', $this->statusFilter))
->when($this->categoryFilter, fn($q) => $q->whereHas('categories', fn($q) => $q->where('categories.id', $this->categoryFilter)))
->when($this->productTypeFilter, fn($q) => $q->where('product_type', $this->productTypeFilter));
if ($isAdmin) {
// Admin sieht alle Produkte
} elseif ($isCustomer) {
// Kunden sehen nur freigegebene, aktive Produkte aus ihrem Hub
$query->where('status', ProductStatus::Active)->where('is_curated', true)->where('is_available', true)->when($user->hub_id, fn($q) => $q->where('hub_id', $user->hub_id));
} else {
// Händler/Hersteller sehen nur eigene Produkte
$query->when($user->partner_id, fn($q) => $q->where('partner_id', $user->partner_id));
}
$products = $query->orderBy($this->sortBy, $this->sortDirection)->paginate(20);
$categories = Category::orderBy('name')->get();
return [
'products' => $products,
'categories' => $categories,
'isAdmin' => $isAdmin,
'isCustomer' => $isCustomer,
];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Produkte') }} (in Entwicklung)</flux:heading>
<flux:subheading>{{ __('Verwalten Sie Ihre Produktliste') }}</flux:subheading>
<flux:heading size="xl">{{ __('Produkte') }}</flux:heading>
<flux:subheading>{{ __('Produktübersicht und Local Feed') }}</flux:subheading>
</div>
<flux:button variant="primary" icon="plus" :href="route('products.create')" wire:navigate>
{{ __('Neues Produkt') }}
</flux:button>
@if (
!$isCustomer &&
auth()->user()->hasAnyRole(['Retailer', 'Manufacturer', 'Admin', 'Super-Admin']))
<div class="flex items-center gap-2">
<flux:button variant="primary" icon="plus" href="{{ route('products.create.teaser') }}">
{{ __('Neues Teaser-Produkt') }}
</flux:button>
<flux:button variant="primary" icon="plus" href="{{ route('products.create.standard') }}">
{{ __('Neues Standard-Produkt') }}
</flux:button>
</div>
@endif
</div>
@if (session()->has('message'))
<flux:callout variant="success" icon="check-circle">
{{ session('message') }}
</flux:callout>
@endif
{{-- Filter & Suche --}}
<flux:card class="p-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
{{-- Suchfeld --}}
<div class="md:col-span-2">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Suche nach Name, Artikelnummer...') }}"
icon="magnifying-glass"
/>
</div>
<flux:card class="shadow-elegant">
<div class="flex flex-wrap items-end gap-4">
<flux:field class="flex-1">
<flux:label>{{ __('Suche') }}</flux:label>
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Name, Artikelnummer...') }}"
icon="magnifying-glass" />
</flux:field>
{{-- Status Filter --}}
<flux:select wire:model.live="statusFilter">
<option value="all">{{ __('Alle Status') }}</option>
<option value="active">{{ __('Aktiv') }}</option>
<option value="draft">{{ __('Entwurf') }}</option>
<option value="inactive">{{ __('Inaktiv') }}</option>
</flux:select>
@if (!$isCustomer)
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:select wire:model.live="statusFilter">
<flux:select.option value="">{{ __('Alle Status') }}</flux:select.option>
<flux:select.option value="pending">{{ __('In Prüfung') }}</flux:select.option>
<flux:select.option value="correction">{{ __('Korrektur nötig') }}</flux:select.option>
<flux:select.option value="active">{{ __('Freigegeben') }}</flux:select.option>
<flux:select.option value="draft">{{ __('Entwurf') }}</flux:select.option>
<flux:select.option value="archived">{{ __('Archiviert') }}</flux:select.option>
<flux:select.option value="sold">{{ __('Verkauft') }}</flux:select.option>
</flux:select>
</flux:field>
@endif
{{-- Kategorie Filter --}}
<flux:select wire:model.live="categoryFilter">
<option value="all">{{ __('Alle Kategorien') }}</option>
<option value="sofas">{{ __('Sofas') }}</option>
<option value="chairs">{{ __('Stühle') }}</option>
<option value="tables">{{ __('Tische') }}</option>
<option value="beds">{{ __('Betten') }}</option>
<option value="wardrobes">{{ __('Schränke') }}</option>
</flux:select>
<flux:field>
<flux:label>{{ __('Produkttyp') }}</flux:label>
<flux:select wire:model.live="productTypeFilter">
<flux:select.option value="">{{ __('Alle Typen') }}</flux:select.option>
<flux:select.option value="local_stock">{{ __('Teaser (Local Express)') }}</flux:select.option>
<flux:select.option value="smart_order">{{ __('Standard (Smart Club)') }}</flux:select.option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Kategorie') }}</flux:label>
<flux:select wire:model.live="categoryFilter">
<flux:select.option value="">{{ __('Alle Kategorien') }}</flux:select.option>
@foreach ($categories as $category)
<flux:select.option :value="$category->id">{{ $category->name }}</flux:select.option>
@endforeach
</flux:select>
</flux:field>
</div>
{{-- Aktive Filter Anzeige --}}
@if($search || $statusFilter !== 'all' || $categoryFilter !== 'all')
<div class="flex items-center gap-2 mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-700">
<span class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Aktive Filter:') }}</span>
@if($search)
<flux:badge color="blue">
{{ __('Suche: ') }}{{ $search }}
<button wire:click="$set('search', '')" class="ml-1">×</button>
</flux:badge>
@endif
@if($statusFilter !== 'all')
<flux:badge color="green">
{{ __('Status: ') }}{{ __($statusFilter) }}
<button wire:click="$set('statusFilter', 'all')" class="ml-1">×</button>
</flux:badge>
@endif
@if($categoryFilter !== 'all')
<flux:badge color="purple">
{{ __('Kategorie: ') }}{{ __($categoryFilter) }}
<button wire:click="$set('categoryFilter', 'all')" class="ml-1">×</button>
</flux:badge>
@endif
<button
wire:click="$set('search', ''); $set('statusFilter', 'all'); $set('categoryFilter', 'all')"
class="text-sm text-accent-600 hover:text-accent-700 dark:text-accent-400 ml-2"
>
{{ __('Alle Filter zurücksetzen') }}
</button>
</div>
@endif
</flux:card>
{{-- Produkttabelle --}}
<flux:card>
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-20">{{ __('Bild') }}</flux:table.column>
<flux:table.column>{{ __('Produkt') }}</flux:table.column>
<flux:table.column>{{ __('Marke') }}</flux:table.column>
<flux:table.column>{{ __('Kategorie') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Preis') }}</flux:table.column>
<flux:table.column class="text-center">{{ __('Status') }}</flux:table.column>
<flux:table.column class="text-center">{{ __('Lager') }}</flux:table.column>
@if ($isAdmin)
<flux:table.column>{{ __('Händler') }}</flux:table.column>
@endif
@if ($isCustomer)
<flux:table.column>{{ __('Händler') }}</flux:table.column>
@else
<flux:table.column>{{ __('Kategorie') }}</flux:table.column>
@endif
<flux:table.column>{{ __('Preis') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
@if ($isAdmin)
<flux:table.column>{{ __('Kuration') }}</flux:table.column>
@endif
<flux:table.column>{{ __('Erstellt') }}</flux:table.column>
<flux:table.column class="text-right w-32">{{ __('Aktionen') }}</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($this->products as $product)
<flux:table.row :key="$product['id']" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
{{-- Bild --}}
<flux:table.cell>
<div class="w-12 h-12 bg-zinc-200 dark:bg-zinc-700 rounded flex items-center justify-center">
<flux:icon.photo class="w-6 h-6 text-zinc-400" />
</div>
</flux:table.cell>
{{-- Produkt --}}
<flux:table.cell>
<div>
<div class="font-semibold text-zinc-900 dark:text-zinc-100">
{{ $product['name'] }}
@forelse($products as $product)
<flux:table.row wire:key="product-{{ $product->id }}">
{{-- Produkt --}}
<flux:table.cell>
<div class="flex items-center gap-3">
@php
$thumbnail = $product->media->sortBy('order_column')->first();
@endphp
@if ($thumbnail)
<img src="{{ Storage::url($thumbnail->file_path) }}"
alt="{{ $thumbnail->alt_text ?? $product->name }}"
class="h-14 w-14 shrink-0 rounded-lg border border-zinc-200 object-cover dark:border-zinc-700" />
@else
<div
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
<flux:icon.photo class="h-5 w-5 text-zinc-400" />
</div>
@endif
<div>
<div class="font-medium text-zinc-900 dark:text-white">
{{ $product->name }}
</div>
<flux:badge size="sm"
color="{{ $product->product_type?->value === 'local_stock' ? 'amber' : 'blue' }}"
class="mt-1">
{{ $product->product_type?->label() ?? '' }}
</flux:badge>
</div>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400 space-y-0.5 mt-1">
<div>B2in: {{ $product['b2in_number'] }}</div>
<div>Art.-Nr.: {{ $product['supplier_number'] }}</div>
</div>
</div>
</flux:table.cell>
</flux:table.cell>
{{-- Marke --}}
<flux:table.cell>
<span class="text-sm text-zinc-700 dark:text-zinc-300">
{{ $product['brand'] }}
</span>
</flux:table.cell>
{{-- Händler (nur Admin) --}}
@if ($isAdmin)
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ $product->partner?->company_name ?? '' }}
</span>
</flux:table.cell>
@endif
{{-- Kategorie --}}
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ $product['category'] }}
</span>
</flux:table.cell>
{{-- Händler (Customer) oder Kategorie (Partner) --}}
@if ($isCustomer)
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ $product->partner?->company_name ?? '' }}
</span>
</flux:table.cell>
@else
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ $product->categories->first()?->name ?? '' }}
</span>
</flux:table.cell>
@endif
{{-- Preis --}}
<flux:table.cell class="text-right">
<span class="font-semibold text-zinc-900 dark:text-zinc-100">
{{ number_format($product['price'], 2, ',', '.') }}
</span>
</flux:table.cell>
{{-- Preis --}}
<flux:table.cell>
@if ($product->price_type?->value === 'on_request')
<span class="text-sm text-zinc-500">{{ __('Auf Anfrage') }}</span>
@elseif($product->price_display_text)
<span class="text-sm font-medium">{{ $product->price_display_text }}</span>
@elseif($product->price)
<span class="font-semibold">{{ number_format($product->price, 2, ',', '.') }} </span>
@else
<span class="text-zinc-400"></span>
@endif
</flux:table.cell>
{{-- Status --}}
<flux:table.cell class="text-center">
@php
$statusColors = [
'active' => 'green',
'draft' => 'yellow',
'inactive' => 'zinc',
];
$statusLabels = [
'active' => __('Aktiv'),
'draft' => __('Entwurf'),
'inactive' => __('Inaktiv'),
];
@endphp
<flux:badge :color="$statusColors[$product['status']]" size="sm">
{{ $statusLabels[$product['status']] }}
</flux:badge>
</flux:table.cell>
{{-- Status --}}
<flux:table.cell>
<flux:badge size="sm" color="{{ $product->status?->color() ?? 'zinc' }}">
{{ $product->status?->label() ?? '' }}
</flux:badge>
</flux:table.cell>
{{-- Lagerstatus --}}
<flux:table.cell class="text-center">
@php
$stockColors = [
'in_stock' => 'green',
'on_order' => 'yellow',
'out_of_stock' => 'red',
];
$stockLabels = [
'in_stock' => __('Auf Lager'),
'on_order' => __('Bestellung'),
'out_of_stock' => __('Nicht verfügbar'),
];
@endphp
<flux:badge :color="$stockColors[$product['stock_status']]" size="sm" variant="outline">
{{ $stockLabels[$product['stock_status']] }}
</flux:badge>
</flux:table.cell>
{{-- Kuration (nur Admin) --}}
@if ($isAdmin)
<flux:table.cell>
@if ($product->is_curated)
<flux:badge size="sm" color="green">{{ __('Freigegeben') }}</flux:badge>
@else
<flux:badge size="sm" color="amber">{{ __('Ausstehend') }}</flux:badge>
@endif
</flux:table.cell>
@endif
{{-- Erstellt am --}}
<flux:table.cell>
<span class="text-sm text-zinc-600 dark:text-zinc-400">
{{ \Carbon\Carbon::parse($product['created_at'])->format('d.m.Y') }}
</span>
</flux:table.cell>
{{-- Erstellt --}}
<flux:table.cell>
<span class="text-sm text-zinc-500">
{{ $product->created_at?->format('d.m.Y') }}
</span>
</flux:table.cell>
{{-- Aktionen --}}
<flux:table.cell class="text-right">
<div class="flex items-center justify-end gap-2">
<flux:button variant="ghost" size="sm" icon="eye">
{{ __('Ansehen') }}
</flux:button>
<flux:dropdown position="bottom" align="end">
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" icon-trailing />
<flux:menu class="w-48">
<flux:menu.item icon="pencil">
{{ __('Bearbeiten') }}
</flux:menu.item>
<flux:menu.item icon="document-duplicate">
{{ __('Duplizieren') }}
</flux:menu.item>
<flux:menu.separator />
<flux:menu.item icon="archive-box">
{{ __('Archivieren') }}
</flux:menu.item>
<flux:menu.item icon="trash" variant="danger">
{{ __('Löschen') }}
</flux:menu.item>
</flux:menu>
</flux:dropdown>
</div>
</flux:table.cell>
</flux:table.row>
{{-- Aktionen --}}
<flux:table.cell>
@if (!$isCustomer)
<div class="flex items-center gap-1">
<flux:button
href="{{ $product->product_type === \App\Enums\ProductType::LocalStock ? route('products.edit.teaser', $product) : route('products.edit.standard', $product) }}"
size="sm" variant="ghost" icon="pencil" />
@if (!in_array($product->status, [ProductStatus::Archived, ProductStatus::Sold]))
<flux:dropdown>
<flux:button size="sm" variant="ghost" icon="ellipsis-vertical" />
<flux:menu>
<flux:menu.item wire:click="markAsSold({{ $product->id }})"
wire:confirm="{{ __('Produkt wirklich als verkauft markieren?') }}"
icon="check-badge">
{{ __('Als verkauft') }}
</flux:menu.item>
<flux:menu.item wire:click="archiveProduct({{ $product->id }})"
wire:confirm="{{ __('Produkt wirklich archivieren?') }}"
icon="archive-box" variant="danger">
{{ __('Archivieren') }}
</flux:menu.item>
</flux:menu>
</flux:dropdown>
@endif
</div>
@endif
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="9" class="text-center py-12">
<div class="flex flex-col items-center justify-center">
<flux:icon.cube class="w-16 h-16 text-zinc-400 mb-4" />
<flux:heading size="lg" class="mb-2">{{ __('Keine Produkte gefunden') }}</flux:heading>
<flux:subheading class="mb-4">
{{ __('Erstellen Sie Ihr erstes Produkt oder passen Sie Ihre Filter an.') }}
</flux:subheading>
<flux:button variant="primary" icon="plus" :href="route('products.create')" wire:navigate>
{{ __('Neues Produkt erstellen') }}
</flux:button>
</div>
</flux:table.cell>
</flux:table.row>
<flux:table.row>
<flux:table.cell colspan="8" class="py-12 text-center text-zinc-500">
<flux:icon.shopping-bag class="mx-auto mb-2 h-12 w-12 text-zinc-400" />
<div>{{ __('Keine Produkte gefunden') }}</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
{{-- Pagination / Stats --}}
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
<div class="flex items-center justify-between">
<div class="text-sm text-zinc-600 dark:text-zinc-400">
{{ __('Zeige') }} <span class="font-semibold">{{ count($this->products) }}</span> {{ __('von') }} <span class="font-semibold">{{ count($this->products) }}</span> {{ __('Produkten') }}
</div>
{{-- Hier würde normalerweise die Pagination kommen --}}
<div class="flex items-center gap-2">
<flux:button variant="ghost" size="sm" icon="chevron-left" disabled>
{{ __('Zurück') }}
</flux:button>
<span class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Seite 1 von 1') }}</span>
<flux:button variant="ghost" size="sm" icon="chevron-right" icon-trailing disabled>
{{ __('Weiter') }}
</flux:button>
</div>
@if ($products->hasPages())
<div class="mt-4">
{{ $products->links() }}
</div>
</div>
@endif
</flux:card>
{{-- Statistiken (Optional) --}}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<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">4</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Aktive Produkte') }}</div>
</div>
</div>
</flux:card>
<flux:card class="p-4">
<div class="flex items-center gap-3">
<div class="p-3 bg-yellow-100 dark:bg-yellow-900/20 rounded-lg">
<flux:icon.document class="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">1</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Entwürfe') }}</div>
</div>
</div>
</flux:card>
<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.cube 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">5</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Auf Lager') }}</div>
</div>
</div>
</flux:card>
<flux:card class="p-4">
<div class="flex items-center gap-3">
<div class="p-3 bg-zinc-100 dark:bg-zinc-700 rounded-lg">
<flux:icon.currency-euro class="w-6 h-6 text-zinc-600 dark:text-zinc-400" />
</div>
<div>
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">6.563 </div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Ø Preis') }}</div>
</div>
</div>
</flux:card>
</div>
</div>