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
@@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\Address;
use App\Models\Order;
use Illuminate\Http\Request;
class AccountController extends Controller
{
public function index()
{
$orders = auth()->user()->orders()->latest()->take(5)->get();
return view('shop.account.index', compact('orders'));
}
public function orders()
{
$orders = auth()->user()->orders()->latest()->paginate(10);
return view('shop.account.orders', compact('orders'));
}
public function orderDetail(Order $order)
{
abort_unless($order->user_id === auth()->id(), 403);
$order->load('items', 'addresses');
return view('shop.account.order', compact('order'));
}
public function addresses()
{
$addresses = auth()->user()->addresses()->get();
return view('shop.account.addresses', compact('addresses'));
}
public function storeAddress(Request $request)
{
$data = $request->validate([
'first_name' => 'required|string|max:100',
'last_name' => 'required|string|max:100',
'company' => 'nullable|string|max:255',
'street' => 'required|string|max:255',
'zip' => 'required|string|max:10',
'city' => 'required|string|max:100',
'country' => 'required|string|max:2',
'phone' => 'nullable|string|max:50',
]);
$data['user_id'] = auth()->id();
Address::create($data);
return back()->with('success', 'Adresse gespeichert.');
}
public function destroyAddress(Address $address)
{
abort_unless($address->user_id === auth()->id(), 403);
$address->delete();
return back()->with('success', 'Adresse gelöscht.');
}
public function settings()
{
return view('shop.account.settings');
}
public function updateSettings(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . auth()->id(),
'password' => 'nullable|min:8|confirmed',
]);
$user = auth()->user();
$user->name = $data['name'];
$user->email = $data['email'];
if (!empty($data['password'])) {
$user->password = bcrypt($data['password']);
}
$user->save();
return back()->with('success', 'Einstellungen gespeichert.');
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\ShippingRate;
use App\Services\CartService;
use Illuminate\Http\Request;
class CartController extends Controller
{
public function __construct(private CartService $cart) {}
public function index()
{
$items = $this->cart->get();
$subtotal = $this->cart->subtotal();
$shippingRate = ShippingRate::forCart($subtotal);
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
$total = $subtotal + $shippingCost;
return view('shop.cart.index', compact('items', 'subtotal', 'shippingRate', 'shippingCost', 'total'));
}
public function add(Request $request)
{
$request->validate(['variant_id' => 'required|exists:product_variants,id', 'qty' => 'integer|min:1']);
$this->cart->add($request->variant_id, $request->integer('qty', 1));
return back()->with('success', 'Artikel zum Warenkorb hinzugefügt.');
}
public function update(Request $request)
{
foreach ($request->input('qty', []) as $key => $qty) {
$this->cart->update($key, (int) $qty);
}
return back()->with('success', 'Warenkorb aktualisiert.');
}
public function remove(string $key)
{
$this->cart->remove($key);
return back()->with('success', 'Artikel entfernt.');
}
}
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\ShippingRate;
use App\Services\CartService;
use App\Services\OrderService;
use Illuminate\Http\Request;
class CheckoutController extends Controller
{
public function __construct(
private CartService $cart,
private OrderService $orderService,
) {}
public function index()
{
if ($this->cart->count() === 0) {
return redirect()->route('cart.index')->with('error', 'Ihr Warenkorb ist leer.');
}
$subtotal = $this->cart->subtotal();
$shippingRate = ShippingRate::forCart($subtotal);
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
$total = $subtotal + $shippingCost;
$userAddresses = auth()->user()?->addresses ?? collect();
return view('shop.checkout.index', compact('subtotal', 'shippingCost', 'total', 'userAddresses'));
}
public function storeAddress(Request $request)
{
$rules = [
'billing.first_name' => 'required|string|max:100',
'billing.last_name' => 'required|string|max:100',
'billing.street' => 'required|string|max:255',
'billing.zip' => 'required|string|max:10',
'billing.city' => 'required|string|max:100',
'billing.country' => 'required|string|max:2',
'payment_method' => 'required|in:prepayment,invoice',
'agb' => 'accepted',
];
if (!auth()->check()) {
$rules['email'] = 'required|email';
}
if ($request->input('shipping_same') !== '1') {
$rules['shipping.first_name'] = 'required|string|max:100';
$rules['shipping.last_name'] = 'required|string|max:100';
$rules['shipping.street'] = 'required|string|max:255';
$rules['shipping.zip'] = 'required|string|max:10';
$rules['shipping.city'] = 'required|string|max:100';
$rules['shipping.country'] = 'required|string|max:2';
}
$data = $request->validate($rules);
if ($request->input('shipping_same') === '1') {
$data['shipping'] = $data['billing'];
}
$data['email'] = $request->input('email');
session(['checkout_data' => $data]);
return redirect()->route('checkout.confirm');
}
public function payment()
{
return redirect()->route('checkout.index');
}
public function confirm(Request $request)
{
$checkoutData = session('checkout_data');
if (!$checkoutData || $this->cart->count() === 0) {
return redirect()->route('checkout.index');
}
if ($request->isMethod('get')) {
$items = $this->cart->get();
$subtotal = $this->cart->subtotal();
$shippingRate = ShippingRate::forCart($subtotal);
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
$total = $subtotal + $shippingCost;
return view('shop.checkout.confirm', compact('checkoutData', 'items', 'subtotal', 'shippingCost', 'total'));
}
$order = $this->orderService->createFromSession(
$checkoutData,
$checkoutData['payment_method']
);
session()->forget('checkout_data');
return redirect()->route('checkout.thankyou', $order->order_number);
}
public function thankyou(string $order)
{
$order = \App\Models\Order::where('order_number', $order)->firstOrFail();
return view('shop.checkout.thankyou', compact('order'));
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\FaqGroup;
class FaqController extends Controller
{
public function index()
{
$groups = FaqGroup::with('faqs')->orderBy('sort_order')->get();
return view('shop.faq', compact('groups'));
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\Banner;
use App\Models\Category;
use App\Models\Page;
use App\Models\Product;
class HomeController extends Controller
{
public function index()
{
$banners = Banner::where('active', true)->orderBy('sort_order')->get();
$categories = Category::whereNull('parent_id')->where('active', true)->orderBy('sort_order')->get();
$topProducts = Product::where('active', true)->where('featured', true)->latest()->take(8)->get();
$newProducts = Product::where('active', true)->latest()->take(8)->get();
$blogPosts = Page::where('type', 'blog')->where('published', true)->latest('published_at')->take(3)->get();
return view('shop.home', compact('banners', 'categories', 'topProducts', 'newProducts', 'blogPosts'));
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\Request;
class OrderTrackingController extends Controller
{
public function index()
{
return view('shop.order_tracking');
}
public function show(Request $request)
{
$request->validate(['order_number' => 'required|string', 'email' => 'required|email']);
$order = Order::where('order_number', $request->order_number)
->where(function ($q) use ($request) {
$q->where('guest_email', $request->email)
->orWhereHas('user', fn($u) => $u->where('email', $request->email));
})->with('items', 'addresses')->first();
if (!$order) {
return back()->with('error', 'Bestellung nicht gefunden. Bitte prüfen Sie die Bestellnummer und E-Mail-Adresse.');
}
return view('shop.order_tracking', compact('order'));
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\Page;
class PageController extends Controller
{
public function show(string $slug)
{
$page = Page::where('slug', $slug)->where('published', true)->firstOrFail();
return view('shop.page', compact('page'));
}
public function blog()
{
$posts = Page::where('type', 'blog')->where('published', true)->latest('published_at')->paginate(10);
return view('shop.blog.index', compact('posts'));
}
public function blogPost(string $slug)
{
$post = Page::where('slug', $slug)->where('type', 'blog')->where('published', true)->firstOrFail();
return view('shop.blog.post', compact('post'));
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index(Request $request)
{
$query = Product::with(['images', 'category'])->where('active', true);
if ($request->filled('q')) {
$query->where('name', 'like', '%'.$request->q.'%');
}
if ($request->filled('min_price')) {
$query->where('base_price', '>=', $request->min_price);
}
if ($request->filled('max_price')) {
$query->where('base_price', '<=', $request->max_price);
}
$products = $query->orderBy('sort_order')->paginate(16);
$categories = Category::whereNull('parent_id')->where('active', true)->get();
return view('shop.products.index', compact('products', 'categories'));
}
public function category(string $slug)
{
$category = Category::where('slug', $slug)->where('active', true)->firstOrFail();
$categoryIds = collect([$category->id])->merge($category->children->pluck('id'));
$products = Product::with(['images'])->where('active', true)->whereIn('category_id', $categoryIds)->orderBy('sort_order')->paginate(16);
return view('shop.products.index', compact('products', 'category'));
}
public function show(string $slug)
{
$product = Product::with(['images', 'category', 'variants.attributeValues.attribute'])
->where('slug', $slug)
->where('active', true)
->firstOrFail();
$related = Product::where('category_id', $product->category_id)
->where('id', '!=', $product->id)
->where('active', true)
->take(4)->get();
return view('shop.products.show', compact('product', 'related'));
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Mail\WiderrufsformularMail;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class WiderrufsController extends Controller
{
public function index()
{
return view('shop.widerruf');
}
public function submit(Request $request)
{
$data = $request->validate([
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|email',
'street' => 'required|string|max:255',
'zip' => 'required|string|max:20',
'city' => 'required|string|max:255',
'customer_number' => 'nullable|string|max:100',
'order_number' => 'nullable|string|max:100',
'order_date' => 'nullable|date',
'date' => 'required|date',
'product' => 'required|string|max:500',
'reason_category' => 'nullable|string|max:100',
'reason' => 'nullable|string|max:2000',
'refund_method' => 'nullable|string|in:original,bank,credit',
'privacy_accepted' => 'accepted',
'accuracy_confirmed'=> 'accepted',
]);
$data['name'] = $data['first_name'] . ' ' . $data['last_name'];
$shopEmail = Setting::get('shop_email');
Mail::to($shopEmail)->send(new WiderrufsformularMail($data));
Mail::to($data['email'])->send(new WiderrufsformularMail($data, true));
return back()->with('success', 'Ihr Widerrufsformular wurde erfolgreich übermittelt. Sie erhalten eine Bestätigungsmail.');
}
}