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
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,233 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
// Adressbuch
Schema::create('addresses', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('first_name');
$table->string('last_name');
$table->string('company')->nullable();
$table->string('street');
$table->string('street2')->nullable();
$table->string('zip');
$table->string('city');
$table->string('country')->default('DE');
$table->string('phone')->nullable();
$table->boolean('is_default')->default(false);
$table->timestamps();
});
// Kategorien
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('image')->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->boolean('active')->default(true);
$table->timestamps();
});
// Produkte
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->longText('description')->nullable();
$table->decimal('base_price', 10, 2);
$table->decimal('tax_rate', 5, 2)->default(19.00);
$table->decimal('weight', 8, 3)->nullable();
$table->boolean('active')->default(true);
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
});
// Produktbilder
Schema::create('product_images', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
$table->string('path');
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
});
// Attribute (z.B. "Größe", "Farbe")
Schema::create('attributes', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
// Attributwerte (z.B. "M", "Rot")
Schema::create('attribute_values', function (Blueprint $table) {
$table->id();
$table->foreignId('attribute_id')->constrained()->cascadeOnDelete();
$table->string('value');
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
});
// Produktvarianten
Schema::create('product_variants', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
$table->string('sku')->unique();
$table->decimal('price_override', 10, 2)->nullable();
$table->integer('stock')->default(0);
$table->integer('stock_warning')->default(5);
$table->boolean('active')->default(true);
$table->timestamps();
});
// Variante ↔ Attributwerte (Pivot)
Schema::create('product_variant_attribute_values', function (Blueprint $table) {
$table->foreignId('product_variant_id')->constrained()->cascadeOnDelete();
$table->foreignId('attribute_value_id')->constrained()->cascadeOnDelete();
$table->primary(['product_variant_id', 'attribute_value_id']);
});
// Versandzonen
Schema::create('shipping_rates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('zone')->default('DE');
$table->decimal('price', 10, 2);
$table->decimal('free_from', 10, 2)->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
});
// Bestellungen
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('order_number')->unique();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('guest_email')->nullable();
$table->enum('status', ['new','payment_received','processing','shipped','completed','cancelled'])->default('new');
$table->enum('payment_method', ['prepayment','invoice']);
$table->decimal('subtotal', 10, 2);
$table->decimal('tax_total', 10, 2);
$table->decimal('shipping_cost', 10, 2)->default(0);
$table->decimal('total', 10, 2);
$table->string('tracking_code')->nullable();
$table->text('notes')->nullable();
$table->timestamp('paid_at')->nullable();
$table->timestamps();
});
// Bestellpositionen
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->cascadeOnDelete();
$table->foreignId('product_variant_id')->nullable()->constrained()->nullOnDelete();
$table->string('product_name');
$table->string('variant_label')->nullable();
$table->integer('quantity');
$table->decimal('unit_price', 10, 2);
$table->decimal('tax_rate', 5, 2);
$table->timestamps();
});
// Bestelladressen (Rechnungs- & Lieferadresse eingefroren)
Schema::create('order_addresses', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->cascadeOnDelete();
$table->enum('type', ['billing','shipping']);
$table->string('first_name');
$table->string('last_name');
$table->string('company')->nullable();
$table->string('street');
$table->string('street2')->nullable();
$table->string('zip');
$table->string('city');
$table->string('country')->default('DE');
$table->string('phone')->nullable();
$table->timestamps();
});
// CMS-Seiten (statisch, blog, faq)
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('title');
$table->longText('content')->nullable();
$table->enum('type', ['static','blog','faq'])->default('static');
$table->string('image')->nullable();
$table->string('meta_title')->nullable();
$table->string('meta_description')->nullable();
$table->boolean('published')->default(true);
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
// Startseiten-Banner
Schema::create('banners', function (Blueprint $table) {
$table->id();
$table->string('image');
$table->string('headline')->nullable();
$table->string('subtext')->nullable();
$table->string('button_label')->nullable();
$table->string('button_link')->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->boolean('active')->default(true);
$table->timestamps();
});
// FAQ-Gruppen
Schema::create('faq_groups', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
});
// FAQ-Einträge
Schema::create('faqs', function (Blueprint $table) {
$table->id();
$table->foreignId('faq_group_id')->nullable()->constrained()->nullOnDelete();
$table->string('question');
$table->text('answer');
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
});
// Shop-Einstellungen (key/value)
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings');
Schema::dropIfExists('faqs');
Schema::dropIfExists('faq_groups');
Schema::dropIfExists('banners');
Schema::dropIfExists('pages');
Schema::dropIfExists('order_addresses');
Schema::dropIfExists('order_items');
Schema::dropIfExists('orders');
Schema::dropIfExists('shipping_rates');
Schema::dropIfExists('product_variant_attribute_values');
Schema::dropIfExists('product_variants');
Schema::dropIfExists('attribute_values');
Schema::dropIfExists('attributes');
Schema::dropIfExists('product_images');
Schema::dropIfExists('products');
Schema::dropIfExists('categories');
Schema::dropIfExists('addresses');
}
};
@@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('role', ['customer', 'admin', 'super_admin'])->default('customer')->after('email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('approved')->default(false)->after('role');
});
// Admins and existing super_admins are always approved
\DB::table('users')->whereIn('role', ['admin', 'super_admin'])->update(['approved' => true]);
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('approved');
});
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('pages', function (Blueprint $table) {
$table->boolean('show_in_footer')->default(true)->after('published');
$table->boolean('show_in_nav')->default(false)->after('show_in_footer');
$table->integer('nav_sort')->default(0)->after('show_in_nav');
});
}
public function down(): void
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn(['show_in_footer', 'show_in_nav', 'nav_sort']);
});
}
};
@@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::table('products', function (Blueprint $table) {
$table->boolean('featured')->default(false)->after('active');
});
}
public function down(): void {
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('featured');
});
}
};
@@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::table('products', function (Blueprint $table) {
$table->boolean('track_stock')->default(true)->after('featured');
});
}
public function down(): void {
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('track_stock');
});
}
};
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class AdminUserSeeder extends Seeder
{
public function run(): void
{
User::firstOrCreate(['email' => 'admin@example.de'], [
'name' => 'Administrator',
'password' => bcrypt('changeme123'),
'role' => 'super_admin',
'email_verified_at' => now(),
]);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
SettingsSeeder::class,
AdminUserSeeder::class,
]);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Database\Seeders;
use App\Models\Setting;
use App\Models\ShippingRate;
use Illuminate\Database\Seeder;
class SettingsSeeder extends Seeder
{
public function run(): void
{
$defaults = [
'shop_name' => 'Mein Online-Shop',
'shop_email' => 'info@example.de',
'shop_phone' => '',
'shop_address' => '',
'shop_owner' => '',
'shop_vat_id' => '',
'bank_name' => '',
'bank_iban' => '',
'bank_bic' => '',
'invoice_payment_days' => '14',
'currency' => 'EUR',
'currency_symbol' => '€',
'smtp_host' => '',
'smtp_port' => '587',
'smtp_user' => '',
'smtp_password' => '',
'smtp_from_name' => 'Mein Online-Shop',
'smtp_from_email' => 'noreply@example.de',
'cookie_consent' => '1',
'google_analytics' => '',
];
foreach ($defaults as $key => $value) {
Setting::firstOrCreate(['key' => $key], ['value' => $value]);
}
ShippingRate::firstOrCreate(['name' => 'Standard (Deutschland)'], [
'zone' => 'DE',
'price' => 4.99,
'free_from' => 50.00,
'active' => true,
]);
}
}