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 @@
|
||||
*.sqlite*
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user