20-02-2026

This commit is contained in:
Kevin Adametz 2026-02-20 17:57:50 +01:00
parent 854ce02bf6
commit 4d6b4930b2
128 changed files with 18247 additions and 2093 deletions

View file

@ -3,8 +3,20 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Attribute extends Model
{
//
protected $fillable = [
'name',
'slug',
];
/**
* Ein Attribut hat viele Werte (z.B. "Farbe" "Rot", "Blau", "Grün").
*/
public function values(): HasMany
{
return $this->hasMany(AttributeValue::class);
}
}

View file

@ -3,8 +3,30 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class AttributeValue extends Model
{
//
protected $fillable = [
'attribute_id',
'value',
'slug',
];
/**
* Gehört zu einem Attribut.
*/
public function attribute(): BelongsTo
{
return $this->belongsTo(Attribute::class);
}
/**
* Kann mehreren Produkt-Varianten zugeordnet sein.
*/
public function productVariants(): BelongsToMany
{
return $this->belongsToMany(ProductVariant::class, 'product_variant_attributes');
}
}

View file

@ -2,9 +2,44 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
//
use HasFactory;
protected $fillable = [
'parent_id',
'name',
'slug',
'description',
];
/**
* Übergeordnete Kategorie.
*/
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id');
}
/**
* Untergeordnete Kategorien.
*/
public function children(): HasMany
{
return $this->hasMany(self::class, 'parent_id');
}
/**
* Produkte in dieser Kategorie.
*/
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class);
}
}

View file

@ -3,8 +3,21 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Collection extends Model
{
//
protected $fillable = [
'name',
'slug',
'description',
];
/**
* Produkte in dieser Kollektion.
*/
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}

View file

@ -33,4 +33,24 @@ class Hub extends Model
{
return $this->hasMany(Partner::class);
}
/**
* Ein Hub hat viele direkt zugeordnete User (Kunden).
*/
public function users(): HasMany
{
return $this->hasMany(User::class);
}
/**
* Ein Hub hat viele Produkte.
*/
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
protected $casts = [
'is_active' => 'boolean',
];
}

36
app/Models/Media.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Media extends Model
{
use HasFactory;
protected $fillable = [
'model_type',
'model_id',
'file_path',
'type',
'alt_text',
'order_column',
];
protected function casts(): array
{
return [
'order_column' => 'integer',
];
}
/**
* Polymorphe Beziehung zum Eltern-Model (Product, Partner, etc.).
*/
public function model(): MorphTo
{
return $this->morphTo();
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Casts\PartnerTypeCast;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -39,12 +40,19 @@ class Partner extends Model
'assembly_radius_km',
'provision_fixed_amount',
'provision_rate_percentage',
'story_text',
'opening_hours',
'specialties',
'founded_year',
];
protected $casts = [
'type' => PartnerTypeCast::class,
'is_active' => 'boolean',
'setup_completed' => 'boolean',
'setup_completed_at' => 'datetime',
'opening_hours' => 'array',
'specialties' => 'array',
];
/**
@ -104,9 +112,19 @@ class Partner extends Model
return $this->hasOne(Brand::class);
}
// TODO: Später die Beziehung zu Products hinzufügen
// public function products(): HasMany
// {
// return $this->hasMany(Product::class);
// }
/**
* Ein Partner hat viele Produkte.
*/
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
/**
* Polymorphe Beziehung zu Media (Team-Fotos, Showroom-Galerie, etc.).
*/
public function media(): \Illuminate\Database\Eloquent\Relations\MorphMany
{
return $this->morphMany(Media::class, 'model');
}
}

246
app/Models/Product.php Normal file
View file

@ -0,0 +1,246 @@
<?php
namespace App\Models;
use App\Enums\PriceType;
use App\Enums\ProductStatus;
use App\Enums\ProductType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'partner_id',
'partner_product_number',
'b2in_article_number',
'brand_id',
'collection_id',
'hub_id',
'name',
'slug',
'product_type',
'status',
'price_type',
'price_display_text',
'description_short',
'description_long',
'care_instructions',
'width_cm',
'height_cm',
'depth_cm',
'dimensions_specific',
'assembly_status',
'meta_title',
'meta_description',
'is_curated',
'curated_at',
'curated_by',
'curation_notes',
'is_available',
'country_of_origin',
'main_material',
'surface_material',
'cover_material',
'color_finish',
'certificates',
'assembly_time_min',
'load_capacity_kg',
'delivery_type',
'assembly_service',
'service_radius_km',
'warranty_months',
'production_time_days',
'productIsAvailable',
'visible_from',
'visible_until',
'co2_footprint_kg',
'recycling_percentage',
'is_regional_production',
'storage_volume_liters',
'assembly_effort_score',
'design_score',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'product_type' => ProductType::class,
'status' => ProductStatus::class,
'price_type' => PriceType::class,
'dimensions_specific' => 'array',
'is_curated' => 'boolean',
'curated_at' => 'datetime',
'is_available' => 'boolean',
'productIsAvailable' => 'boolean',
'certificates' => 'array',
'assembly_service' => 'boolean',
'is_regional_production' => 'boolean',
'visible_from' => 'date',
'visible_until' => 'date',
'co2_footprint_kg' => 'decimal:2',
];
}
/**
* Produkt gehört zu einem Partner (Händler oder Hersteller).
*/
public function partner(): BelongsTo
{
return $this->belongsTo(Partner::class);
}
/**
* Produkt gehört zu einer Marke.
*/
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
/**
* Produkt gehört zu einer Kollektion.
*/
public function collection(): BelongsTo
{
return $this->belongsTo(Collection::class);
}
/**
* Produkt gehört zu einem Hub (regionale Zuordnung).
*/
public function hub(): BelongsTo
{
return $this->belongsTo(Hub::class);
}
/**
* User, der das Produkt kuratiert/freigegeben hat.
*/
public function curator(): BelongsTo
{
return $this->belongsTo(User::class, 'curated_by');
}
/**
* Produkt kann mehreren Kategorien zugeordnet sein.
*/
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
/**
* Produkt kann mehrere Tags haben.
*/
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
/**
* Produkt hat mehrere Varianten (Farben, Größen, etc.).
*/
public function variants(): HasMany
{
return $this->hasMany(ProductVariant::class);
}
/**
* Polymorphe Beziehung zu Media (Bilder, Videos, PDFs).
*/
public function media(): MorphMany
{
return $this->morphMany(Media::class, 'model');
}
/**
* Holzherkunft-Einträge für EUDR-Compliance.
*/
public function woodOrigins(): HasMany
{
return $this->hasMany(ProductWoodOrigin::class);
}
/**
* Änderungshistorie.
*/
public function activities(): HasMany
{
return $this->hasMany(ProductActivity::class);
}
/**
* Verwandte Produkte.
*/
public function relatedProducts(): BelongsToMany
{
return $this->belongsToMany(self::class, 'related_products', 'product_id', 'related_product_id');
}
/**
* Scope: Nur aktive Produkte.
*/
public function scopeActive(Builder $query): void
{
$query->where('status', ProductStatus::Active);
}
/**
* Scope: Nur kuratierte/freigegebene Produkte.
*/
public function scopeCurated(Builder $query): void
{
$query->where('is_curated', true);
}
/**
* Scope: Nur Produkte, die auf Freigabe warten.
*/
public function scopePending(Builder $query): void
{
$query->where('status', ProductStatus::Pending);
}
/**
* Scope: Nur Local Stock Produkte (Säule A).
*/
public function scopeLocalStock(Builder $query): void
{
$query->where('product_type', ProductType::LocalStock);
}
/**
* Scope: Nur Smart Order Produkte (Säule B).
*/
public function scopeSmartOrder(Builder $query): void
{
$query->where('product_type', ProductType::SmartOrder);
}
/**
* Scope: Produkte in einem bestimmten Hub.
*/
public function scopeInHub(Builder $query, int $hubId): void
{
$query->where('hub_id', $hubId);
}
/**
* Scope: Verfügbare Produkte (aktiv, kuratiert, verfügbar).
*/
public function scopeAvailable(Builder $query): void
{
$query->active()->curated()->where('is_available', true);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ProductActivity extends Model
{
use HasFactory;
protected $fillable = [
'product_id',
'user_id',
'action',
'note',
];
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ProductLogistics extends Model
{
public $timestamps = false;
protected $fillable = [
'product_variant_id',
'shipping_class_id',
'package_width_cm',
'package_height_cm',
'package_depth_cm',
'package_weight_g',
'package_count',
'location_bin',
'packaging_type',
'packaging_recyclable_percent',
'is_palletizable',
'hs_code',
];
protected function casts(): array
{
return [
'package_count' => 'integer',
'is_palletizable' => 'boolean',
];
}
/**
* Gehört zu einer Produkt-Variante.
*/
public function productVariant(): BelongsTo
{
return $this->belongsTo(ProductVariant::class);
}
/**
* Versandklasse.
*/
public function shippingClass(): BelongsTo
{
return $this->belongsTo(ShippingClass::class);
}
}

View file

@ -3,8 +3,89 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class ProductVariant extends Model
{
//
protected $fillable = [
'product_id',
'name_suffix',
'is_master_variant',
'sku',
'han_mpn',
'ean_gtin',
'selling_price',
'msrp',
'purchase_price',
'tax_rate_id',
'stock_quantity',
'stock_min_threshold',
'availability_status',
'delivery_time_text',
'currency',
'is_rentable',
'rental_duration_options',
'rental_rate_formula',
'residual_value_percentage',
'variant_weight_g',
'is_active',
];
protected function casts(): array
{
return [
'is_master_variant' => 'boolean',
'selling_price' => 'integer',
'msrp' => 'integer',
'purchase_price' => 'integer',
'stock_quantity' => 'integer',
'is_rentable' => 'boolean',
'rental_duration_options' => 'array',
'residual_value_percentage' => 'decimal:2',
'is_active' => 'boolean',
];
}
/**
* Gehört zu einem Produkt.
*/
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
/**
* Steuersatz der Variante.
*/
public function taxRate(): BelongsTo
{
return $this->belongsTo(TaxRate::class);
}
/**
* Attribut-Werte dieser Variante (z.B. Farbe: Rot, Größe: L).
*/
public function attributeValues(): BelongsToMany
{
return $this->belongsToMany(AttributeValue::class, 'product_variant_attributes');
}
/**
* Logistik-Daten der Variante (1:1).
*/
public function logistics(): HasOne
{
return $this->hasOne(ProductLogistics::class);
}
/**
* Bilder der Variante.
*/
public function media(): MorphMany
{
return $this->morphMany(Media::class, 'model');
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class ProductWoodOrigin extends Model
{
/** @use HasFactory<\Database\Factories\ProductWoodOriginFactory> */
use HasFactory;
protected $fillable = [
'product_id',
'wood_species',
'origin_country',
'origin_region',
'harvest_year',
'forest_operator',
'sustainability_certificate',
'eudr_reference_id',
];
protected function casts(): array
{
return [
'harvest_year' => 'integer',
];
}
/**
* Gehört zu einem Produkt.
*/
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
/**
* EUDR-Dokumente (polymorphe Beziehung via Media).
*/
public function media(): MorphMany
{
return $this->morphMany(Media::class, 'model');
}
}

55
app/Models/Setting.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = [
'group',
'key',
'value',
'type',
'description',
];
/**
* Hole einen Setting-Wert anhand von Gruppe und Key.
*/
public static function getValue(string $group, string $key, mixed $default = null): mixed
{
$setting = self::query()
->where('group', $group)
->where('key', $key)
->first();
if (! $setting) {
return $default;
}
return match ($setting->type) {
'integer' => (int) $setting->value,
'boolean' => filter_var($setting->value, FILTER_VALIDATE_BOOLEAN),
'json' => json_decode($setting->value, true),
default => $setting->value,
};
}
/**
* Setze einen Setting-Wert.
*/
public static function setValue(string $group, string $key, mixed $value): void
{
$setting = self::query()
->where('group', $group)
->where('key', $key)
->first();
if ($setting) {
$setting->update([
'value' => is_array($value) ? json_encode($value) : (string) $value,
]);
}
}
}

View file

@ -3,8 +3,20 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ShippingClass extends Model
{
//
protected $fillable = [
'name',
'description',
];
/**
* Logistik-Einträge mit dieser Versandklasse.
*/
public function productLogistics(): HasMany
{
return $this->hasMany(ProductLogistics::class);
}
}

View file

@ -3,8 +3,20 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tag extends Model
{
//
protected $fillable = [
'name',
'slug',
];
/**
* Produkte mit diesem Tag.
*/
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class);
}
}

View file

@ -3,8 +3,29 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TaxRate extends Model
{
//
protected $fillable = [
'name',
'rate_percentage',
'is_default',
];
protected function casts(): array
{
return [
'rate_percentage' => 'decimal:2',
'is_default' => 'boolean',
];
}
/**
* Varianten mit diesem Steuersatz.
*/
public function productVariants(): HasMany
{
return $this->hasMany(ProductVariant::class);
}
}

View file

@ -2,10 +2,13 @@
namespace App\Models;
use App\Enums\UserOrigin;
use App\Notifications\CustomResetPasswordNotification;
use App\Notifications\CustomVerifyEmailNotification;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -13,13 +16,11 @@ use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Authenticatable implements MustVerifyEmail
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable, SoftDeletes;
use HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
@ -28,6 +29,8 @@ class User extends Authenticatable implements MustVerifyEmail
*/
protected $fillable = [
'partner_id',
'hub_id',
'origin',
'name',
'display_name',
'email',
@ -56,6 +59,7 @@ class User extends Authenticatable implements MustVerifyEmail
'email_verified_at' => 'datetime',
'deleted_at' => 'datetime',
'password' => 'hashed',
'origin' => UserOrigin::class,
];
}
@ -64,6 +68,14 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->belongsTo(Partner::class);
}
/**
* Direkte Hub-Zuordnung des Users (für schnelle Queries).
*/
public function hub(): BelongsTo
{
return $this->belongsTo(Hub::class);
}
/**
* Get the registration code used by this user
*/
@ -79,7 +91,7 @@ class User extends Authenticatable implements MustVerifyEmail
{
return Str::of($this->name)
->explode(' ')
->map(fn(string $name) => Str::of($name)->substr(0, 1))
->map(fn (string $name) => Str::of($name)->substr(0, 1))
->implode('');
}
@ -89,9 +101,9 @@ class User extends Authenticatable implements MustVerifyEmail
public function anonymize(): void
{
$this->update([
'name' => 'Gelöschter Benutzer #' . $this->id,
'name' => 'Gelöschter Benutzer #'.$this->id,
'display_name' => null,
'email' => 'deleted_' . $this->id . '@anonymized.local',
'email' => 'deleted_'.$this->id.'@anonymized.local',
'password' => bcrypt(Str::random(64)),
]);
@ -118,7 +130,6 @@ class User extends Authenticatable implements MustVerifyEmail
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token): void
{
@ -127,8 +138,6 @@ class User extends Authenticatable implements MustVerifyEmail
/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailVerificationNotification(): void
{