20-02-2026
This commit is contained in:
parent
854ce02bf6
commit
4d6b4930b2
128 changed files with 18247 additions and 2093 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Enums\UserOrigin;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
|
@ -31,10 +32,15 @@ class CreateNewUser implements CreatesNewUsers
|
|||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$theme = config('app.theme', '');
|
||||
$origin = UserOrigin::tryFrom($theme);
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
'origin' => $origin?->value,
|
||||
'hub_id' => $input['hub_id'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
app/Casts/PartnerTypeCast.php
Normal file
44
app/Casts/PartnerTypeCast.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\Enums\PartnerType;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PartnerTypeCast implements CastsAttributes
|
||||
{
|
||||
/**
|
||||
* Normalisiert DB-Werte (z.B. "retailer", "broker") zu gültigen Enum-Backing-Values.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function get(Model $model, string $key, mixed $value, array $attributes): ?PartnerType
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = match (strtolower((string) $value)) {
|
||||
'retailer' => PartnerType::Retailer->value,
|
||||
'manufacturer' => PartnerType::Manufacturer->value,
|
||||
'estate-agent', 'broker' => PartnerType::EstateAgent->value,
|
||||
'customer' => PartnerType::Customer->value,
|
||||
default => $value,
|
||||
};
|
||||
|
||||
return PartnerType::tryFrom($normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value instanceof PartnerType ? $value->value : (string) $value;
|
||||
}
|
||||
}
|
||||
28
app/Enums/CurationStatus.php
Normal file
28
app/Enums/CurationStatus.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum CurationStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Approved = 'approved';
|
||||
case Rejected = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Pending => 'Ausstehend',
|
||||
self::Approved => 'Freigegeben',
|
||||
self::Rejected => 'Abgelehnt',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Pending => 'yellow',
|
||||
self::Approved => 'green',
|
||||
self::Rejected => 'red',
|
||||
};
|
||||
}
|
||||
}
|
||||
21
app/Enums/PartnerType.php
Normal file
21
app/Enums/PartnerType.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PartnerType: string
|
||||
{
|
||||
case Retailer = 'Retailer';
|
||||
case Manufacturer = 'Manufacturer';
|
||||
case EstateAgent = 'Estate-Agent';
|
||||
case Customer = 'Customer';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Retailer => 'Händler',
|
||||
self::Manufacturer => 'Hersteller',
|
||||
self::EstateAgent => 'Makler',
|
||||
self::Customer => 'Kunde',
|
||||
};
|
||||
}
|
||||
}
|
||||
19
app/Enums/PriceType.php
Normal file
19
app/Enums/PriceType.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PriceType: string
|
||||
{
|
||||
case Fixed = 'fixed';
|
||||
case FromPrice = 'from_price';
|
||||
case OnRequest = 'on_request';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Fixed => 'Festpreis',
|
||||
self::FromPrice => 'Ab-Preis',
|
||||
self::OnRequest => 'Preis auf Anfrage',
|
||||
};
|
||||
}
|
||||
}
|
||||
37
app/Enums/ProductStatus.php
Normal file
37
app/Enums/ProductStatus.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ProductStatus: string
|
||||
{
|
||||
case Draft = 'draft';
|
||||
case Pending = 'pending';
|
||||
case Correction = 'correction';
|
||||
case Active = 'active';
|
||||
case Archived = 'archived';
|
||||
case Sold = 'sold';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Draft => 'Entwurf',
|
||||
self::Pending => 'In Prüfung',
|
||||
self::Correction => 'Korrektur nötig',
|
||||
self::Active => 'Freigegeben',
|
||||
self::Archived => 'Archiviert',
|
||||
self::Sold => 'Verkauft',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Draft => 'yellow',
|
||||
self::Pending => 'blue',
|
||||
self::Correction => 'orange',
|
||||
self::Active => 'green',
|
||||
self::Archived => 'zinc',
|
||||
self::Sold => 'red',
|
||||
};
|
||||
}
|
||||
}
|
||||
54
app/Enums/ProductType.php
Normal file
54
app/Enums/ProductType.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ProductType: string
|
||||
{
|
||||
/**
|
||||
* Typ A – Teaser-Produkte: Beratungspflicht, Ticket zwingend.
|
||||
* Komplexe Konfiguration (Maße, Module, Materialien) – Abschluss nur im Laden.
|
||||
*/
|
||||
case LocalStock = 'local_stock';
|
||||
|
||||
/**
|
||||
* Typ B – Standard-Produkte: Einfache Varianten, skalierbar, Ticket optional.
|
||||
* Online vollständig konfigurierbar und direkt kaufbar.
|
||||
*/
|
||||
case SmartOrder = 'smart_order';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::LocalStock => 'Local Express (Lagerware)',
|
||||
self::SmartOrder => 'Smart Club (Konfiguration)',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Typ A (LocalStock) erfordert zwingend ein Ticket für den Ladenbesuch.
|
||||
* Typ B (SmartOrder) kann direkt online bestellt werden – Ticket optional.
|
||||
*/
|
||||
public function requiresTicket(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LocalStock => true,
|
||||
self::SmartOrder => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erlaubte Preistypen je Produkttyp.
|
||||
*
|
||||
* Typ A (Teaser): Kein Festpreis online möglich – nur Ab-Preis oder Preis auf Anfrage.
|
||||
* Typ B (Standard): Alle Preistypen erlaubt.
|
||||
*
|
||||
* @return array<int, PriceType>
|
||||
*/
|
||||
public function allowedPriceTypes(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::LocalStock => [PriceType::FromPrice, PriceType::OnRequest],
|
||||
self::SmartOrder => [PriceType::Fixed, PriceType::FromPrice, PriceType::OnRequest],
|
||||
};
|
||||
}
|
||||
}
|
||||
28
app/Enums/UserOrigin.php
Normal file
28
app/Enums/UserOrigin.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserOrigin: string
|
||||
{
|
||||
case Style2Own = 'style2own';
|
||||
case StilEigentum = 'stileigentum';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Style2Own => 'Style2Own',
|
||||
self::StilEigentum => 'StilEigentum',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'du'|'sie'
|
||||
*/
|
||||
public function tonality(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Style2Own => 'du',
|
||||
self::StilEigentum => 'sie',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,13 @@ class BasicAuthMiddleware
|
|||
|
||||
if (
|
||||
str_starts_with($path, 'livewire/') ||
|
||||
str_starts_with($path, 'livewire-') ||
|
||||
str_contains($path, '/livewire/') ||
|
||||
str_contains($path, '/livewire-') ||
|
||||
$request->is('livewire/*') ||
|
||||
$request->is('*/livewire/*')
|
||||
$request->is('livewire-*') ||
|
||||
$request->is('*/livewire/*') ||
|
||||
$request->is('*/livewire-*')
|
||||
) {
|
||||
return $next($request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
36
app/Models/Media.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
246
app/Models/Product.php
Normal 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);
|
||||
}
|
||||
}
|
||||
29
app/Models/ProductActivity.php
Normal file
29
app/Models/ProductActivity.php
Normal 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);
|
||||
}
|
||||
}
|
||||
50
app/Models/ProductLogistics.php
Normal file
50
app/Models/ProductLogistics.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
app/Models/ProductWoodOrigin.php
Normal file
48
app/Models/ProductWoodOrigin.php
Normal 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
55
app/Models/Setting.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
67
app/Policies/PartnerPolicy.php
Normal file
67
app/Policies/PartnerPolicy.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Partner;
|
||||
use App\Models\User;
|
||||
|
||||
class PartnerPolicy
|
||||
{
|
||||
/**
|
||||
* Admins und Super-Admins können alle Partner sehen.
|
||||
* Partner können nur ihren eigenen Eintrag sehen.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'Super-Admin']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admins sehen alle Partner; Partner sehen ihren eigenen Eintrag.
|
||||
*/
|
||||
public function view(User $user, Partner $partner): bool
|
||||
{
|
||||
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->partner_id === $partner->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur Admins können neue Partner direkt anlegen (normale Partner werden eingeladen).
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'Super-Admin']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admins können jeden Partner bearbeiten.
|
||||
* Partner können ihr eigenes Profil bearbeiten.
|
||||
*/
|
||||
public function update(User $user, Partner $partner): bool
|
||||
{
|
||||
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->partner_id === $partner->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur Admins können Partner löschen.
|
||||
*/
|
||||
public function delete(User $user, Partner $partner): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'Super-Admin']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produkte eines Partners kuratieren (freigeben/ablehnen).
|
||||
*/
|
||||
public function curateProducts(User $user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('curate products');
|
||||
}
|
||||
}
|
||||
73
app/Policies/ProductPolicy.php
Normal file
73
app/Policies/ProductPolicy.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
|
||||
class ProductPolicy
|
||||
{
|
||||
/**
|
||||
* Admins, Retailer und Manufacturer können Produkte sehen.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'Super-Admin', 'Retailer', 'Manufacturer']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admins sehen alle Produkte; Partner sehen nur ihre eigenen.
|
||||
*/
|
||||
public function view(User $user, Product $product): bool
|
||||
{
|
||||
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $product->partner_id === $user->partner_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retailer dürfen Teaser-Produkte (Typ A) anlegen.
|
||||
* Manufacturer dürfen Konfigurations-Produkte (Typ B) anlegen.
|
||||
* Admins dürfen beide Typen anlegen.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'Super-Admin', 'Retailer', 'Manufacturer']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admins können alle Produkte bearbeiten.
|
||||
* Partner können nur ihre eigenen Produkte bearbeiten.
|
||||
*/
|
||||
public function update(User $user, Product $product): bool
|
||||
{
|
||||
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $product->partner_id === $user->partner_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admins können alle Produkte löschen.
|
||||
* Partner können nur ihre eigenen Produkte löschen.
|
||||
*/
|
||||
public function delete(User $user, Product $product): bool
|
||||
{
|
||||
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $product->partner_id === $user->partner_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur Admins können Produkte kuratieren (freigeben/ablehnen).
|
||||
*/
|
||||
public function curate(User $user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('curate products');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue