- Laravel 11, Blade, Alpine.js, Tailwind CSS - Produkt- & Kategorieverwaltung mit Varianten - Checkout (Vorkasse + Rechnung), Session-Warenkorb - Admin-Backend (Dashboard, Bestellungen, Kunden, CMS) - DSGVO-konform: Cookie-Consent, Impressum, Widerruf, AGB - PDF-Rechnung via dompdf, SMTP-Mailversand - Kundenverwaltung mit manueller Freischaltung Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
6.4 KiB
PHP
178 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Attribute;
|
|
use App\Models\AttributeValue;
|
|
use App\Models\Category;
|
|
use App\Models\Product;
|
|
use App\Models\ProductImage;
|
|
use App\Models\ProductVariant;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ProductController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$query = Product::with('category')->latest();
|
|
if ($request->filled('q')) {
|
|
$query->where('name', 'like', '%'.$request->q.'%');
|
|
}
|
|
$products = $query->paginate(25);
|
|
return view('admin.products.index', compact('products'));
|
|
}
|
|
|
|
public function create()
|
|
{
|
|
$categories = Category::orderBy('name')->get();
|
|
$attributes = Attribute::with('values')->get();
|
|
$variantsJson = '[]';
|
|
return view('admin.products.form', compact('categories', 'attributes', 'variantsJson'));
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$data = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'category_id' => 'nullable|exists:categories,id',
|
|
'description' => 'nullable|string',
|
|
'base_price' => 'required|numeric|min:0',
|
|
'tax_rate' => 'required|numeric|in:7,19',
|
|
'weight' => 'nullable|numeric|min:0',
|
|
'active' => 'boolean',
|
|
'featured' => 'boolean',
|
|
'track_stock' => 'boolean',
|
|
'sort_order' => 'integer',
|
|
]);
|
|
$data['slug'] = Str::slug($data['name']);
|
|
$data['active'] = $request->boolean('active');
|
|
$data['featured'] = $request->boolean('featured');
|
|
$data['track_stock'] = $request->boolean('track_stock');
|
|
|
|
$product = Product::create($data);
|
|
|
|
$this->syncImages($request, $product);
|
|
$this->syncVariants($request, $product);
|
|
|
|
return redirect()->route('admin.products.index')->with('success', 'Artikel erstellt.');
|
|
}
|
|
|
|
public function edit(Product $product)
|
|
{
|
|
$product->load('images', 'variants.attributeValues.attribute');
|
|
$categories = Category::orderBy('name')->get();
|
|
$attributes = Attribute::with('values')->get();
|
|
$variantsJson = json_encode($product->variants->map(fn($v) => [
|
|
'id' => $v->id,
|
|
'sku' => $v->sku,
|
|
'price_override' => $v->price_override,
|
|
'stock' => $v->stock,
|
|
'stock_warning' => $v->stock_warning,
|
|
'active' => (bool) $v->active,
|
|
'attribute_value_ids' => $v->attributeValues->pluck('id')->values()->all(),
|
|
]));
|
|
return view('admin.products.form', compact('product', 'categories', 'attributes', 'variantsJson'));
|
|
}
|
|
|
|
public function update(Request $request, Product $product)
|
|
{
|
|
$data = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'category_id' => 'nullable|exists:categories,id',
|
|
'description' => 'nullable|string',
|
|
'base_price' => 'required|numeric|min:0',
|
|
'tax_rate' => 'required|numeric|in:7,19',
|
|
'weight' => 'nullable|numeric|min:0',
|
|
'active' => 'boolean',
|
|
'featured' => 'boolean',
|
|
'track_stock' => 'boolean',
|
|
'sort_order' => 'integer',
|
|
]);
|
|
$data['slug'] = Str::slug($data['name']);
|
|
$data['active'] = $request->boolean('active');
|
|
$data['featured'] = $request->boolean('featured');
|
|
$data['track_stock'] = $request->boolean('track_stock');
|
|
|
|
$product->update($data);
|
|
|
|
$this->syncImages($request, $product);
|
|
$this->syncVariants($request, $product);
|
|
|
|
return redirect()->route('admin.products.index')->with('success', 'Artikel aktualisiert.');
|
|
}
|
|
|
|
public function destroy(Product $product)
|
|
{
|
|
$product->delete();
|
|
return redirect()->route('admin.products.index')->with('success', 'Artikel gelöscht.');
|
|
}
|
|
|
|
private function syncImages(Request $request, Product $product): void
|
|
{
|
|
if ($request->hasFile('images')) {
|
|
foreach ($request->file('images') as $i => $file) {
|
|
$path = $file->store('products', 'public');
|
|
ProductImage::create(['product_id' => $product->id, 'path' => $path, 'sort_order' => $i]);
|
|
}
|
|
}
|
|
// Bild löschen
|
|
if ($request->filled('delete_images')) {
|
|
ProductImage::whereIn('id', $request->delete_images)
|
|
->where('product_id', $product->id)
|
|
->delete();
|
|
}
|
|
}
|
|
|
|
private function syncVariants(Request $request, Product $product): void
|
|
{
|
|
$variants = $request->input('variants', []);
|
|
$existingIds = [];
|
|
|
|
foreach ($variants as $v) {
|
|
if (empty($v['sku'])) continue;
|
|
|
|
$variant = isset($v['id'])
|
|
? ProductVariant::find($v['id'])
|
|
: null;
|
|
|
|
$variantData = [
|
|
'product_id' => $product->id,
|
|
'sku' => $v['sku'],
|
|
'price_override' => filled($v['price_override']) ? $v['price_override'] : null,
|
|
'stock' => (int) ($v['stock'] ?? 0),
|
|
'stock_warning' => (int) ($v['stock_warning'] ?? 5),
|
|
'active' => isset($v['active']),
|
|
];
|
|
|
|
if ($variant) {
|
|
$variant->update($variantData);
|
|
} else {
|
|
$variant = ProductVariant::create($variantData);
|
|
}
|
|
|
|
// Attributwerte synchronisieren
|
|
$valueIds = array_filter((array) ($v['attribute_value_ids'] ?? []));
|
|
$variant->attributeValues()->sync($valueIds);
|
|
|
|
$existingIds[] = $variant->id;
|
|
}
|
|
|
|
// Nicht mehr vorhandene Varianten löschen
|
|
$product->variants()->whereNotIn('id', $existingIds)->delete();
|
|
|
|
// Sicherstellen dass jedes Produkt mindestens eine Standard-Variante hat
|
|
if ($product->variants()->count() === 0) {
|
|
ProductVariant::create([
|
|
'product_id' => $product->id,
|
|
'sku' => 'DEFAULT-' . $product->id,
|
|
'price_override'=> null,
|
|
'stock' => 0,
|
|
'stock_warning' => 5,
|
|
'active' => true,
|
|
]);
|
|
}
|
|
}
|
|
}
|