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,50 @@
@extends('layouts.admin')
@section('title', 'Attribute')
@section('content')
<div class="grid grid-cols-2 gap-6">
{{-- Neues Attribut --}}
<div class="bg-white rounded shadow p-6">
<h3 class="font-semibold mb-4">Neues Attribut</h3>
<form method="POST" action="{{ route('admin.attributes.store') }}" class="flex gap-2">
@csrf
<input type="text" name="name" placeholder="z.B. Größe, Farbe" class="flex-1 border rounded px-3 py-2 text-sm" required>
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm">Erstellen</button>
</form>
</div>
</div>
<div class="mt-6 grid grid-cols-2 gap-6">
@foreach($attributes as $attr)
<div class="bg-white rounded shadow p-5">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold">{{ $attr->name }}</h3>
<form method="POST" action="{{ route('admin.attributes.destroy', $attr) }}" onsubmit="return confirm('Attribut und alle Werte löschen?')">
@csrf @method('DELETE')
<button class="text-red-500 text-sm hover:underline">Löschen</button>
</form>
</div>
<div class="space-y-1 mb-4">
@foreach($attr->values as $val)
<div class="flex justify-between items-center text-sm py-1 border-b">
<span>{{ $val->value }}</span>
<form method="POST" action="{{ route('admin.attribute-values.destroy', $val) }}">
@csrf @method('DELETE')
<button class="text-red-400 hover:text-red-600 text-xs">✕</button>
</form>
</div>
@endforeach
@if($attr->values->isEmpty())
<p class="text-xs text-gray-400">Keine Werte.</p>
@endif
</div>
<form method="POST" action="{{ route('admin.attributes.values.store', $attr) }}" class="flex gap-2">
@csrf
<input type="text" name="value" placeholder="Neuer Wert" class="flex-1 border rounded px-2 py-1.5 text-sm" required>
<button class="bg-gray-100 px-3 py-1.5 rounded text-sm hover:bg-gray-200">+ Hinzufügen</button>
</form>
</div>
@endforeach
</div>
@endsection
@@ -0,0 +1,29 @@
@extends('layouts.admin')
@section('title', isset($banner) ? 'Banner bearbeiten' : 'Neuer Banner')
@section('content')
<div class="max-w-xl">
<form method="POST" action="{{ isset($banner) ? route('admin.banners.update', $banner) : route('admin.banners.store') }}" enctype="multipart/form-data" class="bg-white rounded shadow p-6 space-y-4">
@csrf @if(isset($banner)) @method('PUT') @endif
<div>
<label class="block text-sm font-medium mb-1">Bild *</label>
@if(isset($banner) && $banner->image)<img src="{{ asset('storage/'.$banner->image) }}" class="h-20 mb-2 rounded">@endif
<input type="file" name="image" accept="image/*" {{ !isset($banner) ? 'required' : '' }} class="text-sm">
</div>
<div><label class="block text-sm font-medium mb-1">Überschrift</label><input type="text" name="headline" value="{{ old('headline', $banner->headline ?? '') }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div><label class="block text-sm font-medium mb-1">Untertext</label><input type="text" name="subtext" value="{{ old('subtext', $banner->subtext ?? '') }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div class="grid grid-cols-2 gap-4">
<div><label class="block text-sm font-medium mb-1">Button-Text</label><input type="text" name="button_label" value="{{ old('button_label', $banner->button_label ?? '') }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div><label class="block text-sm font-medium mb-1">Button-Link</label><input type="text" name="button_link" value="{{ old('button_link', $banner->button_link ?? '') }}" class="w-full border rounded px-3 py-2 text-sm"></div>
</div>
<div class="flex gap-4">
<div><label class="block text-sm font-medium mb-1">Reihenfolge</label><input type="number" name="sort_order" value="{{ old('sort_order', $banner->sort_order ?? 0) }}" class="w-24 border rounded px-3 py-2 text-sm"></div>
<div class="flex items-end pb-2"><label class="flex items-center gap-2 text-sm"><input type="checkbox" name="active" value="1" {{ old('active', $banner->active ?? true) ? 'checked' : '' }}> Aktiv</label></div>
</div>
<div class="flex gap-3 pt-2">
<button type="submit" class="bg-blue-600 text-white px-5 py-2 rounded hover:bg-blue-700 text-sm">Speichern</button>
<a href="{{ route('admin.banners.index') }}" class="px-5 py-2 rounded border text-sm hover:bg-gray-50">Abbrechen</a>
</div>
</form>
</div>
@endsection
@@ -0,0 +1,27 @@
@extends('layouts.admin')
@section('title', 'Banner')
@section('content')
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold">Startseiten-Banner</h2>
<a href="{{ route('admin.banners.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm">+ Neuer Banner</a>
</div>
<div class="grid grid-cols-3 gap-4">
@forelse($banners as $banner)
<div class="bg-white rounded shadow overflow-hidden">
@if($banner->image)<img src="{{ asset('storage/'.$banner->image) }}" class="w-full h-32 object-cover">@endif
<div class="p-4">
<p class="font-medium text-sm">{{ $banner->headline ?: '(kein Titel)' }}</p>
<p class="text-xs text-gray-400">Reihenfolge: {{ $banner->sort_order }}</p>
<div class="flex gap-2 mt-3">
<a href="{{ route('admin.banners.edit', $banner) }}" class="text-blue-600 text-sm hover:underline">Bearbeiten</a>
<form method="POST" action="{{ route('admin.banners.destroy', $banner) }}" onsubmit="return confirm('Löschen?')">
@csrf @method('DELETE')<button class="text-red-600 text-sm hover:underline">Löschen</button>
</form>
</div>
</div>
</div>
@empty
<p class="col-span-3 text-gray-400">Keine Banner.</p>
@endforelse
</div>
@endsection
@@ -0,0 +1,54 @@
@extends('layouts.admin')
@section('title', isset($category) ? 'Kategorie bearbeiten' : 'Neue Kategorie')
@section('content')
<div class="max-w-xl">
<form method="POST" action="{{ isset($category) ? route('admin.categories.update', $category) : route('admin.categories.store') }}" enctype="multipart/form-data" class="bg-white rounded shadow p-6 space-y-4">
@csrf
@if(isset($category)) @method('PUT') @endif
<div>
<label class="block text-sm font-medium mb-1">Name *</label>
<input type="text" name="name" value="{{ old('name', $category->name ?? '') }}" class="w-full border rounded px-3 py-2 text-sm" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">Übergeordnete Kategorie</label>
<select name="parent_id" class="w-full border rounded px-3 py-2 text-sm">
<option value="">— Keine —</option>
@foreach($parents as $p)
<option value="{{ $p->id }}" {{ old('parent_id', $category->parent_id ?? '') == $p->id ? 'selected' : '' }}>{{ $p->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Beschreibung</label>
<textarea name="description" rows="3" class="w-full border rounded px-3 py-2 text-sm">{{ old('description', $category->description ?? '') }}</textarea>
</div>
<div>
<label class="block text-sm font-medium mb-1">Bild</label>
@if(isset($category) && $category->image)
<img src="{{ asset('storage/'.$category->image) }}" class="h-16 mb-2 rounded">
@endif
<input type="file" name="image" accept="image/*" class="text-sm">
</div>
<div class="flex gap-4">
<div class="flex-1">
<label class="block text-sm font-medium mb-1">Reihenfolge</label>
<input type="number" name="sort_order" value="{{ old('sort_order', $category->sort_order ?? 0) }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
<div class="flex items-end gap-2 pb-2">
<input type="checkbox" name="active" id="active" value="1" {{ old('active', $category->active ?? true) ? 'checked' : '' }}>
<label for="active" class="text-sm">Aktiv</label>
</div>
</div>
<div class="flex gap-3 pt-2">
<button type="submit" class="bg-blue-600 text-white px-5 py-2 rounded hover:bg-blue-700 text-sm">Speichern</button>
<a href="{{ route('admin.categories.index') }}" class="px-5 py-2 rounded border text-sm hover:bg-gray-50">Abbrechen</a>
</div>
</form>
</div>
@endsection
@@ -0,0 +1,45 @@
@extends('layouts.admin')
@section('title', 'Kategorien')
@section('content')
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold">Kategorien</h2>
<a href="{{ route('admin.categories.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm">+ Neue Kategorie</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Übergeordnet</th>
<th class="px-4 py-3">Reihenfolge</th>
<th class="px-4 py-3">Aktiv</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($categories as $cat)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium">{{ $cat->name }}</td>
<td class="px-4 py-3 text-gray-500">{{ $cat->parent?->name ?? '—' }}</td>
<td class="px-4 py-3">{{ $cat->sort_order }}</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-xs {{ $cat->active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500' }}">
{{ $cat->active ? 'Ja' : 'Nein' }}
</span>
</td>
<td class="px-4 py-3 text-right space-x-2">
<a href="{{ route('admin.categories.edit', $cat) }}" class="text-blue-600 hover:underline">Bearbeiten</a>
<form method="POST" action="{{ route('admin.categories.destroy', $cat) }}" class="inline" onsubmit="return confirm('Kategorie löschen?')">
@csrf @method('DELETE')
<button class="text-red-600 hover:underline">Löschen</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-8 text-center text-gray-400">Keine Kategorien vorhanden.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection
@@ -0,0 +1,104 @@
@extends('layouts.admin')
@section('title', 'Kunden')
@section('content')
@php $pendingCount = \App\Models\User::where('role','customer')->where('approved', false)->count(); @endphp
@if($pendingCount > 0)
<div class="mb-4 bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 flex items-center gap-3 text-sm text-amber-800">
<svg class="w-5 h-5 shrink-0 text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
<span><strong>{{ $pendingCount }} Kunde{{ $pendingCount > 1 ? 'n' : '' }}</strong> warte{{ $pendingCount > 1 ? 'n' : 't' }} auf Freischaltung.</span>
</div>
@endif
<div class="flex items-center gap-4 mb-6">
<h2 class="text-lg font-semibold">Kunden</h2>
<form method="GET" class="flex gap-2 ml-2">
<input type="text" name="q" value="{{ request('q') }}"
placeholder="Name oder E-Mail …"
class="border rounded px-3 py-1.5 text-sm w-64">
<button class="bg-gray-200 px-3 py-1.5 rounded text-sm hover:bg-gray-300">Suchen</button>
@if(request('q'))
<a href="{{ route('admin.customers.index') }}" class="px-3 py-1.5 rounded text-sm text-gray-500 hover:bg-gray-100">✕ Zurücksetzen</a>
@endif
</form>
<span class="ml-auto text-sm text-gray-400">{{ $customers->total() }} Kunden gesamt</span>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left border-b">
<tr>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">E-Mail</th>
<th class="px-4 py-3">Registriert</th>
<th class="px-4 py-3 text-center">Bestellungen</th>
<th class="px-4 py-3">Verifiziert</th>
<th class="px-4 py-3 text-center">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($customers as $customer)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium">
<a href="{{ route('admin.customers.show', $customer) }}" class="hover:text-blue-600">
{{ $customer->name }}
</a>
</td>
<td class="px-4 py-3 text-gray-600">{{ $customer->email }}</td>
<td class="px-4 py-3 text-gray-400">{{ $customer->created_at->format('d.m.Y') }}</td>
<td class="px-4 py-3 text-center">
@if($customer->orders_count > 0)
<a href="{{ route('admin.customers.show', $customer) }}"
class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-blue-100 text-blue-700 font-semibold text-xs hover:bg-blue-200">
{{ $customer->orders_count }}
</a>
@else
<span class="text-gray-300">—</span>
@endif
</td>
<td class="px-4 py-3">
@if($customer->email_verified_at)
<span class="text-green-600 text-xs font-medium">✓ Ja</span>
@else
<span class="text-orange-500 text-xs">Ausstehend</span>
@endif
</td>
<td class="px-4 py-3 text-center">
<form method="POST" action="{{ route('admin.customers.toggle-approval', $customer) }}">
@csrf
<button type="submit"
class="text-xs px-2.5 py-1 rounded-full font-medium transition
{{ $customer->approved
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-red-100 text-red-700 hover:bg-red-200' }}">
{{ $customer->approved ? '✓ Aktiv' : '✗ Gesperrt' }}
</button>
</form>
</td>
<td class="px-4 py-3 text-right space-x-3">
<a href="{{ route('admin.customers.show', $customer) }}"
class="text-blue-600 hover:underline text-xs">Details</a>
<form method="POST" action="{{ route('admin.customers.destroy', $customer) }}"
class="inline" onsubmit="return confirm('Kunde und alle Daten löschen?')">
@csrf @method('DELETE')
<button class="text-red-500 hover:underline text-xs">Löschen</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-12 text-center text-gray-400">
@if(request('q'))
Keine Kunden für „{{ request('q') }}" gefunden.
@else
Noch keine registrierten Kunden.
@endif
</td>
</tr>
@endforelse
</tbody>
</table>
<div class="px-4 py-3 border-t">{{ $customers->withQueryString()->links() }}</div>
</div>
@endsection
@@ -0,0 +1,175 @@
@extends('layouts.admin')
@section('title', $customer->name)
@section('content')
<div class="mb-5">
<a href="{{ route('admin.customers.index') }}" class="text-sm text-gray-500 hover:text-blue-600">← Alle Kunden</a>
</div>
<div class="grid grid-cols-3 gap-6">
{{-- Linke Spalte: Kundendaten --}}
<div class="space-y-4">
{{-- Stammdaten --}}
<div class="bg-white rounded shadow p-5">
<div class="flex items-center gap-3 mb-4">
<div class="w-12 h-12 rounded-full bg-blue-600 flex items-center justify-center text-white font-bold text-lg">
{{ mb_substr($customer->name, 0, 1) }}
</div>
<div>
<h2 class="font-semibold">{{ $customer->name }}</h2>
<p class="text-xs text-gray-400">Kunde seit {{ $customer->created_at->format('d.m.Y') }}</p>
</div>
</div>
<dl class="space-y-2 text-sm">
<div>
<dt class="text-xs text-gray-400">E-Mail</dt>
<dd><a href="mailto:{{ $customer->email }}" class="text-blue-600 hover:underline">{{ $customer->email }}</a></dd>
</div>
<div>
<dt class="text-xs text-gray-400">E-Mail verifiziert</dt>
<dd>
@if($customer->email_verified_at)
<span class="text-green-600">✓ {{ $customer->email_verified_at->format('d.m.Y') }}</span>
@else
<span class="text-orange-500">Noch nicht verifiziert</span>
@endif
</dd>
</div>
<div>
<dt class="text-xs text-gray-400">Konto-Status</dt>
<dd>
@if($customer->approved)
<span class="text-green-600 font-medium">✓ Freigeschaltet</span>
@else
<span class="text-red-500 font-medium">✗ Gesperrt / Ausstehend</span>
@endif
</dd>
</div>
</dl>
<div class="mt-4 pt-4 border-t space-y-2">
<form method="POST" action="{{ route('admin.customers.toggle-approval', $customer) }}">
@csrf
<button type="submit"
class="w-full py-2 rounded text-sm font-medium transition
{{ $customer->approved
? 'bg-red-50 text-red-600 hover:bg-red-100'
: 'bg-green-600 text-white hover:bg-green-700' }}">
{{ $customer->approved ? 'Konto sperren' : 'Konto freischalten' }}
</button>
</form>
<form method="POST" action="{{ route('admin.customers.destroy', $customer) }}"
onsubmit="return confirm('Kunde und alle Daten unwiderruflich löschen?')">
@csrf @method('DELETE')
<button class="text-red-500 text-xs hover:underline w-full text-left">Kunde löschen</button>
</form>
</div>
</div>
{{-- Kennzahlen --}}
<div class="bg-white rounded shadow p-5">
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Kennzahlen</h3>
<dl class="space-y-3 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500">Bestellungen gesamt</dt>
<dd class="font-semibold">{{ $stats['total_orders'] }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Umsatz gesamt</dt>
<dd class="font-semibold text-green-700">{{ number_format($stats['total_revenue'], 2, ',', '.') }} €</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">Offene Bestellungen</dt>
<dd class="{{ $stats['open_orders'] > 0 ? 'font-semibold text-orange-500' : 'text-gray-400' }}">
{{ $stats['open_orders'] }}
</dd>
</div>
@if($stats['last_order'])
<div class="flex justify-between">
<dt class="text-gray-500">Letzte Bestellung</dt>
<dd class="text-gray-600">{{ $stats['last_order']->created_at->format('d.m.Y') }}</dd>
</div>
@endif
</dl>
</div>
{{-- Adressen --}}
@if($customer->addresses->count())
<div class="bg-white rounded shadow p-5">
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Gespeicherte Adressen</h3>
<div class="space-y-3">
@foreach($customer->addresses as $addr)
<div class="text-sm border-b pb-3 last:border-0 last:pb-0">
@if($addr->is_default)
<span class="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded mb-1 inline-block">Standard</span>
@endif
<p class="font-medium">{{ $addr->first_name }} {{ $addr->last_name }}</p>
@if($addr->company)<p class="text-gray-500 text-xs">{{ $addr->company }}</p>@endif
<p class="text-gray-600">{{ $addr->street }}, {{ $addr->zip }} {{ $addr->city }}</p>
@if($addr->phone)<p class="text-gray-400 text-xs">{{ $addr->phone }}</p>@endif
</div>
@endforeach
</div>
</div>
@endif
</div>
{{-- Rechte Spalte: Bestellungen --}}
<div class="col-span-2">
<h3 class="font-semibold mb-3">Bestellhistorie</h3>
@if($orders->count())
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b text-left">
<tr>
<th class="px-4 py-3">Bestellnr.</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Zahlungsart</th>
<th class="px-4 py-3 text-right">Betrag</th>
<th class="px-4 py-3">Datum</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y">
@foreach($orders as $order)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono font-medium">{{ $order->order_number }}</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-xs
{{ match($order->status) {
'new' => 'bg-yellow-100 text-yellow-700',
'payment_received' => 'bg-blue-100 text-blue-700',
'processing' => 'bg-purple-100 text-purple-700',
'shipped' => 'bg-indigo-100 text-indigo-700',
'completed' => 'bg-green-100 text-green-700',
'cancelled' => 'bg-red-100 text-red-700',
default => 'bg-gray-100 text-gray-600',
} }}">
{{ $order->status_label }}
</span>
</td>
<td class="px-4 py-3 text-gray-500">{{ $order->payment_label }}</td>
<td class="px-4 py-3 text-right font-medium">{{ number_format($order->total, 2, ',', '.') }} €</td>
<td class="px-4 py-3 text-gray-400">{{ $order->created_at->format('d.m.Y') }}</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('admin.orders.show', $order) }}"
class="text-blue-600 hover:underline text-xs">Details</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="px-4 py-3 border-t">{{ $orders->links() }}</div>
</div>
@else
<div class="bg-white rounded shadow p-10 text-center text-gray-400">
Dieser Kunde hat noch keine Bestellungen aufgegeben.
</div>
@endif
</div>
</div>
@endsection
@@ -0,0 +1,80 @@
@extends('layouts.admin')
@section('title', 'Dashboard')
@section('content')
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-lg shadow p-5">
<p class="text-xs text-gray-400 mb-1">Umsatz heute</p>
<p class="text-2xl font-bold text-green-600">{{ number_format($todayRevenue, 2, ',', '.') }} €</p>
</div>
<div class="bg-white rounded-lg shadow p-5">
<p class="text-xs text-gray-400 mb-1">Umsatz diesen Monat</p>
<p class="text-2xl font-bold text-blue-600">{{ number_format($monthRevenue, 2, ',', '.') }} €</p>
</div>
<div class="bg-white rounded-lg shadow p-5">
<p class="text-xs text-gray-400 mb-1">Offene Bestellungen</p>
<p class="text-2xl font-bold {{ $openOrders > 0 ? 'text-orange-500' : 'text-gray-400' }}">{{ $openOrders }}</p>
</div>
<div class="bg-white rounded-lg shadow p-5">
<p class="text-xs text-gray-400 mb-1">Lagerwarnung</p>
<p class="text-2xl font-bold {{ $lowStock->count() > 0 ? 'text-red-500' : 'text-gray-400' }}">{{ $lowStock->count() }}</p>
</div>
</div>
@if($lowStock->count())
<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-8">
<h3 class="font-semibold text-red-700 mb-2">Lagerbestand niedrig</h3>
<ul class="text-sm space-y-1">
@foreach($lowStock as $v)
<li class="flex justify-between">
<span>{{ $v->product->name }} @if($v->label)({{ $v->label }})@endif</span>
<span class="font-bold text-red-600">{{ $v->stock }} Stück</span>
</li>
@endforeach
</ul>
</div>
@endif
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="px-5 py-3 border-b font-semibold">Letzte Bestellungen</div>
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left">Bestellnr.</th>
<th class="px-4 py-2 text-left">Kunde</th>
<th class="px-4 py-2 text-left">Status</th>
<th class="px-4 py-2 text-right">Betrag</th>
<th class="px-4 py-2 text-left">Datum</th>
<th class="px-4 py-2"></th>
</tr>
</thead>
<tbody class="divide-y">
@foreach($recentOrders as $o)
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 font-mono font-medium">{{ $o->order_number }}</td>
<td class="px-4 py-2">{{ $o->billingAddress()?->full_name ?? $o->guest_email }}</td>
<td class="px-4 py-2">
<span class="px-2 py-0.5 rounded text-xs
{{ match($o->status) {
'new' => 'bg-yellow-100 text-yellow-700',
'payment_received' => 'bg-blue-100 text-blue-700',
'processing' => 'bg-purple-100 text-purple-700',
'shipped' => 'bg-indigo-100 text-indigo-700',
'completed' => 'bg-green-100 text-green-700',
'cancelled' => 'bg-red-100 text-red-700',
default => 'bg-gray-100 text-gray-600',
} }}">
{{ $o->status_label }}
</span>
</td>
<td class="px-4 py-2 text-right">{{ number_format($o->total, 2, ',', '.') }} €</td>
<td class="px-4 py-2 text-gray-400">{{ $o->created_at->format('d.m.Y H:i') }}</td>
<td class="px-4 py-2"><a href="{{ route('admin.orders.show', $o) }}" class="text-blue-600 hover:underline text-xs">Details</a></td>
</tr>
@endforeach
</tbody>
</table>
<div class="px-4 py-3 border-t text-right">
<a href="{{ route('admin.orders.index') }}" class="text-sm text-blue-600 hover:underline">Alle Bestellungen →</a>
</div>
</div>
@endsection
@@ -0,0 +1,47 @@
@extends('layouts.admin')
@section('title', 'FAQ')
@section('content')
<div class="grid grid-cols-3 gap-6">
<div>
<div class="bg-white rounded shadow p-5 mb-4">
<h3 class="font-semibold mb-3">Neue Gruppe</h3>
<form method="POST" action="{{ route('admin.faqs.groups.store') }}" class="space-y-2">
@csrf
<input type="text" name="name" placeholder="Gruppenname" class="w-full border rounded px-3 py-2 text-sm" required>
<button class="w-full bg-blue-600 text-white py-2 rounded text-sm hover:bg-blue-700">Erstellen</button>
</form>
</div>
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-3">Neue Frage</h3>
<form method="POST" action="{{ route('admin.faqs.store') }}" class="space-y-2">
@csrf
<select name="faq_group_id" class="w-full border rounded px-3 py-2 text-sm">
<option value="">Ohne Gruppe</option>
@foreach($groups as $g)<option value="{{ $g->id }}">{{ $g->name }}</option>@endforeach
</select>
<input type="text" name="question" placeholder="Frage" class="w-full border rounded px-3 py-2 text-sm" required>
<textarea name="answer" placeholder="Antwort" rows="3" class="w-full border rounded px-3 py-2 text-sm" required></textarea>
<button class="w-full bg-green-600 text-white py-2 rounded text-sm hover:bg-green-700">Hinzufügen</button>
</form>
</div>
</div>
<div class="col-span-2 space-y-4">
@foreach($groups as $group)
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-3 text-blue-700">{{ $group->name }}</h3>
@foreach($group->faqs as $faq)
<div class="border-b pb-3 mb-3 last:border-0">
<p class="font-medium text-sm">{{ $faq->question }}</p>
<p class="text-sm text-gray-500 mt-1">{{ Str::limit($faq->answer, 100) }}</p>
<form method="POST" action="{{ route('admin.faqs.destroy', $faq) }}" class="mt-1" onsubmit="return confirm('Löschen?')">
@csrf @method('DELETE')
<button class="text-red-400 text-xs hover:text-red-600">Löschen</button>
</form>
</div>
@endforeach
</div>
@endforeach
</div>
</div>
@endsection
@@ -0,0 +1,69 @@
@extends('layouts.admin')
@section('title', 'Bestellungen')
@section('content')
<div class="flex items-center gap-3 mb-6">
<h2 class="text-lg font-semibold">Bestellungen</h2>
<form method="GET" class="flex gap-2 ml-4">
<input type="text" name="q" value="{{ request('q') }}" placeholder="Bestellnr." class="border rounded px-3 py-1.5 text-sm w-36">
<select name="status" class="border rounded px-2 py-1.5 text-sm">
<option value="">Alle Status</option>
@foreach(\App\Models\Order::$statusLabels as $key => $label)
<option value="{{ $key }}" {{ request('status') === $key ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
<select name="payment" class="border rounded px-2 py-1.5 text-sm">
<option value="">Alle Zahlungsarten</option>
@foreach(\App\Models\Order::$paymentLabels as $key => $label)
<option value="{{ $key }}" {{ request('payment') === $key ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
<button class="bg-gray-200 px-3 py-1.5 rounded text-sm hover:bg-gray-300">Filtern</button>
</form>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-3">Bestellnr.</th>
<th class="px-4 py-3">Kunde</th>
<th class="px-4 py-3">Zahlungsart</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3 text-right">Betrag</th>
<th class="px-4 py-3">Datum</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($orders as $o)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono font-medium">{{ $o->order_number }}</td>
<td class="px-4 py-3">{{ $o->billingAddress()?->full_name ?? $o->guest_email }}</td>
<td class="px-4 py-3">{{ $o->payment_label }}</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-xs
{{ match($o->status) {
'new' => 'bg-yellow-100 text-yellow-700',
'payment_received' => 'bg-blue-100 text-blue-700',
'shipped' => 'bg-indigo-100 text-indigo-700',
'completed' => 'bg-green-100 text-green-700',
'cancelled' => 'bg-red-100 text-red-700',
default => 'bg-gray-100 text-gray-600',
} }}">
{{ $o->status_label }}
</span>
</td>
<td class="px-4 py-3 text-right">{{ number_format($o->total, 2, ',', '.') }} €</td>
<td class="px-4 py-3 text-gray-400">{{ $o->created_at->format('d.m.Y') }}</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('admin.orders.show', $o) }}" class="text-blue-600 hover:underline">Details</a>
</td>
</tr>
@empty
<tr><td colspan="7" class="px-4 py-8 text-center text-gray-400">Keine Bestellungen gefunden.</td></tr>
@endforelse
</tbody>
</table>
<div class="px-4 py-3 border-t">{{ $orders->withQueryString()->links() }}</div>
</div>
@endsection
+113
View File
@@ -0,0 +1,113 @@
@extends('layouts.admin')
@section('title', 'Bestellung ' . $order->order_number)
@section('content')
<div class="flex items-center justify-between mb-6">
<div>
<a href="{{ route('admin.orders.index') }}" class="text-sm text-gray-500 hover:text-blue-600">← Alle Bestellungen</a>
<h2 class="text-xl font-bold mt-1">{{ $order->order_number }}</h2>
<p class="text-sm text-gray-400">{{ $order->created_at->format('d.m.Y H:i') }} Uhr</p>
</div>
<div class="flex gap-2">
<a href="{{ route('admin.orders.pdf', $order) }}" class="bg-gray-100 px-4 py-2 rounded text-sm hover:bg-gray-200">PDF-Rechnung herunterladen</a>
<form method="POST" action="{{ route('admin.orders.invoice', $order) }}">
@csrf
<button class="bg-blue-100 text-blue-700 px-4 py-2 rounded text-sm hover:bg-blue-200">Rechnung erneut mailen</button>
</form>
</div>
</div>
<div class="grid grid-cols-3 gap-6">
<div class="col-span-2 space-y-6">
{{-- Artikel --}}
<div class="bg-white rounded shadow overflow-hidden">
<div class="px-5 py-3 border-b font-semibold">Bestellpositionen</div>
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">Artikel</th><th class="px-4 py-2 text-center">Menge</th><th class="px-4 py-2 text-right">Einzelpreis</th><th class="px-4 py-2 text-right">MwSt.</th><th class="px-4 py-2 text-right">Gesamt</th></tr></thead>
<tbody class="divide-y">
@foreach($order->items as $item)
<tr>
<td class="px-4 py-2">{{ $item->product_name }}@if($item->variant_label) <span class="text-gray-400">({{ $item->variant_label }})</span>@endif</td>
<td class="px-4 py-2 text-center">{{ $item->quantity }}</td>
<td class="px-4 py-2 text-right">{{ number_format($item->unit_price, 2, ',', '.') }} €</td>
<td class="px-4 py-2 text-right">{{ number_format($item->tax_rate, 0) }} %</td>
<td class="px-4 py-2 text-right font-medium">{{ number_format($item->line_total, 2, ',', '.') }} €</td>
</tr>
@endforeach
</tbody>
<tfoot class="border-t">
<tr><td colspan="4" class="px-4 py-2 text-right text-gray-500">Zwischensumme</td><td class="px-4 py-2 text-right">{{ number_format($order->subtotal, 2, ',', '.') }} €</td></tr>
<tr><td colspan="4" class="px-4 py-2 text-right text-gray-500">MwSt. gesamt</td><td class="px-4 py-2 text-right">{{ number_format($order->tax_total, 2, ',', '.') }} €</td></tr>
<tr><td colspan="4" class="px-4 py-2 text-right text-gray-500">Versand</td><td class="px-4 py-2 text-right">{{ number_format($order->shipping_cost, 2, ',', '.') }} €</td></tr>
<tr class="font-bold"><td colspan="4" class="px-4 py-2 text-right">Gesamt</td><td class="px-4 py-2 text-right text-lg">{{ number_format($order->total, 2, ',', '.') }} €</td></tr>
</tfoot>
</table>
</div>
{{-- Adressen --}}
<div class="grid grid-cols-2 gap-4">
@foreach(['billing' => 'Rechnungsadresse', 'shipping' => 'Lieferadresse'] as $type => $label)
@php $addr = $order->addresses->where('type', $type)->first(); @endphp
@if($addr)
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-2">{{ $label }}</h3>
<p class="text-sm">
{{ $addr->full_name }}<br>
@if($addr->company){{ $addr->company }}<br>@endif
{{ $addr->street }}<br>
{{ $addr->zip }} {{ $addr->city }}<br>
{{ $addr->country }}
@if($addr->phone)<br>{{ $addr->phone }}@endif
</p>
</div>
@endif
@endforeach
</div>
</div>
{{-- Status & Aktionen --}}
<div class="space-y-4">
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-3">Bestelldetails</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between"><dt class="text-gray-500">Zahlungsart</dt><dd>{{ $order->payment_label }}</dd></div>
<div class="flex justify-between"><dt class="text-gray-500">Status</dt>
<dd><span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-700">{{ $order->status_label }}</span></dd>
</div>
<div class="flex justify-between"><dt class="text-gray-500">Zahlung</dt><dd>{{ $order->paid_at?->format('d.m.Y') ?? '—' }}</dd></div>
@if($order->tracking_code)<div class="flex justify-between"><dt class="text-gray-500">Tracking</dt><dd class="font-mono">{{ $order->tracking_code }}</dd></div>@endif
</dl>
</div>
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-3">Status ändern</h3>
<form method="POST" action="{{ route('admin.orders.status', $order) }}">
@csrf @method('PUT')
<div class="mb-3">
<select name="status" class="w-full border rounded px-3 py-2 text-sm">
@foreach(\App\Models\Order::$statusLabels as $key => $label)
<option value="{{ $key }}" {{ $order->status === $key ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label class="block text-xs text-gray-500 mb-1">Trackingcode (optional)</label>
<input type="text" name="tracking_code" value="{{ $order->tracking_code }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
<button class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 text-sm">Status speichern</button>
</form>
</div>
@if($order->user)
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-2">Kunde</h3>
<p class="text-sm">{{ $order->user->name }}<br>{{ $order->user->email }}</p>
</div>
@elseif($order->guest_email)
<div class="bg-white rounded shadow p-5">
<h3 class="font-semibold mb-2">Gast</h3>
<p class="text-sm">{{ $order->guest_email }}</p>
</div>
@endif
</div>
</div>
@endsection
+252
View File
@@ -0,0 +1,252 @@
@extends('layouts.admin')
@section('title', isset($page) ? 'Seite bearbeiten' : 'Neue Seite')
@push('head')
{{-- Quill WYSIWYG --}}
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
<style>
.ql-container { font-size: 14px; min-height: 320px; border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; }
.ql-toolbar { border-top-left-radius: 6px; border-top-right-radius: 6px; background: #f9fafb; }
.ql-editor { min-height: 320px; line-height: 1.7; }
.ql-editor h1 { font-size: 1.8em; font-weight: 700; margin-bottom: .5em; }
.ql-editor h2 { font-size: 1.4em; font-weight: 600; margin-bottom: .4em; }
.ql-editor h3 { font-size: 1.15em; font-weight: 600; margin-bottom: .3em; }
.ql-editor a { color: #2563eb; text-decoration: underline; }
.ql-editor blockquote { border-left: 4px solid #d1d5db; padding-left: 1em; color: #6b7280; font-style: italic; }
.ql-editor pre { background: #1e293b; color: #e2e8f0; padding: 1em; border-radius: 6px; font-family: monospace; overflow-x: auto; }
.ql-editor ul, .ql-editor ol { padding-left: 1.5em; }
.ql-editor img { max-width: 100%; border-radius: 4px; }
</style>
@endpush
@section('content')
<div class="max-w-4xl">
<form id="page-form"
method="POST"
action="{{ isset($page) ? route('admin.pages.update', $page) : route('admin.pages.store') }}"
enctype="multipart/form-data"
class="space-y-4">
@csrf @if(isset($page)) @method('PUT') @endif
{{-- Metadaten --}}
<div class="bg-white rounded shadow p-6 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Titel *</label>
<input type="text" name="title" id="title-input"
value="{{ old('title', $page->title ?? '') }}"
class="w-full border rounded px-3 py-2 text-sm" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">
Slug (URL)
<span class="text-xs text-gray-400 font-normal">– automatisch aus Titel</span>
</label>
<div class="flex items-center border rounded overflow-hidden">
<span class="px-2 py-2 bg-gray-50 text-gray-400 text-xs border-r select-none">/seite/</span>
<input type="text" name="slug" id="slug-input"
value="{{ old('slug', $page->slug ?? '') }}"
class="flex-1 px-3 py-2 text-sm font-mono focus:outline-none"
placeholder="wird-automatisch-generiert">
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Typ</label>
<select name="type" class="w-full border rounded px-3 py-2 text-sm">
<option value="static" {{ old('type', $page->type ?? 'static') === 'static' ? 'selected' : '' }}>Statische Seite (Impressum, AGB, …)</option>
<option value="blog" {{ old('type', $page->type ?? '') === 'blog' ? 'selected' : '' }}>Blog-Beitrag</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Titelbild</label>
@if(isset($page) && $page->image)
<img src="{{ asset('storage/'.$page->image) }}" class="h-10 mb-1 rounded">
@endif
<input type="file" name="image" accept="image/*" class="text-sm">
</div>
</div>
</div>
{{-- WYSIWYG Editor --}}
<div class="bg-white rounded shadow overflow-hidden">
<div class="px-5 py-3 border-b flex items-center justify-between">
<span class="text-sm font-semibold text-gray-700">Inhalt</span>
<button type="button" id="toggle-source"
class="text-xs px-3 py-1 border rounded hover:bg-gray-50 text-gray-500">
HTML-Quellcode
</button>
</div>
{{-- Quill Editor --}}
<div id="quill-editor" class="border-0">
{!! old('content', $page->content ?? '') !!}
</div>
{{-- Roher HTML-Quellcode (Toggle) --}}
<textarea id="source-editor" name="_content_source" rows="16"
class="hidden w-full px-4 py-3 text-sm font-mono border-0 focus:outline-none resize-none"
style="min-height: 320px">{{ old('content', $page->content ?? '') }}</textarea>
{{-- Verstecktes Feld das beim Submit gefüllt wird --}}
<input type="hidden" name="content" id="content-hidden"
value="{{ old('content', $page->content ?? '') }}">
</div>
{{-- SEO & Optionen --}}
<div class="bg-white rounded shadow p-6 space-y-4">
<h3 class="text-sm font-semibold text-gray-700">SEO & Einstellungen</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Meta-Titel <span class="text-xs text-gray-400 font-normal">(Browser-Tab)</span></label>
<input type="text" name="meta_title"
value="{{ old('meta_title', $page->meta_title ?? '') }}"
class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">Meta-Beschreibung <span class="text-xs text-gray-400 font-normal">(Suchergebnisse)</span></label>
<input type="text" name="meta_description"
value="{{ old('meta_description', $page->meta_description ?? '') }}"
class="w-full border rounded px-3 py-2 text-sm"
maxlength="160">
</div>
<div>
<label class="block text-sm font-medium mb-1">Veröffentlichungsdatum</label>
<input type="date" name="published_at"
value="{{ old('published_at', isset($page->published_at) ? $page->published_at->format('Y-m-d') : '') }}"
class="w-full border rounded px-3 py-2 text-sm">
</div>
<div class="flex items-end pb-2">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" name="published" value="1"
{{ old('published', $page->published ?? true) ? 'checked' : '' }}
class="w-4 h-4 accent-blue-600">
<span>Veröffentlicht</span>
</label>
</div>
</div>
<hr class="border-gray-100">
<h3 class="text-sm font-semibold text-gray-700">Navigation</h3>
<div class="grid grid-cols-3 gap-4">
<label class="flex items-start gap-2 text-sm cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input type="checkbox" name="show_in_footer" value="1"
{{ old('show_in_footer', $page->show_in_footer ?? true) ? 'checked' : '' }}
class="mt-0.5 accent-blue-600">
<div>
<span class="font-medium">Footer</span>
<p class="text-xs text-gray-400 mt-0.5">Link in der Fußzeile des Shops</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
<input type="checkbox" name="show_in_nav" value="1"
{{ old('show_in_nav', $page->show_in_nav ?? false) ? 'checked' : '' }}
class="mt-0.5 accent-blue-600">
<div>
<span class="font-medium">Hauptmenü</span>
<p class="text-xs text-gray-400 mt-0.5">Link in der Top-Navigation</p>
</div>
</label>
<div>
<label class="block text-sm font-medium mb-1">Reihenfolge im Menü</label>
<input type="number" name="nav_sort" value="{{ old('nav_sort', $page->nav_sort ?? 0) }}"
class="w-full border rounded px-3 py-2 text-sm" min="0">
<p class="text-xs text-gray-400 mt-1">Niedrigere Zahl = weiter links</p>
</div>
</div>
</div>
@if($errors->any())
<div class="bg-red-50 border border-red-200 rounded px-4 py-3 text-sm text-red-700">
@foreach($errors->all() as $e)<p>• {{ $e }}</p>@endforeach
</div>
@endif
<div class="flex gap-3 pb-8">
<button type="submit" class="bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700 text-sm font-medium">
Speichern
</button>
<a href="{{ route('admin.pages.index') }}"
class="px-6 py-2.5 rounded-lg border text-sm hover:bg-gray-50">
Abbrechen
</a>
</div>
</form>
</div>
<script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
<script>
// ── Quill initialisieren ────────────────────────────────────────────────────
const quill = new Quill('#quill-editor', {
theme: 'snow',
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
[{ align: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
['blockquote', 'code-block'],
['link', 'image'],
['clean']
]
},
placeholder: 'Seiteninhalt hier eingeben …',
});
// ── Slug automatisch aus Titel generieren ───────────────────────────────────
const titleInput = document.getElementById('title-input');
const slugInput = document.getElementById('slug-input');
let slugManuallyEdited = slugInput.value.length > 0;
titleInput.addEventListener('input', () => {
if (slugManuallyEdited) return;
slugInput.value = titleInput.value
.toLowerCase()
.replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
});
slugInput.addEventListener('input', () => { slugManuallyEdited = true; });
// ── HTML-Quellcode-Toggle ───────────────────────────────────────────────────
const toggleBtn = document.getElementById('toggle-source');
const sourceEditor = document.getElementById('source-editor');
const quillWrapper = document.getElementById('quill-editor');
let sourceMode = false;
toggleBtn.addEventListener('click', () => {
sourceMode = !sourceMode;
if (sourceMode) {
// Quill → HTML
sourceEditor.value = quill.root.innerHTML === '<p><br></p>' ? '' : quill.root.innerHTML;
quillWrapper.style.display = 'none';
sourceEditor.classList.remove('hidden');
toggleBtn.textContent = 'WYSIWYG';
toggleBtn.classList.add('bg-yellow-50', 'text-yellow-700', 'border-yellow-300');
} else {
// HTML → Quill
quill.root.innerHTML = sourceEditor.value;
sourceEditor.classList.add('hidden');
quillWrapper.style.display = '';
toggleBtn.textContent = 'HTML-Quellcode';
toggleBtn.classList.remove('bg-yellow-50', 'text-yellow-700', 'border-yellow-300');
}
});
// ── Beim Absenden: Quill-HTML in hidden field schreiben ─────────────────────
document.getElementById('page-form').addEventListener('submit', () => {
const html = sourceMode
? sourceEditor.value
: (quill.root.innerHTML === '<p><br></p>' ? '' : quill.root.innerHTML);
document.getElementById('content-hidden').value = html;
});
</script>
@push('styles')
<style>
/* Quill-Bilder klickbar im Editor */
.ql-editor img { cursor: pointer; }
</style>
@endpush
@endsection
@@ -0,0 +1,29 @@
@extends('layouts.admin')
@section('title', 'Seiten')
@section('content')
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold">Seiten</h2>
<a href="{{ route('admin.pages.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm">+ Neue Seite</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left"><tr><th class="px-4 py-3">Titel</th><th class="px-4 py-3">Slug</th><th class="px-4 py-3">Typ</th><th class="px-4 py-3">Veröffentlicht</th><th class="px-4 py-3"></th></tr></thead>
<tbody class="divide-y">
@forelse($pages as $page)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium">{{ $page->title }}</td>
<td class="px-4 py-3 font-mono text-gray-500">/seite/{{ $page->slug }}</td>
<td class="px-4 py-3"><span class="px-2 py-0.5 rounded text-xs bg-gray-100">{{ $page->type }}</span></td>
<td class="px-4 py-3"><span class="px-2 py-0.5 rounded text-xs {{ $page->published ? 'bg-green-100 text-green-700' : 'bg-gray-100' }}">{{ $page->published ? 'Ja' : 'Nein' }}</span></td>
<td class="px-4 py-3 text-right space-x-2">
<a href="{{ route('admin.pages.edit', $page) }}" class="text-blue-600 hover:underline">Bearbeiten</a>
<form method="POST" action="{{ route('admin.pages.destroy', $page) }}" class="inline" onsubmit="return confirm('Löschen?')">@csrf @method('DELETE')<button class="text-red-600 hover:underline">Löschen</button></form>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-8 text-center text-gray-400">Keine Seiten.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection
@@ -0,0 +1,190 @@
@extends('layouts.admin')
@section('title', isset($product) ? 'Artikel bearbeiten' : 'Neuer Artikel')
@section('content')
<form method="POST" action="{{ isset($product) ? route('admin.products.update', $product) : route('admin.products.store') }}" enctype="multipart/form-data" x-data="productForm()">
@csrf
@if(isset($product)) @method('PUT') @endif
<div class="grid grid-cols-3 gap-6">
{{-- Linke Spalte: Basisdaten --}}
<div class="col-span-2 space-y-4">
<div class="bg-white rounded shadow p-6 space-y-4">
<h3 class="font-semibold text-gray-700">Basisdaten</h3>
<div>
<label class="block text-sm font-medium mb-1">Name *</label>
<input type="text" name="name" value="{{ old('name', $product->name ?? '') }}" class="w-full border rounded px-3 py-2 text-sm" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">Beschreibung</label>
<textarea name="description" rows="6" class="w-full border rounded px-3 py-2 text-sm">{{ old('description', $product->description ?? '') }}</textarea>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Bruttopreis (€) *</label>
<input type="number" name="base_price" step="0.01" min="0" value="{{ old('base_price', $product->base_price ?? '') }}" class="w-full border rounded px-3 py-2 text-sm" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">MwSt.-Satz *</label>
<select name="tax_rate" class="w-full border rounded px-3 py-2 text-sm">
<option value="19" {{ old('tax_rate', $product->tax_rate ?? 19) == 19 ? 'selected' : '' }}>19 %</option>
<option value="7" {{ old('tax_rate', $product->tax_rate ?? 19) == 7 ? 'selected' : '' }}>7 %</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Gewicht (kg)</label>
<input type="number" name="weight" step="0.001" min="0" value="{{ old('weight', $product->weight ?? '') }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
</div>
</div>
{{-- Varianten --}}
<div class="bg-white rounded shadow p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold text-gray-700">Varianten</h3>
<button type="button" @click="addVariant()" class="text-sm bg-gray-100 px-3 py-1.5 rounded hover:bg-gray-200">+ Variante hinzufügen</button>
</div>
<template x-if="variants.length === 0">
<p class="text-sm text-gray-400">Keine Varianten — der Artikel wird mit dem Basispreis verkauft.</p>
</template>
<div class="space-y-4">
<template x-for="(v, i) in variants" :key="i">
<div class="border rounded p-4 space-y-3">
<input type="hidden" :name="'variants[' + i + '][id]'" :value="v.id">
<div class="grid grid-cols-4 gap-3">
<div>
<label class="block text-xs font-medium mb-1">SKU *</label>
<input type="text" :name="'variants[' + i + '][sku]'" x-model="v.sku" class="w-full border rounded px-2 py-1.5 text-sm" required>
</div>
<div>
<label class="block text-xs font-medium mb-1">Preis (€, leer = Basispreis)</label>
<input type="number" step="0.01" :name="'variants[' + i + '][price_override]'" x-model="v.price_override" class="w-full border rounded px-2 py-1.5 text-sm">
</div>
<div>
<label class="block text-xs font-medium mb-1">Bestand</label>
<input type="number" :name="'variants[' + i + '][stock]'" x-model="v.stock" class="w-full border rounded px-2 py-1.5 text-sm">
</div>
<div>
<label class="block text-xs font-medium mb-1">Warnung ab</label>
<input type="number" :name="'variants[' + i + '][stock_warning]'" x-model="v.stock_warning" class="w-full border rounded px-2 py-1.5 text-sm">
</div>
</div>
{{-- Attributwerte --}}
<div>
<label class="block text-xs font-medium mb-2">Attributwerte</label>
<div class="flex flex-wrap gap-3">
@foreach($attributes as $attr)
<div>
<div class="text-xs text-gray-500 mb-1">{{ $attr->name }}</div>
<div class="flex flex-wrap gap-1">
@foreach($attr->values as $val)
<label class="flex items-center gap-1 text-xs cursor-pointer">
<input type="checkbox"
:name="'variants[' + i + '][attribute_value_ids][]'"
value="{{ $val->id }}"
:checked="v.attribute_value_ids.includes({{ $val->id }})"
@change="toggleAttrValue(v, {{ $val->id }})">
{{ $val->value }}
</label>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" :name="'variants[' + i + '][active]'" :checked="v.active" @change="v.active = !v.active">
Aktiv
</label>
<button type="button" @click="removeVariant(i)" class="text-red-600 text-sm hover:underline">Entfernen</button>
</div>
</div>
</template>
</div>
</div>
{{-- Bilder --}}
<div class="bg-white rounded shadow p-6 space-y-3">
<h3 class="font-semibold text-gray-700">Bilder</h3>
@if(isset($product) && $product->images->count())
<div class="flex flex-wrap gap-3">
@foreach($product->images as $img)
<div class="relative">
<img src="{{ asset('storage/'.$img->path) }}" class="h-20 w-20 object-cover rounded border">
<label class="absolute -top-1 -right-1 bg-red-600 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center cursor-pointer" title="Löschen">
<input type="checkbox" name="delete_images[]" value="{{ $img->id }}" class="sr-only">✕
</label>
</div>
@endforeach
</div>
@endif
<input type="file" name="images[]" multiple accept="image/*" class="text-sm">
<p class="text-xs text-gray-400">Mehrere Dateien auswählbar. Das erste Bild wird als Hauptbild verwendet.</p>
</div>
</div>
{{-- Rechte Spalte: Meta --}}
<div class="space-y-4">
<div class="bg-white rounded shadow p-6 space-y-4">
<h3 class="font-semibold text-gray-700">Organisation</h3>
<div>
<label class="block text-sm font-medium mb-1">Kategorie</label>
<select name="category_id" class="w-full border rounded px-3 py-2 text-sm">
<option value="">— Keine —</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id', $product->category_id ?? '') == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Reihenfolge</label>
<input type="number" name="sort_order" value="{{ old('sort_order', $product->sort_order ?? 0) }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="active" value="1" {{ old('active', $product->active ?? true) ? 'checked' : '' }}>
Aktiv (im Shop sichtbar)
</label>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="featured" value="1" {{ old('featured', $product->featured ?? false) ? 'checked' : '' }} class="accent-yellow-500">
<span>⭐ Top Produkt <span class="text-gray-400 font-normal">(Startseite)</span></span>
</label>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="track_stock" value="1" {{ old('track_stock', $product->track_stock ?? true) ? 'checked' : '' }} class="accent-blue-600">
<span>Lagerbestand verwalten <span class="text-gray-400 font-normal">(deaktiviert = immer lieferbar)</span></span>
</label>
</div>
<div class="flex gap-2">
<button type="submit" class="flex-1 bg-blue-600 text-white py-2 rounded hover:bg-blue-700 text-sm">Speichern</button>
<a href="{{ route('admin.products.index') }}" class="flex-1 text-center py-2 rounded border text-sm hover:bg-gray-50">Abbrechen</a>
</div>
</div>
</div>
</form>
<script>
function productForm() {
return {
variants: {!! $variantsJson !!},
addVariant() {
this.variants.push({ id: null, sku: '', price_override: '', stock: 0, stock_warning: 5, active: true, attribute_value_ids: [] });
},
removeVariant(i) {
this.variants.splice(i, 1);
},
toggleAttrValue(v, id) {
const idx = v.attribute_value_ids.indexOf(id);
if (idx === -1) v.attribute_value_ids.push(id);
else v.attribute_value_ids.splice(idx, 1);
}
}
}
</script>
@endsection
@@ -0,0 +1,57 @@
@extends('layouts.admin')
@section('title', 'Artikel')
@section('content')
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">Artikel</h2>
<form method="GET" class="flex gap-2">
<input type="text" name="q" value="{{ request('q') }}" placeholder="Suchen…" class="border rounded px-3 py-1.5 text-sm">
<button class="bg-gray-200 px-3 py-1.5 rounded text-sm hover:bg-gray-300">Suchen</button>
</form>
</div>
<a href="{{ route('admin.products.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm">+ Neuer Artikel</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-left">
<tr>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Kategorie</th>
<th class="px-4 py-3">Preis (Brutto)</th>
<th class="px-4 py-3">MwSt.</th>
<th class="px-4 py-3">Aktiv</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($products as $p)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium">
{{ $p->name }}
@if($p->featured)<span class="ml-1 text-yellow-500 text-xs" title="Top Produkt">⭐</span>@endif
</td>
<td class="px-4 py-3 text-gray-500">{{ $p->category?->name ?? '—' }}</td>
<td class="px-4 py-3">{{ number_format($p->base_price, 2, ',', '.') }} €</td>
<td class="px-4 py-3">{{ number_format($p->tax_rate, 0) }} %</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-xs {{ $p->active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500' }}">
{{ $p->active ? 'Ja' : 'Nein' }}
</span>
</td>
<td class="px-4 py-3 text-right space-x-2">
<a href="{{ route('admin.products.edit', $p) }}" class="text-blue-600 hover:underline">Bearbeiten</a>
<form method="POST" action="{{ route('admin.products.destroy', $p) }}" class="inline" onsubmit="return confirm('Artikel löschen?')">
@csrf @method('DELETE')
<button class="text-red-600 hover:underline">Löschen</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400">Keine Artikel gefunden.</td></tr>
@endforelse
</tbody>
</table>
<div class="px-4 py-3 border-t">{{ $products->withQueryString()->links() }}</div>
</div>
@endsection
@@ -0,0 +1,114 @@
@extends('layouts.admin')
@section('title', 'Einstellungen')
@section('content')
<form method="POST" action="{{ route('admin.settings.update') }}" class="max-w-3xl" enctype="multipart/form-data">
@csrf @method('PUT')
<div class="space-y-6">
{{-- Shop-Daten --}}
<div class="bg-white rounded shadow p-6">
<h3 class="font-semibold mb-4">Shop-Informationen</h3>
<div class="grid grid-cols-2 gap-4">
{{-- Logo --}}
<div class="col-span-2">
<label class="block text-sm font-medium mb-2">Shop-Logo</label>
@if($settings['shop_logo'])
<div class="flex items-center gap-4 mb-3">
<img src="{{ asset('storage/'.$settings['shop_logo']) }}"
alt="Logo" class="h-14 object-contain border rounded p-1 bg-gray-50">
<label class="flex items-center gap-2 text-sm text-red-600 cursor-pointer">
<input type="checkbox" name="delete_logo" value="1">
Logo löschen
</label>
</div>
@endif
<input type="file" name="shop_logo" accept="image/*" class="text-sm">
<p class="text-xs text-gray-400 mt-1">PNG oder SVG empfohlen, max. 2 MB. Wird im Header und Footer angezeigt.</p>
</div>
@foreach(['shop_name' => 'Shop-Name', 'shop_email' => 'Shop-E-Mail', 'shop_phone' => 'Telefon', 'shop_owner' => 'Inhaber / Firma', 'shop_vat_id' => 'USt-IdNr.'] as $key => $label)
<div>
<label class="block text-sm font-medium mb-1">{{ $label }}</label>
<input type="text" name="{{ $key }}" value="{{ $settings[$key] }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
@endforeach
<div class="col-span-2">
<label class="block text-sm font-medium mb-1">Adresse (für Impressum & Rechnung)</label>
<textarea name="shop_address" rows="3" class="w-full border rounded px-3 py-2 text-sm">{{ $settings['shop_address'] }}</textarea>
</div>
</div>
</div>
{{-- Bankdaten --}}
<div class="bg-white rounded shadow p-6">
<h3 class="font-semibold mb-4">Bankverbindung (Vorkasse)</h3>
<div class="grid grid-cols-3 gap-4">
@foreach(['bank_name' => 'Bank', 'bank_iban' => 'IBAN', 'bank_bic' => 'BIC'] as $key => $label)
<div>
<label class="block text-sm font-medium mb-1">{{ $label }}</label>
<input type="text" name="{{ $key }}" value="{{ $settings[$key] }}" class="w-full border rounded px-3 py-2 text-sm font-mono">
</div>
@endforeach
</div>
<div class="mt-4 max-w-xs">
<label class="block text-sm font-medium mb-1">Zahlungsziel Rechnung (Tage)</label>
<input type="number" name="invoice_payment_days" value="{{ $settings['invoice_payment_days'] }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
</div>
{{-- SMTP --}}
<div class="bg-white rounded shadow p-6">
<h3 class="font-semibold mb-4">E-Mail / SMTP</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">SMTP Host</label>
<input type="text" name="smtp_host" value="{{ $settings['smtp_host'] }}" class="w-full border rounded px-3 py-2 text-sm font-mono">
</div>
<div>
<label class="block text-sm font-medium mb-1">SMTP Port</label>
<input type="number" name="smtp_port" value="{{ $settings['smtp_port'] }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">SMTP Benutzername</label>
<input type="text" name="smtp_user" value="{{ $settings['smtp_user'] }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">SMTP Passwort</label>
<input type="password" name="smtp_password" value="{{ $settings['smtp_password'] }}" class="w-full border rounded px-3 py-2 text-sm" autocomplete="new-password">
</div>
<div>
<label class="block text-sm font-medium mb-1">Absendername</label>
<input type="text" name="smtp_from_name" value="{{ $settings['smtp_from_name'] }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium mb-1">Absender-E-Mail</label>
<input type="email" name="smtp_from_email" value="{{ $settings['smtp_from_email'] }}" class="w-full border rounded px-3 py-2 text-sm">
</div>
</div>
</div>
{{-- Sonstiges --}}
<div class="bg-white rounded shadow p-6">
<h3 class="font-semibold mb-4">Sonstiges</h3>
<label class="flex items-center gap-2 text-sm mb-3">
<input type="checkbox" name="cookie_consent" value="1" {{ $settings['cookie_consent'] ? 'checked' : '' }}>
Cookie-Consent-Banner anzeigen
</label>
<label class="flex items-center gap-2 text-sm mb-3">
<input type="checkbox" name="require_customer_approval" value="1" {{ ($settings['require_customer_approval'] ?? false) ? 'checked' : '' }}>
<span>Neue Kunden müssen manuell freigeschaltet werden <span class="text-gray-400">(nach Registrierung gesperrt bis Admin-Freigabe)</span></span>
</label>
<div>
<label class="block text-sm font-medium mb-1">Google Analytics Tracking-ID (leer = deaktiviert)</label>
<input type="text" name="google_analytics" value="{{ $settings['google_analytics'] }}" class="w-full border rounded px-3 py-2 text-sm font-mono max-w-xs" placeholder="G-XXXXXXXXXX">
</div>
</div>
<div class="flex gap-3">
<button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">Einstellungen speichern</button>
<a href="{{ route('admin.shipping.index') }}" class="border px-6 py-2 rounded hover:bg-gray-50 text-sm">Versandkosten verwalten →</a>
</div>
</div>
</form>
@endsection
@@ -0,0 +1,42 @@
@extends('layouts.admin')
@section('title', 'Versandkosten')
@section('content')
<div class="max-w-2xl">
<div class="bg-white rounded shadow p-6 mb-6">
<h3 class="font-semibold mb-4">Neue Versandzone</h3>
<form method="POST" action="{{ route('admin.shipping.store') }}" class="grid grid-cols-2 gap-3">
@csrf
<div><label class="block text-xs mb-1">Name</label><input type="text" name="name" class="w-full border rounded px-3 py-2 text-sm" required></div>
<div><label class="block text-xs mb-1">Zone (z.B. DE, AT)</label><input type="text" name="zone" value="DE" class="w-full border rounded px-3 py-2 text-sm" required></div>
<div><label class="block text-xs mb-1">Preis (€)</label><input type="number" name="price" step="0.01" class="w-full border rounded px-3 py-2 text-sm" required></div>
<div><label class="block text-xs mb-1">Kostenlos ab (€, leer = nie)</label><input type="number" name="free_from" step="0.01" class="w-full border rounded px-3 py-2 text-sm"></div>
<div class="col-span-2 flex items-center gap-3">
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="active" value="1" checked> Aktiv</label>
<button class="bg-blue-600 text-white px-4 py-2 rounded text-sm hover:bg-blue-700">Erstellen</button>
</div>
</form>
</div>
@foreach($rates as $rate)
<div class="bg-white rounded shadow p-5 mb-4">
<form method="POST" action="{{ route('admin.shipping.update', $rate) }}" class="grid grid-cols-2 gap-3">
@csrf @method('PUT')
<div><label class="block text-xs mb-1">Name</label><input type="text" name="name" value="{{ $rate->name }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div><label class="block text-xs mb-1">Zone</label><input type="text" name="zone" value="{{ $rate->zone }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div><label class="block text-xs mb-1">Preis (€)</label><input type="number" name="price" step="0.01" value="{{ $rate->price }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div><label class="block text-xs mb-1">Kostenlos ab (€)</label><input type="number" name="free_from" step="0.01" value="{{ $rate->free_from }}" class="w-full border rounded px-3 py-2 text-sm"></div>
<div class="col-span-2 flex items-center justify-between">
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="active" value="1" {{ $rate->active ? 'checked' : '' }}> Aktiv</label>
<div class="flex gap-2">
<button class="bg-green-600 text-white px-4 py-2 rounded text-sm hover:bg-green-700">Speichern</button>
<form method="POST" action="{{ route('admin.shipping.destroy', $rate) }}" onsubmit="return confirm('Löschen?')">
@csrf @method('DELETE')
<button class="bg-red-100 text-red-600 px-4 py-2 rounded text-sm hover:bg-red-200">Löschen</button>
</form>
</div>
</div>
</form>
</div>
@endforeach
</div>
@endsection