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:
@@ -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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user