Initial commit: WitShop Laravel Online-Shop
- Laravel 11, Blade, Alpine.js, Tailwind CSS - Produkt- & Kategorieverwaltung mit Varianten - Checkout (Vorkasse + Rechnung), Session-Warenkorb - Admin-Backend (Dashboard, Bestellungen, Kunden, CMS) - DSGVO-konform: Cookie-Consent, Impressum, Widerruf, AGB - PDF-Rechnung via dompdf, SMTP-Mailversand - Kundenverwaltung mit manueller Freischaltung Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user