diff --git a/app/Http/Controllers/Admin/CouponController.php b/app/Http/Controllers/Admin/CouponController.php
new file mode 100644
index 0000000..75e06d6
--- /dev/null
+++ b/app/Http/Controllers/Admin/CouponController.php
@@ -0,0 +1,68 @@
+get();
+ return view('admin.coupons.index', compact('coupons'));
+ }
+
+ public function create()
+ {
+ return view('admin.coupons.form');
+ }
+
+ public function store(Request $request)
+ {
+ $data = $request->validate([
+ 'code' => 'required|string|max:50|unique:coupons,code',
+ 'type' => 'required|in:percent,fixed',
+ 'value' => 'required|numeric|min:0.01',
+ 'min_order_value' => 'nullable|numeric|min:0',
+ 'expires_at' => 'nullable|date',
+ 'max_uses' => 'nullable|integer|min:1',
+ 'active' => 'boolean',
+ ]);
+ $data['code'] = strtoupper(trim($data['code']));
+ $data['active'] = $request->boolean('active');
+
+ Coupon::create($data);
+ return redirect()->route('admin.coupons.index')->with('success', 'Gutschein erstellt.');
+ }
+
+ public function edit(Coupon $coupon)
+ {
+ return view('admin.coupons.form', compact('coupon'));
+ }
+
+ public function update(Request $request, Coupon $coupon)
+ {
+ $data = $request->validate([
+ 'code' => 'required|string|max:50|unique:coupons,code,' . $coupon->id,
+ 'type' => 'required|in:percent,fixed',
+ 'value' => 'required|numeric|min:0.01',
+ 'min_order_value' => 'nullable|numeric|min:0',
+ 'expires_at' => 'nullable|date',
+ 'max_uses' => 'nullable|integer|min:1',
+ 'active' => 'boolean',
+ ]);
+ $data['code'] = strtoupper(trim($data['code']));
+ $data['active'] = $request->boolean('active');
+
+ $coupon->update($data);
+ return redirect()->route('admin.coupons.index')->with('success', 'Gutschein aktualisiert.');
+ }
+
+ public function destroy(Coupon $coupon)
+ {
+ $coupon->delete();
+ return redirect()->route('admin.coupons.index')->with('success', 'Gutschein gelöscht.');
+ }
+}
diff --git a/app/Http/Controllers/Admin/NewsletterController.php b/app/Http/Controllers/Admin/NewsletterController.php
new file mode 100644
index 0000000..2079754
--- /dev/null
+++ b/app/Http/Controllers/Admin/NewsletterController.php
@@ -0,0 +1,35 @@
+get();
+ return view('admin.newsletter.index', compact('subscribers'));
+ }
+
+ public function export()
+ {
+ $rows = NewsletterSubscriber::whereNotNull('confirmed_at')
+ ->orderBy('email')
+ ->pluck('email');
+
+ $csv = "E-Mail\n" . $rows->implode("\n");
+
+ return response($csv, 200, [
+ 'Content-Type' => 'text/csv; charset=UTF-8',
+ 'Content-Disposition' => 'attachment; filename="newsletter-' . date('Y-m-d') . '.csv"',
+ ]);
+ }
+
+ public function destroy(NewsletterSubscriber $subscriber)
+ {
+ $subscriber->delete();
+ return back()->with('success', 'Abonnent gelöscht.');
+ }
+}
diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php
index 52b5b2c..2e3d983 100644
--- a/app/Http/Controllers/Admin/ProductController.php
+++ b/app/Http/Controllers/Admin/ProductController.php
@@ -39,6 +39,7 @@ class ProductController extends Controller
'category_id' => 'nullable|exists:categories,id',
'description' => 'nullable|string',
'base_price' => 'required|numeric|min:0',
+ 'sale_price' => 'nullable|numeric|min:0',
'tax_rate' => 'required|numeric|in:7,19',
'weight' => 'nullable|numeric|min:0',
'active' => 'boolean',
@@ -83,6 +84,7 @@ class ProductController extends Controller
'category_id' => 'nullable|exists:categories,id',
'description' => 'nullable|string',
'base_price' => 'required|numeric|min:0',
+ 'sale_price' => 'nullable|numeric|min:0',
'tax_rate' => 'required|numeric|in:7,19',
'weight' => 'nullable|numeric|min:0',
'active' => 'boolean',
diff --git a/app/Http/Controllers/Shop/CheckoutController.php b/app/Http/Controllers/Shop/CheckoutController.php
index c9e1213..b467407 100644
--- a/app/Http/Controllers/Shop/CheckoutController.php
+++ b/app/Http/Controllers/Shop/CheckoutController.php
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
+use App\Models\Coupon;
use App\Models\ShippingRate;
use App\Services\CartService;
use App\Services\OrderService;
@@ -21,12 +22,49 @@ class CheckoutController extends Controller
return redirect()->route('cart.index')->with('error', 'Ihr Warenkorb ist leer.');
}
$subtotal = $this->cart->subtotal();
- $shippingRate = ShippingRate::forCart($subtotal);
+ $country = session('checkout_country', 'DE');
+ $shippingRate = ShippingRate::forCart($subtotal, $country);
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
- $total = $subtotal + $shippingCost;
+ $discount = $this->getCouponDiscount($subtotal);
+ $total = max(0, $subtotal - $discount) + $shippingCost;
$userAddresses = auth()->user()?->addresses ?? collect();
+ $couponCode = session('coupon_code');
- return view('shop.checkout.index', compact('subtotal', 'shippingCost', 'total', 'userAddresses'));
+ return view('shop.checkout.index', compact('subtotal', 'shippingCost', 'total', 'userAddresses', 'discount', 'couponCode'));
+ }
+
+ public function applyCoupon(Request $request)
+ {
+ $code = trim($request->input('coupon_code', ''));
+ if (!$code) {
+ return back()->with('coupon_error', 'Bitte Gutscheincode eingeben.');
+ }
+ $coupon = Coupon::findValid($code);
+ if (!$coupon) {
+ return back()->with('coupon_error', 'Gutscheincode ungültig oder abgelaufen.');
+ }
+ $subtotal = $this->cart->subtotal();
+ if ($coupon->min_order_value && $subtotal < $coupon->min_order_value) {
+ return back()->with('coupon_error', 'Mindestbestellwert von ' . number_format($coupon->min_order_value, 2, ',', '.') . ' € nicht erreicht.');
+ }
+ session(['coupon_code' => $coupon->code]);
+ return back()->with('coupon_success', 'Gutschein "' . $coupon->code . '" angewendet!');
+ }
+
+ public function removeCoupon()
+ {
+ session()->forget('coupon_code');
+ return back()->with('coupon_success', 'Gutschein entfernt.');
+ }
+
+ public function shippingCost(Request $request)
+ {
+ $country = strtoupper($request->input('country', 'DE'));
+ session(['checkout_country' => $country]);
+ $subtotal = $this->cart->subtotal();
+ $shippingRate = ShippingRate::forCart($subtotal, $country);
+ $cost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
+ return response()->json(['cost' => $cost, 'formatted' => number_format($cost, 2, ',', '.') . ' €']);
}
public function storeAddress(Request $request)
@@ -83,10 +121,13 @@ class CheckoutController extends Controller
if ($request->isMethod('get')) {
$items = $this->cart->get();
$subtotal = $this->cart->subtotal();
- $shippingRate = ShippingRate::forCart($subtotal);
+ $country = $checkoutData['shipping']['country'] ?? $checkoutData['billing']['country'] ?? 'DE';
+ $shippingRate = ShippingRate::forCart($subtotal, $country);
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
- $total = $subtotal + $shippingCost;
- return view('shop.checkout.confirm', compact('checkoutData', 'items', 'subtotal', 'shippingCost', 'total'));
+ $discount = $this->getCouponDiscount($subtotal);
+ $total = max(0, $subtotal - $discount) + $shippingCost;
+ $couponCode = session('coupon_code');
+ return view('shop.checkout.confirm', compact('checkoutData', 'items', 'subtotal', 'shippingCost', 'total', 'discount', 'couponCode'));
}
$order = $this->orderService->createFromSession(
@@ -94,7 +135,7 @@ class CheckoutController extends Controller
$checkoutData['payment_method']
);
- session()->forget('checkout_data');
+ session()->forget(['checkout_data', 'checkout_country']);
return redirect()->route('checkout.thankyou', $order->order_number);
}
@@ -104,4 +145,15 @@ class CheckoutController extends Controller
$order = \App\Models\Order::where('order_number', $order)->firstOrFail();
return view('shop.checkout.thankyou', compact('order'));
}
+
+ private function getCouponDiscount(float $subtotal): float
+ {
+ if ($code = session('coupon_code')) {
+ $coupon = Coupon::findValid($code);
+ if ($coupon) {
+ return $coupon->calcDiscount($subtotal);
+ }
+ }
+ return 0;
+ }
}
diff --git a/app/Http/Controllers/Shop/HomeController.php b/app/Http/Controllers/Shop/HomeController.php
index 895f00a..6dcb9ec 100644
--- a/app/Http/Controllers/Shop/HomeController.php
+++ b/app/Http/Controllers/Shop/HomeController.php
@@ -12,12 +12,13 @@ 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();
+ $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)->with('images','variants','category')->latest()->take(8)->get();
+ $newProducts = Product::where('active', true)->with('images','variants','category')->latest()->take(8)->get();
+ $saleProducts = Product::where('active', true)->whereNotNull('sale_price')->with('images','variants','category')->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'));
+ return view('shop.home', compact('banners', 'categories', 'topProducts', 'newProducts', 'saleProducts', 'blogPosts'));
}
}
diff --git a/app/Http/Controllers/Shop/NewsletterController.php b/app/Http/Controllers/Shop/NewsletterController.php
new file mode 100644
index 0000000..6af5d0b
--- /dev/null
+++ b/app/Http/Controllers/Shop/NewsletterController.php
@@ -0,0 +1,57 @@
+validate(['newsletter_email' => 'required|email|max:255']);
+ $email = strtolower($request->input('newsletter_email'));
+
+ $existing = NewsletterSubscriber::where('email', $email)->first();
+
+ if ($existing && $existing->isConfirmed()) {
+ return back()->with('newsletter_info', 'Diese E-Mail-Adresse ist bereits angemeldet.');
+ }
+
+ $token = Str::random(64);
+
+ if ($existing) {
+ $existing->update(['token' => $token]);
+ } else {
+ NewsletterSubscriber::create(['email' => $email, 'token' => $token]);
+ }
+
+ $confirmUrl = route('newsletter.confirm', $token);
+ $shopName = \App\Models\Setting::get('shop_name', 'Shop');
+
+ Mail::raw(
+ "Bitte bestätigen Sie Ihre Newsletter-Anmeldung:\n\n{$confirmUrl}\n\n"
+ . "Falls Sie sich nicht angemeldet haben, können Sie diese E-Mail ignorieren.\n\n"
+ . "– {$shopName}",
+ fn($m) => $m->to($email)->subject("Newsletter-Anmeldung bestätigen – {$shopName}")
+ );
+
+ return back()->with('newsletter_success', 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Anmeldung.');
+ }
+
+ public function confirm(string $token)
+ {
+ $subscriber = NewsletterSubscriber::where('token', $token)->whereNull('confirmed_at')->first();
+
+ if (!$subscriber) {
+ return redirect('/')->with('error', 'Ungültiger oder bereits verwendeter Bestätigungslink.');
+ }
+
+ $subscriber->update(['confirmed_at' => now(), 'token' => null]);
+
+ return redirect('/')->with('success', 'Newsletter-Anmeldung erfolgreich bestätigt!');
+ }
+}
diff --git a/app/Models/Coupon.php b/app/Models/Coupon.php
new file mode 100644
index 0000000..1e63b80
--- /dev/null
+++ b/app/Models/Coupon.php
@@ -0,0 +1,36 @@
+ 'boolean',
+ 'expires_at' => 'datetime',
+ 'value' => 'decimal:2',
+ 'min_order_value' => 'decimal:2',
+ ];
+
+ public static function findValid(string $code): ?self
+ {
+ return self::where('code', strtoupper(trim($code)))
+ ->where('active', true)
+ ->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>=', now()))
+ ->where(fn($q) => $q->whereNull('max_uses')->orWhereColumn('used_count', '<', 'max_uses'))
+ ->first();
+ }
+
+ public function calcDiscount(float $subtotal): float
+ {
+ if ($this->min_order_value && $subtotal < $this->min_order_value) {
+ return 0.0;
+ }
+ if ($this->type === 'percent') {
+ return round($subtotal * $this->value / 100, 2);
+ }
+ return min((float) $this->value, $subtotal);
+ }
+}
diff --git a/app/Models/NewsletterSubscriber.php b/app/Models/NewsletterSubscriber.php
new file mode 100644
index 0000000..dd0c0d6
--- /dev/null
+++ b/app/Models/NewsletterSubscriber.php
@@ -0,0 +1,16 @@
+ 'datetime'];
+
+ public function isConfirmed(): bool
+ {
+ return $this->confirmed_at !== null;
+ }
+}
diff --git a/app/Models/Order.php b/app/Models/Order.php
index 2ca03c0..1e3a5d5 100644
--- a/app/Models/Order.php
+++ b/app/Models/Order.php
@@ -10,12 +10,14 @@ class Order extends Model
{
protected $fillable = [
'order_number','user_id','guest_email','status','payment_method',
+ 'coupon_code','discount_amount',
'subtotal','tax_total','shipping_cost','total','tracking_code','notes','paid_at',
];
protected $casts = [
'paid_at' => 'datetime',
'subtotal' => 'decimal:2',
+ 'discount_amount' => 'decimal:2',
'tax_total' => 'decimal:2',
'shipping_cost' => 'decimal:2',
'total' => 'decimal:2',
diff --git a/app/Models/Product.php b/app/Models/Product.php
index 6500d5a..f918ded 100644
--- a/app/Models/Product.php
+++ b/app/Models/Product.php
@@ -8,9 +8,19 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Product extends Model
{
- protected $fillable = ['category_id','name','slug','description','base_price','tax_rate','weight','active','featured','track_stock','sort_order'];
+ protected $fillable = ['category_id','name','slug','description','base_price','sale_price','tax_rate','weight','active','featured','track_stock','sort_order'];
- protected $casts = ['active' => 'boolean', 'featured' => 'boolean', 'track_stock' => 'boolean', 'base_price' => 'decimal:2', 'tax_rate' => 'decimal:2'];
+ protected $casts = ['active' => 'boolean', 'featured' => 'boolean', 'track_stock' => 'boolean', 'base_price' => 'decimal:2', 'sale_price' => 'decimal:2', 'tax_rate' => 'decimal:2'];
+
+ public function isOnSale(): bool
+ {
+ return $this->sale_price !== null && (float)$this->sale_price < (float)$this->base_price;
+ }
+
+ public function getEffectivePriceAttribute(): float
+ {
+ return $this->isOnSale() ? (float)$this->sale_price : (float)$this->base_price;
+ }
public function category(): BelongsTo
{
diff --git a/app/Models/ShippingRate.php b/app/Models/ShippingRate.php
index 36136f1..faa8494 100644
--- a/app/Models/ShippingRate.php
+++ b/app/Models/ShippingRate.php
@@ -10,9 +10,32 @@ class ShippingRate extends Model
protected $casts = ['active' => 'boolean', 'price' => 'decimal:2', 'free_from' => 'decimal:2'];
- public static function forCart(float $total, string $zone = 'DE'): ?self
+ // Länder → Zonen-Mapping
+ public static array $countryZones = [
+ 'DE' => 'DE',
+ 'AT' => 'DACH',
+ 'CH' => 'DACH',
+ 'LI' => 'DACH',
+ 'FR' => 'EU', 'NL' => 'EU', 'BE' => 'EU', 'LU' => 'EU',
+ 'IT' => 'EU', 'ES' => 'EU', 'PT' => 'EU', 'PL' => 'EU',
+ 'CZ' => 'EU', 'SK' => 'EU', 'HU' => 'EU', 'RO' => 'EU',
+ 'BG' => 'EU', 'HR' => 'EU', 'SI' => 'EU', 'GR' => 'EU',
+ 'DK' => 'EU', 'SE' => 'EU', 'FI' => 'EU', 'EE' => 'EU',
+ 'LV' => 'EU', 'LT' => 'EU', 'IE' => 'EU', 'MT' => 'EU',
+ 'CY' => 'EU',
+ ];
+
+ public static function zoneForCountry(string $country): string
{
- return self::where('zone', $zone)->where('active', true)->first();
+ return self::$countryZones[strtoupper($country)] ?? 'WORLD';
+ }
+
+ public static function forCart(float $total, string $country = 'DE'): ?self
+ {
+ $zone = self::zoneForCountry($country);
+ $rate = self::where('zone', $zone)->where('active', true)->first();
+ // Fallback auf DE wenn keine Zone gefunden
+ return $rate ?? self::where('zone', 'DE')->where('active', true)->first();
}
public function getCostForTotal(float $total): float
diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php
index 81bb603..c61900d 100644
--- a/app/Services/OrderService.php
+++ b/app/Services/OrderService.php
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Mail\OrderConfirmationMail;
use App\Mail\OrderShippedMail;
+use App\Models\Coupon;
use App\Models\Order;
use App\Models\OrderAddress;
use App\Models\OrderItem;
@@ -17,40 +18,58 @@ class OrderService
public function createFromSession(array $addressData, string $paymentMethod): Order
{
- $items = $this->cart->get();
+ $items = $this->cart->get();
$subtotal = $this->cart->subtotal();
- $shippingRate = ShippingRate::forCart($subtotal);
+ $country = $addressData['shipping']['country'] ?? $addressData['billing']['country'] ?? 'DE';
+ $shippingRate = ShippingRate::forCart($subtotal, $country);
$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);
+ // Gutschein aus Session
+ $couponCode = null;
+ $discountAmount = 0;
+ if ($code = session('coupon_code')) {
+ $coupon = Coupon::findValid($code);
+ if ($coupon) {
+ $discountAmount = $coupon->calcDiscount($subtotal);
+ $couponCode = $coupon->code;
+ $coupon->increment('used_count');
+ }
+ }
+
+ $taxable = max(0, $subtotal - $discountAmount);
+ $taxTotal = array_sum(array_map(function ($item) use ($subtotal, $discountAmount) {
+ $ratio = $subtotal > 0 ? ($item['price'] * $item['qty']) / $subtotal : 0;
+ $itemNet = ($item['price'] * $item['qty'] - $discountAmount * $ratio) / (1 + $item['tax_rate'] / 100);
+ return round(($item['price'] * $item['qty'] - $discountAmount * $ratio) - $itemNet, 2);
}, $items));
+ $total = max(0, $subtotal - $discountAmount) + $shippingCost;
+
$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,
+ 'order_number' => Order::generateOrderNumber(),
+ 'user_id' => auth()->id(),
+ 'guest_email' => auth()->check() ? null : ($addressData['email'] ?? null),
+ 'status' => 'new',
+ 'payment_method' => $paymentMethod,
+ 'coupon_code' => $couponCode,
+ 'discount_amount' => $discountAmount,
+ 'subtotal' => $subtotal,
+ 'tax_total' => $taxTotal,
+ 'shipping_cost' => $shippingCost,
+ 'total' => $total,
]);
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'],
+ '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']);
@@ -63,6 +82,7 @@ class OrderService
}
$this->cart->clear();
+ session()->forget('coupon_code');
Mail::to($order->guest_email ?? $order->user?->email)->send(new OrderConfirmationMail($order));
diff --git a/database/migrations/2024_01_01_000019_create_coupons_table.php b/database/migrations/2024_01_01_000019_create_coupons_table.php
new file mode 100644
index 0000000..247189f
--- /dev/null
+++ b/database/migrations/2024_01_01_000019_create_coupons_table.php
@@ -0,0 +1,36 @@
+id();
+ $table->string('code')->unique();
+ $table->enum('type', ['percent', 'fixed'])->default('fixed');
+ $table->decimal('value', 10, 2);
+ $table->decimal('min_order_value', 10, 2)->nullable();
+ $table->timestamp('expires_at')->nullable();
+ $table->unsignedInteger('max_uses')->nullable();
+ $table->unsignedInteger('used_count')->default(0);
+ $table->boolean('active')->default(true);
+ $table->timestamps();
+ });
+
+ Schema::table('orders', function (Blueprint $table) {
+ $table->string('coupon_code')->nullable()->after('payment_method');
+ $table->decimal('discount_amount', 10, 2)->default(0)->after('coupon_code');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('orders', function (Blueprint $table) {
+ $table->dropColumn(['coupon_code', 'discount_amount']);
+ });
+ Schema::dropIfExists('coupons');
+ }
+};
diff --git a/database/migrations/2024_01_01_000020_create_newsletter_subscribers_table.php b/database/migrations/2024_01_01_000020_create_newsletter_subscribers_table.php
new file mode 100644
index 0000000..4b82e17
--- /dev/null
+++ b/database/migrations/2024_01_01_000020_create_newsletter_subscribers_table.php
@@ -0,0 +1,23 @@
+id();
+ $table->string('email')->unique();
+ $table->string('token', 64)->nullable();
+ $table->timestamp('confirmed_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('newsletter_subscribers');
+ }
+};
diff --git a/database/migrations/2024_01_01_000021_add_sale_price_to_products.php b/database/migrations/2024_01_01_000021_add_sale_price_to_products.php
new file mode 100644
index 0000000..e86e8de
--- /dev/null
+++ b/database/migrations/2024_01_01_000021_add_sale_price_to_products.php
@@ -0,0 +1,21 @@
+decimal('sale_price', 10, 2)->nullable()->after('base_price');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('products', function (Blueprint $table) {
+ $table->dropColumn('sale_price');
+ });
+ }
+};
diff --git a/resources/views/admin/coupons/form.blade.php b/resources/views/admin/coupons/form.blade.php
new file mode 100644
index 0000000..10dec35
--- /dev/null
+++ b/resources/views/admin/coupons/form.blade.php
@@ -0,0 +1,74 @@
+@extends('layouts.admin')
+@section('title', isset($coupon) ? 'Gutschein bearbeiten' : 'Neuer Gutschein')
+@section('content')
+
+
← Zurück
+
{{ isset($coupon) ? 'Gutschein bearbeiten' : 'Neuer Gutschein' }}
+
+
+
+@endsection
diff --git a/resources/views/admin/coupons/index.blade.php b/resources/views/admin/coupons/index.blade.php
new file mode 100644
index 0000000..2dddea6
--- /dev/null
+++ b/resources/views/admin/coupons/index.blade.php
@@ -0,0 +1,59 @@
+@extends('layouts.admin')
+@section('title', 'Gutscheine')
+@section('content')
+
+
+@if(session('success'))
+ {{ session('success') }}
+@endif
+
+
+
+
+
+ | Code |
+ Typ |
+ Wert |
+ Mind. Bestellwert |
+ Läuft ab |
+ Verwendet |
+ Status |
+ |
+
+
+
+ @forelse($coupons as $coupon)
+
+ | {{ $coupon->code }} |
+ {{ $coupon->type === 'percent' ? 'Prozent' : 'Betrag' }} |
+
+ {{ number_format($coupon->value, 2, ',', '.') }}{{ $coupon->type === 'percent' ? ' %' : ' €' }}
+ |
+ {{ $coupon->min_order_value ? number_format($coupon->min_order_value, 2, ',', '.') . ' €' : '–' }} |
+ {{ $coupon->expires_at ? $coupon->expires_at->format('d.m.Y') : '–' }} |
+ {{ $coupon->used_count }}{{ $coupon->max_uses ? ' / ' . $coupon->max_uses : '' }} |
+
+ @if($coupon->active)
+ Aktiv
+ @else
+ Inaktiv
+ @endif
+ |
+
+ Bearbeiten
+
+ |
+
+ @empty
+ | Noch keine Gutscheine angelegt. |
+ @endforelse
+
+
+
+@endsection
diff --git a/resources/views/admin/newsletter/index.blade.php b/resources/views/admin/newsletter/index.blade.php
new file mode 100644
index 0000000..01b2b5f
--- /dev/null
+++ b/resources/views/admin/newsletter/index.blade.php
@@ -0,0 +1,55 @@
+@extends('layouts.admin')
+@section('title', 'Newsletter-Abonnenten')
+@section('content')
+
+
+@if(session('success'))
+ {{ session('success') }}
+@endif
+
+
+ {{ $subscribers->whereNotNull('confirmed_at')->count() }} bestätigt,
+ {{ $subscribers->whereNull('confirmed_at')->count() }} ausstehend
+
+
+
+
+
+
+ | E-Mail |
+ Status |
+ Angemeldet am |
+ Bestätigt am |
+ |
+
+
+
+ @forelse($subscribers as $sub)
+
+ | {{ $sub->email }} |
+
+ @if($sub->isConfirmed())
+ Bestätigt
+ @else
+ Ausstehend
+ @endif
+ |
+ {{ $sub->created_at->format('d.m.Y H:i') }} |
+ {{ $sub->confirmed_at?->format('d.m.Y H:i') ?? '–' }} |
+
+
+ |
+
+ @empty
+ | Keine Abonnenten. |
+ @endforelse
+
+
+
+@endsection
diff --git a/resources/views/admin/products/form.blade.php b/resources/views/admin/products/form.blade.php
index eec35e0..d94a9f6 100644
--- a/resources/views/admin/products/form.blade.php
+++ b/resources/views/admin/products/form.blade.php
@@ -21,11 +21,15 @@
-
+
+
+
+
+