b2in/resources/views/livewire/products/index.blade.php
2026-02-20 17:57:50 +01:00

344 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
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};
layout('components.layouts.app');
title('Produkte');
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';
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') }}</flux:heading>
<flux:subheading>{{ __('Produktübersicht und Local Feed') }}</flux:subheading>
</div>
@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="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>
@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
<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>
</flux:card>
{{-- Produkttabelle --}}
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Produkt') }}</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></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-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>
</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
{{-- 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>
@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>
<flux:badge size="sm" color="{{ $product->status?->color() ?? 'zinc' }}">
{{ $product->status?->label() ?? '' }}
</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 --}}
<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>
@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="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>
@if ($products->hasPages())
<div class="mt-4">
{{ $products->links() }}
</div>
@endif
</flux:card>
</div>