diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 320499c..5e27a63 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -45,6 +45,7 @@ jobs:
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:clear
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:clear
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan migrate --force
+ /opt/plesk/php/8.4/bin/php -d open_basedir="" artisan db:seed --class=EmailTemplateSeeder --force
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan config:cache
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan route:cache
/opt/plesk/php/8.4/bin/php -d open_basedir="" artisan view:cache
diff --git a/app/Http/Controllers/Admin/EmailTemplateController.php b/app/Http/Controllers/Admin/EmailTemplateController.php
new file mode 100644
index 0000000..6a40958
--- /dev/null
+++ b/app/Http/Controllers/Admin/EmailTemplateController.php
@@ -0,0 +1,36 @@
+get();
+ return view('admin.email-templates.index', compact('templates'));
+ }
+
+ public function edit(EmailTemplate $emailTemplate)
+ {
+ return view('admin.email-templates.edit', ['template' => $emailTemplate]);
+ }
+
+ public function update(Request $request, EmailTemplate $emailTemplate)
+ {
+ $request->validate([
+ 'subject' => 'required|string|max:255',
+ 'body' => 'required|string',
+ ]);
+
+ $emailTemplate->update([
+ 'subject' => $request->input('subject'),
+ 'body' => $request->input('body'),
+ ]);
+
+ return back()->with('success', 'E-Mail-Template gespeichert.');
+ }
+}
diff --git a/app/Http/Controllers/Admin/PageController.php b/app/Http/Controllers/Admin/PageController.php
index fdc92f3..ede5394 100644
--- a/app/Http/Controllers/Admin/PageController.php
+++ b/app/Http/Controllers/Admin/PageController.php
@@ -59,11 +59,13 @@ class PageController extends Controller
'show_in_footer' => 'boolean',
'show_in_nav' => 'boolean',
'nav_sort' => 'integer|min:0',
+ 'layout' => 'nullable|in:default,sidebar,full,landing',
]);
$data['slug'] = $data['slug'] ? Str::slug($data['slug']) : Str::slug($data['title']);
$data['published'] = $request->boolean('published');
$data['show_in_footer'] = $request->boolean('show_in_footer');
$data['show_in_nav'] = $request->boolean('show_in_nav');
+ $data['layout'] = $request->input('layout', 'default');
return $data;
}
}
diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php
index 6bb8e1f..debcdca 100644
--- a/app/Http/Controllers/Admin/SettingsController.php
+++ b/app/Http/Controllers/Admin/SettingsController.php
@@ -13,6 +13,7 @@ class SettingsController extends Controller
'bank_name','bank_iban','bank_bic','invoice_payment_days',
'smtp_host','smtp_port','smtp_user','smtp_password','smtp_from_name','smtp_from_email',
'cookie_consent','google_analytics','require_customer_approval',
+ 'theme_name',
];
public function index()
diff --git a/app/Mail/OrderConfirmationMail.php b/app/Mail/OrderConfirmationMail.php
index 22229ae..3a4625b 100644
--- a/app/Mail/OrderConfirmationMail.php
+++ b/app/Mail/OrderConfirmationMail.php
@@ -2,6 +2,7 @@
namespace App\Mail;
+use App\Models\EmailTemplate;
use App\Models\Order;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
@@ -16,8 +17,71 @@ class OrderConfirmationMail extends Mailable
public function build()
{
+ $order = $this->order;
+ $template = EmailTemplate::getByKey('order_confirmation');
+
+ if (!$template) {
+ return $this
+ ->subject('Bestellbestätigung ' . $order->order_number . ' – ' . Setting::get('shop_name'))
+ ->view('emails.order_confirmation');
+ }
+
+ $itemRows = '';
+ foreach ($order->items as $item) {
+ $variant = $item->variant_label ? ' (' . e($item->variant_label) . ') ' : '';
+ $itemRows .= '
'
+ . '' . e($item->product_name) . $variant . ' '
+ . '' . $item->quantity . ' '
+ . '' . number_format($item->unit_price, 2, ',', '.') . ' € '
+ . '' . number_format($item->line_total, 2, ',', '.') . ' € '
+ . ' ';
+ }
+ $itemsTable = ''
+ . ''
+ . 'Artikel '
+ . 'Menge '
+ . 'Einzelpreis '
+ . 'Gesamt '
+ . ' ' . $itemRows . ' '
+ . ''
+ . 'Zwischensumme ' . number_format($order->subtotal, 2, ',', '.') . ' € '
+ . 'Versand ' . number_format($order->shipping_cost, 2, ',', '.') . ' € '
+ . 'Gesamt (inkl. MwSt.) ' . number_format($order->total, 2, ',', '.') . ' € '
+ . '
';
+
+ if ($order->payment_method === 'prepayment') {
+ $paymentInfo = ''
+ . 'Bitte überweisen Sie den Betrag auf folgendes Konto: '
+ . 'Empfänger: ' . e(Setting::get('shop_owner')) . ' '
+ . 'Bank: ' . e(Setting::get('bank_name')) . ' '
+ . 'IBAN: ' . e(Setting::get('bank_iban')) . ' '
+ . 'BIC: ' . e(Setting::get('bank_bic')) . ' '
+ . 'Verwendungszweck: ' . e($order->order_number) . ' '
+ . 'Betrag: ' . number_format($order->total, 2, ',', '.') . ' € '
+ . '
';
+ } else {
+ $days = Setting::get('invoice_payment_days', 14);
+ $paymentInfo = ''
+ . 'Sie erhalten Ihre Rechnung separat per E-Mail. '
+ . 'Zahlungsziel: ' . $days . ' Tage nach Rechnungserhalt.'
+ . '
';
+ }
+
+ $customerName = $order->billingAddress()?->full_name ?? $order->guest_email;
+
+ $data = [
+ 'customer_name' => $customerName,
+ 'order_number' => $order->order_number,
+ 'order_total' => number_format($order->total, 2, ',', '.'),
+ 'payment_info' => $paymentInfo,
+ 'order_items_table' => $itemsTable,
+ 'shop_name' => Setting::get('shop_name'),
+ 'shop_email' => Setting::get('shop_email'),
+ 'shop_address' => Setting::get('shop_address'),
+ ];
+
return $this
- ->subject('Bestellbestätigung ' . $this->order->order_number . ' – ' . Setting::get('shop_name'))
- ->view('emails.order_confirmation');
+ ->subject($template->renderSubject($data))
+ ->html($template->renderBody($data));
}
}
diff --git a/app/Mail/OrderShippedMail.php b/app/Mail/OrderShippedMail.php
index d0f6a30..2e5b08d 100644
--- a/app/Mail/OrderShippedMail.php
+++ b/app/Mail/OrderShippedMail.php
@@ -2,6 +2,7 @@
namespace App\Mail;
+use App\Models\EmailTemplate;
use App\Models\Order;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
@@ -16,8 +17,32 @@ class OrderShippedMail extends Mailable
public function build()
{
+ $order = $this->order;
+ $template = EmailTemplate::getByKey('order_shipped');
+
+ if (!$template) {
+ return $this
+ ->subject('Ihre Bestellung wurde versendet – ' . $order->order_number)
+ ->view('emails.order_shipped');
+ }
+
+ $trackingInfo = $order->tracking_code
+ ? ''
+ . 'Sendungsverfolgung: '
+ . 'Tracking-Nummer: ' . e($order->tracking_code) . ' '
+ . '
'
+ : '';
+
+ $data = [
+ 'order_number' => $order->order_number,
+ 'tracking_code' => $order->tracking_code ?? '',
+ 'tracking_info' => $trackingInfo,
+ 'shop_name' => Setting::get('shop_name'),
+ 'shop_email' => Setting::get('shop_email'),
+ ];
+
return $this
- ->subject('Ihre Bestellung wurde versendet – ' . $this->order->order_number)
- ->view('emails.order_shipped');
+ ->subject($template->renderSubject($data))
+ ->html($template->renderBody($data));
}
}
diff --git a/app/Mail/PaymentReceivedMail.php b/app/Mail/PaymentReceivedMail.php
index 41404b4..65fc896 100644
--- a/app/Mail/PaymentReceivedMail.php
+++ b/app/Mail/PaymentReceivedMail.php
@@ -2,6 +2,7 @@
namespace App\Mail;
+use App\Models\EmailTemplate;
use App\Models\Order;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
@@ -20,9 +21,29 @@ class PaymentReceivedMail extends Mailable
public function build()
{
+ $order = $this->order;
+ $template = EmailTemplate::getByKey('payment_received');
+
+ if (!$template) {
+ return $this
+ ->subject('Ihre Rechnung – ' . $order->order_number . ' – ' . Setting::get('shop_name'))
+ ->view('emails.payment_received')
+ ->attachData($this->pdfContent, $this->pdfFilename, ['mime' => 'application/pdf']);
+ }
+
+ $customerName = $order->billingAddress()?->full_name ?? $order->guest_email;
+
+ $data = [
+ 'customer_name' => $customerName,
+ 'order_number' => $order->order_number,
+ 'order_total' => number_format($order->total, 2, ',', '.'),
+ 'shop_name' => Setting::get('shop_name'),
+ 'shop_email' => Setting::get('shop_email'),
+ ];
+
return $this
- ->subject('Ihre Rechnung – ' . $this->order->order_number . ' – ' . Setting::get('shop_name'))
- ->view('emails.payment_received')
+ ->subject($template->renderSubject($data))
+ ->html($template->renderBody($data))
->attachData($this->pdfContent, $this->pdfFilename, ['mime' => 'application/pdf']);
}
}
diff --git a/app/Mail/WiderrufsformularMail.php b/app/Mail/WiderrufsformularMail.php
index a867fb9..e01c8a6 100644
--- a/app/Mail/WiderrufsformularMail.php
+++ b/app/Mail/WiderrufsformularMail.php
@@ -2,6 +2,7 @@
namespace App\Mail;
+use App\Models\EmailTemplate;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -18,10 +19,32 @@ class WiderrufsformularMail extends Mailable
public function build()
{
- $subject = $this->isConfirmation
- ? 'Bestätigung Ihres Widerrufs – ' . Setting::get('shop_name')
- : 'Neues Widerrufsformular eingegangen';
+ if (!$this->isConfirmation) {
+ $subject = 'Neues Widerrufsformular eingegangen';
+ return $this->subject($subject)->view('emails.widerruf');
+ }
- return $this->subject($subject)->view('emails.widerruf');
+ $template = EmailTemplate::getByKey('widerruf_confirmation');
+
+ if (!$template) {
+ return $this
+ ->subject('Bestätigung Ihres Widerrufs – ' . Setting::get('shop_name'))
+ ->view('emails.widerruf');
+ }
+
+ $vars = [
+ 'customer_name' => $this->data['name'],
+ 'widerruf_name' => $this->data['name'],
+ 'widerruf_email' => $this->data['email'],
+ 'widerruf_order_number' => $this->data['order_number'] ?? '—',
+ 'widerruf_product' => $this->data['product'],
+ 'widerruf_date' => $this->data['date'],
+ 'widerruf_reason' => $this->data['reason'] ?? '—',
+ 'shop_name' => Setting::get('shop_name'),
+ ];
+
+ return $this
+ ->subject($template->renderSubject($vars))
+ ->html($template->renderBody($vars));
}
}
diff --git a/app/Models/EmailTemplate.php b/app/Models/EmailTemplate.php
new file mode 100644
index 0000000..19360ae
--- /dev/null
+++ b/app/Models/EmailTemplate.php
@@ -0,0 +1,34 @@
+ 'array'];
+
+ public static function getByKey(string $key): ?self
+ {
+ return static::where('key', $key)->first();
+ }
+
+ public function renderBody(array $data): string
+ {
+ $body = $this->body;
+ foreach ($data as $k => $v) {
+ $body = str_replace('{{' . $k . '}}', (string) $v, $body);
+ }
+ return $body;
+ }
+
+ public function renderSubject(array $data): string
+ {
+ $subject = $this->subject;
+ foreach ($data as $k => $v) {
+ $subject = str_replace('{{' . $k . '}}', (string) $v, $subject);
+ }
+ return $subject;
+ }
+}
diff --git a/app/Models/Page.php b/app/Models/Page.php
index b0fbc3a..59cf362 100644
--- a/app/Models/Page.php
+++ b/app/Models/Page.php
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
- protected $fillable = ['slug','title','content','type','image','meta_title','meta_description','published','published_at','show_in_footer','show_in_nav','nav_sort'];
+ protected $fillable = ['slug','title','content','type','image','meta_title','meta_description','published','published_at','show_in_footer','show_in_nav','nav_sort','layout'];
protected $casts = ['published' => 'boolean', 'published_at' => 'datetime', 'show_in_footer' => 'boolean', 'show_in_nav' => 'boolean'];
}
diff --git a/config/themes.php b/config/themes.php
new file mode 100644
index 0000000..a0e2a4a
--- /dev/null
+++ b/config/themes.php
@@ -0,0 +1,39 @@
+ [
+ 'name' => 'Blau (Standard)',
+ 'primary' => '#1d4ed8',
+ 'dark' => '#1e40af',
+ 'light' => '#eff6ff',
+ 'text' => '#1e40af',
+ ],
+ 'green' => [
+ 'name' => 'Grün',
+ 'primary' => '#16a34a',
+ 'dark' => '#15803d',
+ 'light' => '#f0fdf4',
+ 'text' => '#15803d',
+ ],
+ 'red' => [
+ 'name' => 'Rot',
+ 'primary' => '#dc2626',
+ 'dark' => '#b91c1c',
+ 'light' => '#fef2f2',
+ 'text' => '#b91c1c',
+ ],
+ 'purple' => [
+ 'name' => 'Lila',
+ 'primary' => '#7c3aed',
+ 'dark' => '#6d28d9',
+ 'light' => '#f5f3ff',
+ 'text' => '#6d28d9',
+ ],
+ 'orange' => [
+ 'name' => 'Orange',
+ 'primary' => '#ea580c',
+ 'dark' => '#c2410c',
+ 'light' => '#fff7ed',
+ 'text' => '#c2410c',
+ ],
+];
diff --git a/database/migrations/2024_01_01_000017_create_email_templates_table.php b/database/migrations/2024_01_01_000017_create_email_templates_table.php
new file mode 100644
index 0000000..18cf064
--- /dev/null
+++ b/database/migrations/2024_01_01_000017_create_email_templates_table.php
@@ -0,0 +1,25 @@
+id();
+ $table->string('key')->unique();
+ $table->string('label');
+ $table->string('subject');
+ $table->longText('body');
+ $table->json('variables')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('email_templates');
+ }
+};
diff --git a/database/migrations/2024_01_01_000018_add_layout_to_pages.php b/database/migrations/2024_01_01_000018_add_layout_to_pages.php
new file mode 100644
index 0000000..cb48c6b
--- /dev/null
+++ b/database/migrations/2024_01_01_000018_add_layout_to_pages.php
@@ -0,0 +1,21 @@
+string('layout')->default('default')->after('show_in_footer');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('pages', function (Blueprint $table) {
+ $table->dropColumn('layout');
+ });
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index a97af88..fc30fbd 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -11,6 +11,7 @@ class DatabaseSeeder extends Seeder
$this->call([
SettingsSeeder::class,
AdminUserSeeder::class,
+ EmailTemplateSeeder::class,
]);
}
}
diff --git a/database/seeders/EmailTemplateSeeder.php b/database/seeders/EmailTemplateSeeder.php
new file mode 100644
index 0000000..aa6132d
--- /dev/null
+++ b/database/seeders/EmailTemplateSeeder.php
@@ -0,0 +1,133 @@
+ 'order_confirmation',
+ 'label' => 'Bestellbestätigung',
+ 'subject' => 'Bestellbestätigung {{order_number}} – {{shop_name}}',
+ 'variables' => ['customer_name','order_number','order_total','payment_info','order_items_table','shop_name','shop_email','shop_address','shop_owner','bank_name','bank_iban','bank_bic','invoice_payment_days'],
+ 'body' => <<<'HTML'
+
+
+
+
+Vielen Dank für Ihre Bestellung!
+Guten Tag {{customer_name}},
+wir haben Ihre Bestellung {{order_number}} erhalten und bestätigen sie hiermit.
+
+{{payment_info}}
+
+Bestellübersicht
+{{order_items_table}}
+
+
+
+ {{shop_name}} | {{shop_address}}
+ Bei Fragen: {{shop_email}}
+
+
+
+HTML,
+ ],
+ [
+ 'key' => 'order_shipped',
+ 'label' => 'Versandbestätigung',
+ 'subject' => 'Ihre Bestellung {{order_number}} ist unterwegs',
+ 'variables' => ['order_number','tracking_code','shop_name','shop_email'],
+ 'body' => <<<'HTML'
+
+
+
+
+Ihre Bestellung ist unterwegs!
+Ihre Bestellung {{order_number}} wurde versendet.
+{{tracking_info}}
+Bei Fragen zu Ihrer Lieferung können Sie uns jederzeit kontaktieren.
+
+{{shop_name}} | {{shop_email}}
+
+
+HTML,
+ ],
+ [
+ 'key' => 'payment_received',
+ 'label' => 'Zahlungsbestätigung',
+ 'subject' => 'Zahlung eingegangen – Bestellung {{order_number}}',
+ 'variables' => ['customer_name','order_number','order_total','shop_name','shop_email'],
+ 'body' => <<<'HTML'
+
+
+
+
+Zahlung eingegangen
+Guten Tag {{customer_name}},
+Ihre Zahlung für Bestellung {{order_number}} ({{order_total}} €) ist eingegangen.
+Im Anhang finden Sie Ihre Rechnung als PDF.
+Wir werden Ihre Bestellung schnellstmöglich bearbeiten und versenden.
+
+{{shop_name}} | {{shop_email}}
+
+
+HTML,
+ ],
+ [
+ 'key' => 'widerruf_confirmation',
+ 'label' => 'Widerrufsbestätigung',
+ 'subject' => 'Bestätigung Ihres Widerrufs',
+ 'variables' => ['customer_name','widerruf_name','widerruf_email','widerruf_order_number','widerruf_product','widerruf_date','widerruf_reason','shop_name'],
+ 'body' => <<<'HTML'
+
+
+
+
+Bestätigung Ihres Widerrufs
+Guten Tag {{customer_name}},
+Ihr Widerruf ist bei uns eingegangen. Wir werden ihn schnellstmöglich bearbeiten.
+
+
+ Name {{widerruf_name}}
+ E-Mail {{widerruf_email}}
+ Bestellnummer {{widerruf_order_number}}
+ Produkt {{widerruf_product}}
+ Datum {{widerruf_date}}
+ Begründung {{widerruf_reason}}
+
+
+{{shop_name}}
+
+
+HTML,
+ ],
+ ];
+
+ foreach ($templates as $tpl) {
+ EmailTemplate::updateOrCreate(['key' => $tpl['key']], $tpl);
+ }
+ }
+}
diff --git a/resources/views/admin/email-templates/edit.blade.php b/resources/views/admin/email-templates/edit.blade.php
new file mode 100644
index 0000000..ef31878
--- /dev/null
+++ b/resources/views/admin/email-templates/edit.blade.php
@@ -0,0 +1,48 @@
+@extends('layouts.admin')
+@section('title', 'E-Mail-Template: ' . $template->label)
+@section('content')
+
+
+@if(session('success'))
+ {{ session('success') }}
+@endif
+
+
+@endsection
diff --git a/resources/views/admin/email-templates/index.blade.php b/resources/views/admin/email-templates/index.blade.php
new file mode 100644
index 0000000..de6729f
--- /dev/null
+++ b/resources/views/admin/email-templates/index.blade.php
@@ -0,0 +1,43 @@
+@extends('layouts.admin')
+@section('title', 'E-Mail-Templates')
+@section('content')
+
+
E-Mail-Templates
+
+
+@if(session('success'))
+ {{ session('success') }}
+@endif
+
+@if($templates->isEmpty())
+
+ Keine Templates gefunden. Bitte führen Sie php artisan db:seed --class=EmailTemplateSeeder aus.
+
+@else
+
+
+
+
+ Template
+ Betreff
+ Zuletzt geändert
+
+
+
+
+ @foreach($templates as $tpl)
+
+ {{ $tpl->label }}
+ {{ $tpl->subject }}
+ {{ $tpl->updated_at->format('d.m.Y H:i') }}
+
+ Bearbeiten
+
+
+ @endforeach
+
+
+
+@endif
+@endsection
diff --git a/resources/views/admin/pages/form.blade.php b/resources/views/admin/pages/form.blade.php
index 64f32a3..e44fd25 100644
--- a/resources/views/admin/pages/form.blade.php
+++ b/resources/views/admin/pages/form.blade.php
@@ -152,6 +152,14 @@
class="w-full border rounded px-3 py-2 text-sm" min="0">
Niedrigere Zahl = weiter links
+
+ Seiten-Layout
+
+ @foreach(['default'=>'Standard (zentriert)','sidebar'=>'Mit Sidebar','full'=>'Volle Breite','landing'=>'Landing Page (ohne Header/Footer)'] as $val => $label)
+ layout ?? 'default') === $val ? 'selected' : '' }}>{{ $label }}
+ @endforeach
+
+
diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php
index 31d51d9..cff38d5 100644
--- a/resources/views/layouts/admin.blade.php
+++ b/resources/views/layouts/admin.blade.php
@@ -29,6 +29,7 @@
FAQ
System
Admin-Benutzer
+ E-Mail-Templates
Einstellungen
diff --git a/resources/views/layouts/shop.blade.php b/resources/views/layouts/shop.blade.php
index 60d303a..286e239 100644
--- a/resources/views/layouts/shop.blade.php
+++ b/resources/views/layouts/shop.blade.php
@@ -6,6 +6,33 @@
@yield('title', \App\Models\Setting::get('shop_name', 'Shop'))
@vite(['resources/css/app.css', 'resources/js/app.js'])
+ @php
+ $themes = config('themes');
+ $themeName = \App\Models\Setting::get('theme_name', 'default');
+ $theme = $themes[$themeName] ?? $themes['default'];
+ @endphp
+
@@ -15,7 +42,7 @@
Wir verwenden Cookies, um Ihnen die bestmögliche Nutzererfahrung zu bieten. Mit dem Klick auf „Akzeptieren" stimmen Sie der Verwendung aller Cookies zu. Mehr erfahren
Ablehnen
- Akzeptieren
+ Akzeptieren