Initial commit: WitShop Laravel Online-Shop

- 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>
This commit is contained in:
2026-06-20 00:34:09 +02:00
co-authored by Claude Sonnet 4.6
commit 41078fdc08
208 changed files with 21725 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Services;
use App\Models\ProductVariant;
use Illuminate\Support\Facades\Session;
class CartService
{
private const SESSION_KEY = 'cart';
public function get(): array
{
return Session::get(self::SESSION_KEY, []);
}
public function add(int $variantId, int $qty = 1): void
{
$cart = $this->get();
$key = "v{$variantId}";
if (isset($cart[$key])) {
$cart[$key]['qty'] += $qty;
} else {
$variant = ProductVariant::with('product')->findOrFail($variantId);
$cart[$key] = [
'variant_id' => $variantId,
'product_name' => $variant->product->name,
'variant_label'=> $variant->label,
'price' => $variant->effective_price,
'tax_rate' => $variant->product->tax_rate,
'qty' => $qty,
];
}
Session::put(self::SESSION_KEY, $cart);
}
public function update(string $key, int $qty): void
{
$cart = $this->get();
if (isset($cart[$key])) {
if ($qty <= 0) {
unset($cart[$key]);
} else {
$cart[$key]['qty'] = $qty;
}
}
Session::put(self::SESSION_KEY, $cart);
}
public function remove(string $key): void
{
$cart = $this->get();
unset($cart[$key]);
Session::put(self::SESSION_KEY, $cart);
}
public function clear(): void
{
Session::forget(self::SESSION_KEY);
}
public function subtotal(): float
{
return array_sum(array_map(fn($i) => $i['price'] * $i['qty'], $this->get()));
}
public function count(): int
{
return array_sum(array_column($this->get(), 'qty'));
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Services;
use App\Models\Order;
use Barryvdh\DomPDF\Facade\Pdf;
class InvoiceService
{
public function generate(Order $order): \Barryvdh\DomPDF\PDF
{
$order->load('items', 'addresses');
return Pdf::loadView('pdf.invoice', compact('order'))
->setPaper('a4', 'portrait');
}
public function filename(Order $order): string
{
return 'Rechnung-' . $order->order_number . '.pdf';
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace App\Services;
use App\Mail\OrderConfirmationMail;
use App\Mail\OrderShippedMail;
use App\Models\Order;
use App\Models\OrderAddress;
use App\Models\OrderItem;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use Illuminate\Support\Facades\Mail;
class OrderService
{
public function __construct(private CartService $cart) {}
public function createFromSession(array $addressData, string $paymentMethod): Order
{
$items = $this->cart->get();
$subtotal = $this->cart->subtotal();
$shippingRate = ShippingRate::forCart($subtotal);
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
$taxTotal = array_sum(array_map(function ($item) {
$net = $item['price'] / (1 + $item['tax_rate'] / 100);
return round(($item['price'] - $net) * $item['qty'], 2);
}, $items));
$order = Order::create([
'order_number' => Order::generateOrderNumber(),
'user_id' => auth()->id(),
'guest_email' => auth()->check() ? null : ($addressData['email'] ?? null),
'status' => 'new',
'payment_method' => $paymentMethod,
'subtotal' => $subtotal,
'tax_total' => $taxTotal,
'shipping_cost' => $shippingCost,
'total' => $subtotal + $shippingCost,
]);
foreach ($items as $item) {
OrderItem::create([
'order_id' => $order->id,
'product_variant_id'=> $item['variant_id'],
'product_name' => $item['product_name'],
'variant_label' => $item['variant_label'],
'quantity' => $item['qty'],
'unit_price' => $item['price'],
'tax_rate' => $item['tax_rate'],
]);
// Bestand reduzieren — nur wenn track_stock aktiv
$variant = ProductVariant::with('product')->find($item['variant_id']);
if ($variant && $variant->product->track_stock) {
$variant->decrement('stock', $item['qty']);
}
}
foreach (['billing', 'shipping'] as $type) {
$src = $addressData[$type] ?? $addressData['billing'];
OrderAddress::create(array_merge($src, ['order_id' => $order->id, 'type' => $type]));
}
$this->cart->clear();
Mail::to($order->guest_email ?? $order->user?->email)->send(new OrderConfirmationMail($order));
return $order;
}
public function updateStatus(Order $order, string $newStatus, ?string $trackingCode = null): void
{
$order->status = $newStatus;
if ($newStatus === 'payment_received' && !$order->paid_at) {
$order->paid_at = now();
}
if ($trackingCode) {
$order->tracking_code = $trackingCode;
}
$order->save();
if ($newStatus === 'shipped') {
$email = $order->guest_email ?? $order->user?->email;
if ($email) {
Mail::to($email)->send(new OrderShippedMail($order));
}
}
}
}