21-11-2025

This commit is contained in:
Kevin Adametz 2025-11-21 18:21:23 +01:00
parent fa2ebd457d
commit 07959c0ba2
113 changed files with 4730 additions and 898 deletions

View file

@ -15,6 +15,19 @@ class BasicAuthMiddleware
*/
public function handle(Request $request, Closure $next): Response
{
// Skip Basic Auth für Livewire-Requests komplett
// Diese sind bereits durch Laravel Session/CSRF geschützt
$path = $request->path();
if (
str_starts_with($path, 'livewire/') ||
str_contains($path, '/livewire/') ||
$request->is('livewire/*') ||
$request->is('*/livewire/*')
) {
return $next($request);
}
// Credentials from .env file
$user = config('auth.basic.user');
$pass = config('auth.basic.password');

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsurePartnerSetupCompleted
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
// Wenn User keinen Partner hat, fortfahren
if (!$user || !$user->partner_id) {
return $next($request);
}
// Wenn User sich auf der Setup-Seite befindet (Route oder URL), fortfahren
if ($request->routeIs('partner.setup.wizard') || $request->is('partner/setup*')) {
return $next($request);
}
$partner = $user->partner;
// Wenn Setup bereits abgeschlossen ist, fortfahren
if ($partner && $partner->setup_completed) {
return $next($request);
}
// Ansonsten zum Setup-Wizard umleiten
return redirect()->route('partner.setup.wizard');
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\Models\PartnerInvitation;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PartnerInvitationMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
public PartnerInvitation $invitation,
public string $invitationUrl
) {}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Einladung: ' . $this->invitation->company_name . ' - B2In Platform',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.partner-invitation',
with: [
'invitation' => $this->invitation,
'invitationUrl' => $this->invitationUrl,
'companyName' => $this->invitation->company_name,
'contactFullName' => $this->invitation->contact_full_name,
'partnerType' => $this->invitation->role?->display_name ?? $this->invitation->role?->name,
'expiresAt' => $this->invitation->expires_at,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

10
app/Models/Attribute.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Attribute extends Model
{
//
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AttributeValue extends Model
{
//
}

33
app/Models/Brand.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Brand extends Model
{
use HasFactory;
protected $fillable = [
'partner_id',
'name',
'slug',
'description',
'logo_url',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Eine Brand gehört zu einem Partner (Manufacturer)
*/
public function partner(): BelongsTo
{
return $this->belongsTo(Partner::class);
}
}

10
app/Models/Category.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
//
}

10
app/Models/Collection.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Collection extends Model
{
//
}

36
app/Models/Hub.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\HasMany;
class Hub extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'keyvisual_url',
'emblem_url',
'is_active',
];
/**
* Ein Hub besteht aus vielen Location-Einträgen (PLZs).
*/
public function locations(): HasMany
{
return $this->hasMany(HubLocation::class);
}
/**
* Ein Hub hat viele Partner (Händler, Makler).
*/
public function partners(): HasMany
{
return $this->hasMany(Partner::class);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class HubLocation extends Model
{
use HasFactory;
protected $fillable = [
'hub_id',
'city_name',
'zip_code',
];
/**
* Dieser Location-Eintrag gehört zu einem Hub.
*/
public function hub(): BelongsTo
{
return $this->belongsTo(Hub::class);
}
}

58
app/Models/Partner.php Normal file
View file

@ -0,0 +1,58 @@
<?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\HasMany;
class Partner extends Model
{
use HasFactory;
protected $fillable = [
'company_name',
'slug',
'type',
'hub_id',
'description',
'logo_url',
'is_active',
'setup_completed',
'setup_completed_at',
'delivery_radius_km',
'assembly_radius_km',
'provision_fixed_amount',
'provision_rate_percentage',
];
protected $casts = [
'is_active' => 'boolean',
'setup_completed' => 'boolean',
'setup_completed_at' => 'datetime',
];
/**
* Ein Partner (Händler/Makler) kann einem Hub zugeordnet sein.
* Ein Hersteller ist evtl. "global" (hub_id = null).
*/
public function hub(): BelongsTo
{
return $this->belongsTo(Hub::class);
}
/**
* Ein Partner (Firma) kann mehrere User-Logins haben.
*/
public function users(): HasMany
{
return $this->hasMany(User::class);
}
// TODO: Später die Beziehung zu Products hinzufügen
// public function products(): HasMany
// {
// return $this->hasMany(Product::class);
// }
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
use Spatie\Permission\Models\Role;
class PartnerInvitation extends Model
{
use HasFactory;
protected $fillable = [
'company_name',
'contact_first_name',
'contact_last_name',
'role_id',
'email',
'token',
'status',
'expires_at',
'invited_by',
'partner_id',
'accepted_at',
];
protected $casts = [
'expires_at' => 'datetime',
'accepted_at' => 'datetime',
];
/**
* Get the full contact name
*/
public function getContactFullNameAttribute(): ?string
{
$parts = array_filter([
$this->contact_first_name,
$this->contact_last_name,
]);
return !empty($parts) ? implode(' ', $parts) : null;
}
/**
* Generate a unique invitation token
*/
public static function generateToken(): string
{
do {
$token = Str::random(64);
} while (self::where('token', $token)->exists());
return $token;
}
/**
* Check if invitation is still valid
*/
public function isValid(): bool
{
return $this->status === 'pending' && $this->expires_at->isFuture();
}
/**
* Check if invitation is expired
*/
public function isExpired(): bool
{
return $this->expires_at->isPast() && $this->status === 'pending';
}
/**
* Mark invitation as expired
*/
public function markAsExpired(): void
{
$this->update(['status' => 'expired']);
}
/**
* Accept the invitation
*/
public function accept(Partner $partner): void
{
$this->update([
'status' => 'accepted',
'accepted_at' => now(),
'partner_id' => $partner->id,
]);
}
public function markAsAccepted(Partner $partner): void
{
$this->update([
'status' => 'accepted',
'accepted_at' => now(),
'partner_id' => $partner->id,
]);
}
/**
* Role assigned to the invitation
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* User who sent the invitation
*/
public function invitedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by');
}
/**
* Partner created from this invitation
*/
public function partner(): BelongsTo
{
return $this->belongsTo(Partner::class);
}
/**
* Scope to get pending invitations
*/
public function scopePending($query)
{
return $query->where('status', 'pending')
->where('expires_at', '>', now());
}
/**
* Scope to get expired invitations
*/
public function scopeExpired($query)
{
return $query->where('status', 'pending')
->where('expires_at', '<=', now());
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProductVariant extends Model
{
//
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ShippingClass extends Model
{
//
}

10
app/Models/Tag.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
//
}

10
app/Models/TaxRate.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TaxRate extends Model
{
//
}

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class User extends Authenticatable
{
@ -22,9 +23,11 @@ class User extends Authenticatable
* @var list<string>
*/
protected $fillable = [
'partner_id',
'name',
'email',
'password',
'email_verified_at',
];
/**
@ -50,6 +53,10 @@ class User extends Authenticatable
];
}
public function partner(): BelongsTo
{
return $this->belongsTo(Partner::class);
}
/**
* Get the user's initials
*/
@ -57,7 +64,7 @@ class User extends Authenticatable
{
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('');
}
}

View file

@ -2,6 +2,7 @@
namespace App\Providers;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -19,6 +20,14 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
// Always force HTTPS when request comes via HTTPS
// Important for Livewire signed URLs (file uploads) behind Traefik proxy
$scheme = request()->header('X-Forwarded-Proto')
?? request()->server('HTTP_X_FORWARDED_PROTO')
?? (request()->secure() ? 'https' : 'http');
if ($scheme === 'https') {
URL::forceScheme('https');
}
}
}