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

@ -149,6 +149,9 @@ source ~/.bashrc
curl -fsSL https://claude.ai/install.sh | bash curl -fsSL https://claude.ai/install.sh | bash
echo 'export PATH=\"/root/.local/bin:$PATH\"' >> /home/sail/.bashrc echo 'export PATH=\"/root/.local/bin:$PATH\"' >> /home/sail/.bashrc
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /home/sail/.bashrc
source /home/sail/.bashrc
### Port-Konflikte ### Port-Konflikte

View file

@ -15,6 +15,19 @@ class BasicAuthMiddleware
*/ */
public function handle(Request $request, Closure $next): Response 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 // Credentials from .env file
$user = config('auth.basic.user'); $user = config('auth.basic.user');
$pass = config('auth.basic.password'); $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\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class User extends Authenticatable class User extends Authenticatable
{ {
@ -22,9 +23,11 @@ class User extends Authenticatable
* @var list<string> * @var list<string>
*/ */
protected $fillable = [ protected $fillable = [
'partner_id',
'name', 'name',
'email', 'email',
'password', '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 * Get the user's initials
*/ */
@ -57,7 +64,7 @@ class User extends Authenticatable
{ {
return Str::of($this->name) return Str::of($this->name)
->explode(' ') ->explode(' ')
->map(fn (string $name) => Str::of($name)->substr(0, 1)) ->map(fn(string $name) => Str::of($name)->substr(0, 1))
->implode(''); ->implode('');
} }
} }

View file

@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -19,6 +20,14 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void 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');
}
} }
} }

View file

@ -11,8 +11,14 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
// Partner Setup-Zwang für eingeloggte User
$middleware->alias([
'partner.setup' => \App\Http\Middleware\EnsurePartnerSetupCompleted::class,
]);
// BasicAuth ganz am Ende, nach Session-Middleware
if (env('BASIC_AUTH_ENABLED', true)) { if (env('BASIC_AUTH_ENABLED', true)) {
$middleware->web(\App\Http\Middleware\BasicAuthMiddleware::class); $middleware->append(\App\Http\Middleware\BasicAuthMiddleware::class);
} }
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {

View file

@ -6,4 +6,5 @@ return [
App\Providers\ThemeServiceProvider::class, App\Providers\ThemeServiceProvider::class,
App\Providers\VoltServiceProvider::class, App\Providers\VoltServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class, Barryvdh\Debugbar\ServiceProvider::class,
FluxPro\FluxProServiceProvider::class,
]; ];

View file

@ -16,7 +16,7 @@
"laravel/sanctum": "^4.1", "laravel/sanctum": "^4.1",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1", "livewire/flux": "^2.1.1",
"livewire/flux-pro": "^2.1", "livewire/flux-pro": "^2.6",
"livewire/volt": "^1.7.0", "livewire/volt": "^1.7.0",
"spatie/laravel-permission": "^6.17" "spatie/laravel-permission": "^6.17"
}, },
@ -88,14 +88,18 @@
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
"repositories": [ "repositories": {
{ "flux-pro": {
"type": "composer",
"url": "https://composer.fluxui.dev"
},
"0": {
"type": "path", "type": "path",
"url": "packages/*/*" "url": "packages/*/*"
}, },
{ "1": {
"type": "composer", "type": "composer",
"url": "https://composer.fluxui.dev" "url": "https://composer.fluxui.dev"
} }
] }
} }

391
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "10a392ebee126b2fc1bc1946158acb90", "content-hash": "dec4bfb2c36983f51725d04db995a549",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -633,29 +633,28 @@
}, },
{ {
"name": "dragonmantank/cron-expression", "name": "dragonmantank/cron-expression",
"version": "v3.4.0", "version": "v3.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git", "url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "8c784d071debd117328803d86b2097615b457500" "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013",
"reference": "8c784d071debd117328803d86b2097615b457500", "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2|^8.0", "php": "^8.2|^8.3|^8.4|^8.5"
"webmozart/assert": "^1.0"
}, },
"replace": { "replace": {
"mtdowling/cron-expression": "^1.0" "mtdowling/cron-expression": "^1.0"
}, },
"require-dev": { "require-dev": {
"phpstan/extension-installer": "^1.0", "phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^1.0", "phpstan/phpstan": "^1.12.32|^2.1.31",
"phpunit/phpunit": "^7.0|^8.0|^9.0" "phpunit/phpunit": "^8.5.48|^9.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -686,7 +685,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues", "issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0"
}, },
"funding": [ "funding": [
{ {
@ -694,7 +693,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-10-09T13:47:03+00:00" "time": "2025-10-31T18:51:33+00:00"
}, },
{ {
"name": "egulias/email-validator", "name": "egulias/email-validator",
@ -1309,16 +1308,16 @@
}, },
{ {
"name": "laravel/fortify", "name": "laravel/fortify",
"version": "v1.31.1", "version": "v1.31.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/fortify.git", "url": "https://github.com/laravel/fortify.git",
"reference": "e39a49592e1440508337a765cdc913ff5bcba66f" "reference": "a046d52ee087ee52c9852b840cf4bbad19f10934"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/fortify/zipball/e39a49592e1440508337a765cdc913ff5bcba66f", "url": "https://api.github.com/repos/laravel/fortify/zipball/a046d52ee087ee52c9852b840cf4bbad19f10934",
"reference": "e39a49592e1440508337a765cdc913ff5bcba66f", "reference": "a046d52ee087ee52c9852b840cf4bbad19f10934",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1370,20 +1369,20 @@
"issues": "https://github.com/laravel/fortify/issues", "issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify" "source": "https://github.com/laravel/fortify"
}, },
"time": "2025-10-03T09:10:57+00:00" "time": "2025-10-21T14:47:38+00:00"
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v12.34.0", "version": "v12.37.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687" "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687", "url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125",
"reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687", "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1589,7 +1588,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2025-10-14T13:58:31+00:00" "time": "2025-11-04T15:39:33+00:00"
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
@ -2032,16 +2031,16 @@
}, },
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "3.30.0", "version": "3.30.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem.git", "url": "https://github.com/thephpleague/flysystem.git",
"reference": "2203e3151755d874bb2943649dae1eb8533ac93e" "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da",
"reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2109,9 +2108,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/flysystem/issues", "issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.30.0" "source": "https://github.com/thephpleague/flysystem/tree/3.30.1"
}, },
"time": "2025-06-25T13:29:59+00:00" "time": "2025-10-20T15:35:26+00:00"
}, },
{ {
"name": "league/flysystem-local", "name": "league/flysystem-local",
@ -2394,16 +2393,16 @@
}, },
{ {
"name": "livewire/flux", "name": "livewire/flux",
"version": "v2.6.0", "version": "v2.6.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/flux.git", "url": "https://github.com/livewire/flux.git",
"reference": "3cb2ea40978449da74b3814eeef75f0388124224" "reference": "227b88db0a02db91666af2303ea6727a3af78c51"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/flux/zipball/3cb2ea40978449da74b3814eeef75f0388124224", "url": "https://api.github.com/repos/livewire/flux/zipball/227b88db0a02db91666af2303ea6727a3af78c51",
"reference": "3cb2ea40978449da74b3814eeef75f0388124224", "reference": "227b88db0a02db91666af2303ea6727a3af78c51",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2411,7 +2410,7 @@
"illuminate/support": "^10.0|^11.0|^12.0", "illuminate/support": "^10.0|^11.0|^12.0",
"illuminate/view": "^10.0|^11.0|^12.0", "illuminate/view": "^10.0|^11.0|^12.0",
"laravel/prompts": "^0.1|^0.2|^0.3", "laravel/prompts": "^0.1|^0.2|^0.3",
"livewire/livewire": "^3.5.19", "livewire/livewire": "^3.5.19|^4.0",
"php": "^8.1", "php": "^8.1",
"symfony/console": "^6.0|^7.0" "symfony/console": "^6.0|^7.0"
}, },
@ -2454,26 +2453,26 @@
], ],
"support": { "support": {
"issues": "https://github.com/livewire/flux/issues", "issues": "https://github.com/livewire/flux/issues",
"source": "https://github.com/livewire/flux/tree/v2.6.0" "source": "https://github.com/livewire/flux/tree/v2.6.1"
}, },
"time": "2025-10-13T23:17:18+00:00" "time": "2025-10-28T21:12:05+00:00"
}, },
{ {
"name": "livewire/flux-pro", "name": "livewire/flux-pro",
"version": "2.6.0", "version": "2.6.1",
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://composer.fluxui.dev/download/a01b7791-4494-476d-bfd6-9605f43121a7/flux-pro-2.6.0.zip", "url": "https://composer.fluxui.dev/download/a0397651-df75-43ac-b21a-8a5ac8ad46b4/flux-pro-2.6.1.zip",
"reference": "0b2f0c4523bded72b06a47532cf5db248cfa3072", "reference": "12a6570b061c858739b40a9509424c4b4cc42b62",
"shasum": "7741f0b5bb2e9a69cf3ec20976f40a9f396b770d" "shasum": "10e8f4dad0b0232e5b47ce291ef1c55610be5298"
}, },
"require": { "require": {
"illuminate/console": "^10.0|^11.0|^12.0", "illuminate/console": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0", "illuminate/support": "^10.0|^11.0|^12.0",
"illuminate/view": "^10.0|^11.0|^12.0", "illuminate/view": "^10.0|^11.0|^12.0",
"laravel/prompts": "^0.1.24|^0.2|^0.3", "laravel/prompts": "^0.1.24|^0.2|^0.3",
"livewire/flux": "2.6.0|dev-main", "livewire/flux": "2.6.1|dev-main",
"livewire/livewire": "^3.6.2", "livewire/livewire": "^3.6.2|^4.0",
"php": "^8.1", "php": "^8.1",
"symfony/console": "^6.0|^7.0" "symfony/console": "^6.0|^7.0"
}, },
@ -2513,7 +2512,7 @@
"livewire", "livewire",
"ui" "ui"
], ],
"time": "2025-10-13T23:31:47+00:00" "time": "2025-10-28T21:23:07+00:00"
}, },
{ {
"name": "livewire/livewire", "name": "livewire/livewire",
@ -2593,21 +2592,21 @@
}, },
{ {
"name": "livewire/volt", "name": "livewire/volt",
"version": "v1.7.2", "version": "v1.9.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/volt.git", "url": "https://github.com/livewire/volt.git",
"reference": "91ba934e72bbd162442840862959ade24dbe728a" "reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/volt/zipball/91ba934e72bbd162442840862959ade24dbe728a", "url": "https://api.github.com/repos/livewire/volt/zipball/4b289eef2f15398987a923d9f813cad6a6a19ea4",
"reference": "91ba934e72bbd162442840862959ade24dbe728a", "reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"laravel/framework": "^10.38.2|^11.0|^12.0", "laravel/framework": "^10.38.2|^11.0|^12.0",
"livewire/livewire": "^3.6.1", "livewire/livewire": "^3.6.1|^4.0",
"php": "^8.1" "php": "^8.1"
}, },
"require-dev": { "require-dev": {
@ -2661,7 +2660,7 @@
"issues": "https://github.com/livewire/volt/issues", "issues": "https://github.com/livewire/volt/issues",
"source": "https://github.com/livewire/volt" "source": "https://github.com/livewire/volt"
}, },
"time": "2025-08-06T15:40:50+00:00" "time": "2025-10-30T02:46:00+00:00"
}, },
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
@ -2873,25 +2872,25 @@
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
"version": "v1.3.2", "version": "v1.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/schema.git", "url": "https://github.com/nette/schema.git",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d" "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d", "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nette/utils": "^4.0", "nette/utils": "^4.0",
"php": "8.1 - 8.4" "php": "8.1 - 8.5"
}, },
"require-dev": { "require-dev": {
"nette/tester": "^2.5.2", "nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0", "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.8" "tracy/tracy": "^2.8"
}, },
"type": "library", "type": "library",
@ -2901,6 +2900,9 @@
} }
}, },
"autoload": { "autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [ "classmap": [
"src/" "src/"
] ]
@ -2929,9 +2931,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nette/schema/issues", "issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.3.2" "source": "https://github.com/nette/schema/tree/v1.3.3"
}, },
"time": "2024-10-06T23:10:23+00:00" "time": "2025-10-30T22:57:59+00:00"
}, },
{ {
"name": "nette/utils", "name": "nette/utils",
@ -3024,16 +3026,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.6.1", "version": "v5.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" "reference": "3a454ca033b9e06b63282ce19562e892747449bb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "reference": "3a454ca033b9e06b63282ce19562e892747449bb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3076,37 +3078,37 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
}, },
"time": "2025-08-13T20:13:15+00:00" "time": "2025-10-21T19:32:17+00:00"
}, },
{ {
"name": "nunomaduro/termwind", "name": "nunomaduro/termwind",
"version": "v2.3.1", "version": "v2.3.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nunomaduro/termwind.git", "url": "https://github.com/nunomaduro/termwind.git",
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123" "reference": "eb61920a53057a7debd718a5b89c2178032b52c0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0",
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123", "reference": "eb61920a53057a7debd718a5b89c2178032b52c0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-mbstring": "*", "ext-mbstring": "*",
"php": "^8.2", "php": "^8.2",
"symfony/console": "^7.2.6" "symfony/console": "^7.3.4"
}, },
"require-dev": { "require-dev": {
"illuminate/console": "^11.44.7", "illuminate/console": "^11.46.1",
"laravel/pint": "^1.22.0", "laravel/pint": "^1.25.1",
"mockery/mockery": "^1.6.12", "mockery/mockery": "^1.6.12",
"pestphp/pest": "^2.36.0 || ^3.8.2", "pestphp/pest": "^2.36.0 || ^3.8.4",
"phpstan/phpstan": "^1.12.25", "phpstan/phpstan": "^1.12.32",
"phpstan/phpstan-strict-rules": "^1.6.2", "phpstan/phpstan-strict-rules": "^1.6.2",
"symfony/var-dumper": "^7.2.6", "symfony/var-dumper": "^7.3.4",
"thecodingmachine/phpstan-strict-rules": "^1.0.0" "thecodingmachine/phpstan-strict-rules": "^1.0.0"
}, },
"type": "library", "type": "library",
@ -3149,7 +3151,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/nunomaduro/termwind/issues", "issues": "https://github.com/nunomaduro/termwind/issues",
"source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2"
}, },
"funding": [ "funding": [
{ {
@ -3165,7 +3167,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-05-08T08:14:37+00:00" "time": "2025-10-18T11:10:27+00:00"
}, },
{ {
"name": "paragonie/constant_time_encoding", "name": "paragonie/constant_time_encoding",
@ -3777,16 +3779,16 @@
}, },
{ {
"name": "psy/psysh", "name": "psy/psysh",
"version": "v0.12.12", "version": "v0.12.14",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/bobthecow/psysh.git", "url": "https://github.com/bobthecow/psysh.git",
"reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7" "reference": "95c29b3756a23855a30566b745d218bee690bef2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/cd23863404a40ccfaf733e3af4db2b459837f7e7", "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2",
"reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7", "reference": "95c29b3756a23855a30566b745d218bee690bef2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3801,11 +3803,12 @@
"symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.2" "bamarni/composer-bin-plugin": "^1.2",
"composer/class-map-generator": "^1.6"
}, },
"suggest": { "suggest": {
"composer/class-map-generator": "Improved tab completion performance with better class discovery.",
"ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
"ext-pdo-sqlite": "The doc command requires SQLite to work.",
"ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well."
}, },
"bin": [ "bin": [
@ -3849,9 +3852,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/bobthecow/psysh/issues", "issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.12" "source": "https://github.com/bobthecow/psysh/tree/v0.12.14"
}, },
"time": "2025-09-20T13:46:31+00:00" "time": "2025-10-27T17:15:31+00:00"
}, },
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
@ -4053,16 +4056,16 @@
}, },
{ {
"name": "spatie/laravel-permission", "name": "spatie/laravel-permission",
"version": "6.21.0", "version": "6.23.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/spatie/laravel-permission.git", "url": "https://github.com/spatie/laravel-permission.git",
"reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3" "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3", "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
"reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3", "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4124,7 +4127,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/spatie/laravel-permission/issues", "issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/6.21.0" "source": "https://github.com/spatie/laravel-permission/tree/6.23.0"
}, },
"funding": [ "funding": [
{ {
@ -4132,7 +4135,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-07-23T16:08:05+00:00" "time": "2025-11-03T20:16:13+00:00"
}, },
{ {
"name": "symfony/clock", "name": "symfony/clock",
@ -4210,16 +4213,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.3.4", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7",
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4284,7 +4287,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v7.3.4" "source": "https://github.com/symfony/console/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -4304,7 +4307,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-22T15:31:00+00:00" "time": "2025-10-14T15:46:26+00:00"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",
@ -4681,16 +4684,16 @@
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v7.3.2", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" "reference": "9f696d2f1e340484b4683f7853b273abff94421f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f",
"reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", "reference": "9f696d2f1e340484b4683f7853b273abff94421f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4725,7 +4728,7 @@
"description": "Finds files and directories via an intuitive fluent interface", "description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/finder/tree/v7.3.2" "source": "https://github.com/symfony/finder/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -4745,20 +4748,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-15T13:41:35+00:00" "time": "2025-10-15T18:45:57+00:00"
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v7.3.4", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-foundation.git", "url": "https://github.com/symfony/http-foundation.git",
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce31218c7cac92eab280762c4375fb70a6f4f897",
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4808,7 +4811,7 @@
"description": "Defines an object-oriented layer for the HTTP specification", "description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.3.4" "source": "https://github.com/symfony/http-foundation/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -4828,20 +4831,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-16T08:38:17+00:00" "time": "2025-10-24T21:42:11+00:00"
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
"version": "v7.3.4", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-kernel.git", "url": "https://github.com/symfony/http-kernel.git",
"reference": "b796dffea7821f035047235e076b60ca2446e3cf" "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf", "url": "https://api.github.com/repos/symfony/http-kernel/zipball/24fd3f123532e26025f49f1abefcc01a69ef15ab",
"reference": "b796dffea7821f035047235e076b60ca2446e3cf", "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4926,7 +4929,7 @@
"description": "Provides a structured process for converting a Request into a Response", "description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.3.4" "source": "https://github.com/symfony/http-kernel/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -4946,20 +4949,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-27T12:32:17+00:00" "time": "2025-10-28T10:19:01+00:00"
}, },
{ {
"name": "symfony/mailer", "name": "symfony/mailer",
"version": "v7.3.4", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/mailer.git", "url": "https://github.com/symfony/mailer.git",
"reference": "ab97ef2f7acf0216955f5845484235113047a31d" "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d", "url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba",
"reference": "ab97ef2f7acf0216955f5845484235113047a31d", "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5010,7 +5013,7 @@
"description": "Helps sending emails", "description": "Helps sending emails",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/mailer/tree/v7.3.4" "source": "https://github.com/symfony/mailer/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -5030,7 +5033,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-17T05:51:54+00:00" "time": "2025-10-24T14:27:20+00:00"
}, },
{ {
"name": "symfony/mime", "name": "symfony/mime",
@ -6526,16 +6529,16 @@
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
"version": "v7.3.4", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-dumper.git", "url": "https://github.com/symfony/var-dumper.git",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6589,7 +6592,7 @@
"dump" "dump"
], ],
"support": { "support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4" "source": "https://github.com/symfony/var-dumper/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -6609,7 +6612,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-11T10:12:26+00:00" "time": "2025-09-27T09:00:46+00:00"
}, },
{ {
"name": "tijsverkoyen/css-to-inline-styles", "name": "tijsverkoyen/css-to-inline-styles",
@ -6823,64 +6826,6 @@
} }
], ],
"time": "2024-11-21T01:49:47+00:00" "time": "2024-11-21T01:49:47+00:00"
},
{
"name": "webmozart/assert",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"php": "^7.2 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
},
"time": "2022-06-03T18:03:27+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -7820,16 +7765,16 @@
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",
"version": "v1.46.0", "version": "v1.47.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sail.git", "url": "https://github.com/laravel/sail.git",
"reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e" "reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e", "url": "https://api.github.com/repos/laravel/sail/zipball/9a11e822238167ad8b791e4ea51155d25cf4d8f2",
"reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e", "reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7842,7 +7787,7 @@
}, },
"require-dev": { "require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"phpstan/phpstan": "^1.10" "phpstan/phpstan": "^2.0"
}, },
"bin": [ "bin": [
"bin/sail" "bin/sail"
@ -7879,7 +7824,7 @@
"issues": "https://github.com/laravel/sail/issues", "issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail" "source": "https://github.com/laravel/sail"
}, },
"time": "2025-09-23T13:44:39+00:00" "time": "2025-10-28T13:55:29+00:00"
}, },
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
@ -11008,16 +10953,16 @@
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
"version": "v7.3.3", "version": "v7.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/yaml.git", "url": "https://github.com/symfony/yaml.git",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d" "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d", "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -11060,7 +11005,7 @@
"description": "Loads and dumps YAML files", "description": "Loads and dumps YAML files",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/yaml/tree/v7.3.3" "source": "https://github.com/symfony/yaml/tree/v7.3.5"
}, },
"funding": [ "funding": [
{ {
@ -11080,7 +11025,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-27T11:34:33+00:00" "time": "2025-09-27T09:00:46+00:00"
}, },
{ {
"name": "ta-tikoma/phpunit-architecture-test", "name": "ta-tikoma/phpunit-architecture-test",
@ -11190,6 +11135,64 @@
} }
], ],
"time": "2024-03-03T12:36:25+00:00" "time": "2024-03-03T12:36:25+00:00"
},
{
"name": "webmozart/assert",
"version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
"reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^7.2 || ^8.0"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
"time": "2025-10-29T15:56:20+00:00"
} }
], ],
"aliases": [], "aliases": [],

View file

@ -17,8 +17,8 @@ return [
] ]
], ],
'hero' => [ 'hero' => [
'title' => 'Lokaler Handel trifft auf <span class="text-secondary">europäisches Design</span>', 'title' => '<span class="text-secondary">B2in</span> Brücken zwischen lokalen Kunden, lokalem Handel und <span class="text-secondary">inspirierenden Designs.</span>',
'subtitle' => '<strong>B2in (Brigdes 2 international)</strong> ist die zentrale B2B-Plattform, die kuratierte Möbel-Hersteller, lokale Fachexperten und kaufkräftige Kunden <strong>zum gegenseitigen Erfolg</strong> verbindet.', 'subtitle' => 'Wo lokaler Handel und namhafte Lieferanten digital, aber lokal, in einem Netzwerk zusammenarbeiten.',
'image' => 'b2in/hero-room.jpg', 'image' => 'b2in/hero-room.jpg',
'image_alt' => 'Modern international skyline showcasing architectural design', 'image_alt' => 'Modern international skyline showcasing architectural design',
'cta1_text' => 'Für lokale Händler', 'cta1_text' => 'Für lokale Händler',
@ -30,7 +30,7 @@ return [
'Persönlicher Service', 'Persönlicher Service',
'Werte die bleiben' 'Werte die bleiben'
], ],
'card_title' => '', 'card_title' => 'B2in',
'card_text' => 'Connecting Design and <span class="text-secondary">Property</span>' 'card_text' => 'Connecting Design and <span class="text-secondary">Property</span>'
], ],
'ecosystem_core' => [ 'ecosystem_core' => [
@ -57,9 +57,9 @@ return [
'vision_section' => [ 'vision_section' => [
'title' => 'Gebaut auf Vertrauen', 'title' => 'Gebaut auf Vertrauen',
'paragraphs' => [ 'paragraphs' => [
'<strong>Unsere Mission ist es, eine Brücke zu bauen:</strong> Wir geben dem lokalen Fachexperten die digitalen Werkzeuge an die Hand, um seine Stärke auszuspielen.', '<strong>B2in (Bridges2international)</strong> verbindet Immobilienmakler, Möbelfachhändler, Möbelhersteller und Markenpartner auf einer gemeinsamen Plattform.',
'Gleichzeitig bieten wir visionären Herstellern einen kuratierten, direkten Zugang zu regionalen Märkten.', '<strong>Unser Ziel:</strong> den Kunden zu Hause abzuholen, den lokalen Handel zu stärken, partnerschaftliche Kooperationen zu fördern und alle Welten digital miteinander zu verbinden ohne dabei die lokalen Wurzeln aus den Augen zu verlieren.',
'Wir glauben an die Kraft der Synergie, nicht an den Wettbewerb zwischen Online und Offline.' '<strong>So entsteht ein Netzwerk, das Nähe schafft</strong> regional verwurzelt, europaweit vernetzt und auf nachhaltigen Erfolg ausgerichtet ist.',
], ],
'image' => 'b2in/marcel-scheibe.jpg', 'image' => 'b2in/marcel-scheibe.jpg',
'image_alt' => 'Professionelles Team in kollaborativem Meeting', 'image_alt' => 'Professionelles Team in kollaborativem Meeting',
@ -101,9 +101,9 @@ return [
'Unser einzigartiges Modell schafft einen Marktplatz, den es so vorher nicht gab. Der Kunde wählt seine Region und erhält eine integrierte Ansicht: Zuerst die Angebote der lokalen Fachexperten, ergänzt durch das exklusive Sortiment unserer europäischen Hersteller.', 'Unser einzigartiges Modell schafft einen Marktplatz, den es so vorher nicht gab. Der Kunde wählt seine Region und erhält eine integrierte Ansicht: Zuerst die Angebote der lokalen Fachexperten, ergänzt durch das exklusive Sortiment unserer europäischen Hersteller.',
'Das Ergebnis ist <strong>maximale Auswahl für den Kunden</strong> und <strong>maximaler Erfolg für unsere Partner</strong>.', 'Das Ergebnis ist <strong>maximale Auswahl für den Kunden</strong> und <strong>maximaler Erfolg für unsere Partner</strong>.',
], ],
'image' => 'b2in/integriertes-model.jpg', 'image' => 'b2in/best-of-two-worlds.jpg',
'image_alt' => 'Integriertes Modell', 'image_alt' => 'Das Ergebnis für den Kunden, das perfekte Zuhause',
'image_caption' => 'Integriertes Modell' 'image_caption' => 'Das Ergebnis für den Kunden, das perfekte Zuhause'
], ],
'cta_section' => [ 'cta_section' => [
'title' => ' Werden <span class="text-primary">Sie Partner</span><br>im führenden Möbel-Netzwerk', 'title' => ' Werden <span class="text-primary">Sie Partner</span><br>im führenden Möbel-Netzwerk',
@ -203,14 +203,16 @@ return [
] ]
], ],
'about_hero' => [ 'about_hero' => [
'title' => 'Über <span class="text-secondary">B2in</span>', 'title' => '<span class="text-secondary">Über B2in:</span> Unsere Mission',
'quote' => '"Unsere Vision ist es, Unternehmen durch innovative Konnektivitätslösungen zu verbinden und nachhaltiges Wachstum in der digitalen Welt zu ermöglichen. Bei B2in schaffen wir nicht nur Verbindungen wir bauen Brücken in die Zukunft."', 'quote' => '"Unsere Mission ist es, die Zukunft des lokalen Möbelhandels zu sichern. Wir geben dem Fachexperten vor Ort die digitalen Werkzeuge, um gegen die Dominanz der Online-Giganten zu bestehen.<br><br> Bei B2in bauen wir nicht nur Verbindungen wir bauen Brücken zwischen europäischem Design, regionaler Expertise und dem Zuhause der Menschen."',
'founder_name' => 'Marcel Scheibe', 'founder_name' => 'Marcel Scheibe',
'founder_title' => 'Gründer & CEO, B2in', 'founder_title' => 'Gründer & CEO, B2in',
'image' => 'b2in/about-hero.jpg', 'image' => 'b2in/about-hero.jpg',
'image_alt' => 'Marcel Scheibe, Gründer und CEO von B2in', 'image_alt' => 'Marcel Scheibe, Gründer und CEO von B2in',
'year' => '2024',
'year_text' => 'Gründungsjahr' 'card_title' => 'B2in',
'card_text' => 'Connecting Design and <span class="text-secondary">Property</span>'
], ],
'broker_section' => [ 'broker_section' => [
'title' => 'Lifetime <span class="text-secondary">Vergütung</span> für Makler', 'title' => 'Lifetime <span class="text-secondary">Vergütung</span> für Makler',
@ -287,7 +289,7 @@ return [
'image_alt' => 'Luxury interior design' 'image_alt' => 'Luxury interior design'
], ],
'ecosystem_hero' => [ 'ecosystem_hero' => [
'title' => 'B2in: Wie unser <span class="text-secondary">Ökosystem</span> Wachstum für alle <span class="text-secondary">Partner</span> generiert', 'title' => 'Wie unser <span class="text-secondary">Ökosystem</span> Wachstum für alle <span class="text-secondary">Partner</span> generiert',
'subtitle' => 'Ein intelligentes Netzwerk, das Endkunden, Händler, Lieferanten, Makler und Technologie nahtlos miteinander verbindet. Jeder Teilnehmer profitiert vom gesamten System und schafft gemeinsam außergewöhnliche Möbel- und Immobilienerlebnisse.', 'subtitle' => 'Ein intelligentes Netzwerk, das Endkunden, Händler, Lieferanten, Makler und Technologie nahtlos miteinander verbindet. Jeder Teilnehmer profitiert vom gesamten System und schafft gemeinsam außergewöhnliche Möbel- und Immobilienerlebnisse.',
'features' => [ 'features' => [
[ [
@ -314,16 +316,15 @@ return [
'image' => 'b2in/ecosystem-hero.jpg', 'image' => 'b2in/ecosystem-hero.jpg',
'image_alt' => 'Ecosystem Hero Image', 'image_alt' => 'Ecosystem Hero Image',
'card_title' => 'B2in Portal', 'card_title' => 'B2in Portal',
'card_text' => 'Zentrale Plattform', 'card_text' => 'Die Technologie dahinter, die das Ökosystem ermöglicht',
'hub' => [ 'hub' => [
'title' => 'B2in Portal', 'title' => 'B2in Portal',
'subtitle' => 'Zentrale Plattform' 'subtitle' => 'Die Technologie dahinter, die das Ökosystem ermöglicht'
], ],
'connection_points' => [ 'stats' => [
['name' => 'Endkunden'], 'Exklusive Auswahl',
['name' => 'Makler'], 'Persönlicher Service',
['name' => 'Lieferanten'], 'Werte die bleiben'
['name' => 'B2A']
] ]
], ],
'ecosystem_stats' => [ 'ecosystem_stats' => [
@ -358,9 +359,9 @@ return [
'Unser Ökosystem startet nicht bei Ihnen, sondern beim Endkunden.', 'Unser Ökosystem startet nicht bei Ihnen, sondern beim Endkunden.',
'Unsere reichweitenstarken Marken <span class="text-secondary font-bold">style2own</span> und <span class="text-secondary font-bold">stileigentum</span> schaffen durch Inspiration und exklusive Konzepte eine kontinuierliche, kaufkräftige Nachfrage nach hochwertigen Möbeln.', 'Unsere reichweitenstarken Marken <span class="text-secondary font-bold">style2own</span> und <span class="text-secondary font-bold">stileigentum</span> schaffen durch Inspiration und exklusive Konzepte eine kontinuierliche, kaufkräftige Nachfrage nach hochwertigen Möbeln.',
], ],
'image' => 'b2in/integriertes-model.jpg', 'image' => 'b2in/ecosystem_start.jpg',
'image_alt' => 'Die Marken', 'image_alt' => 'Die Marken für den Endkunden',
'image_caption' => 'Die Marken', 'image_caption' => 'Die Marken für den Endkunden',
'image_description' => 'Eine stilisierte Grafik, die zeigt, wie von den Logos style2own und stileigentum Pfeile in den nächsten Abschnitt führen.', 'image_description' => 'Eine stilisierte Grafik, die zeigt, wie von den Logos style2own und stileigentum Pfeile in den nächsten Abschnitt führen.',
], ],
'ecosystem_hub' => [ 'ecosystem_hub' => [
@ -369,10 +370,10 @@ return [
'Sobald ein Kunde seine Region wählt (z.B. Bielefeld), spielt unsere Plattform ihre Stärke aus. Dank der <span class="text-secondary font-bold">"Local First"-Logik</span> werden die Angebote unserer <span class="text-secondary font-bold">lokalen Händler</span> prominent platziert.', 'Sobald ein Kunde seine Region wählt (z.B. Bielefeld), spielt unsere Plattform ihre Stärke aus. Dank der <span class="text-secondary font-bold">"Local First"-Logik</span> werden die Angebote unserer <span class="text-secondary font-bold">lokalen Händler</span> prominent platziert.',
'Gleichzeitig wird das Sortiment durch die exklusiven Produkte unserer <span class="text-secondary font-bold">europäischen Hersteller</span> ergänzt. So entsteht eine unschlagbare Auswahl.', 'Gleichzeitig wird das Sortiment durch die exklusiven Produkte unserer <span class="text-secondary font-bold">europäischen Hersteller</span> ergänzt. So entsteht eine unschlagbare Auswahl.',
], ],
'image' => 'b2in/integriertes-model.jpg', 'image' => 'b2in/ecosystem_hub.jpg',
'image_alt' => 'Das B2In-Erlebnis', 'image_alt' => 'Die Synergie zwischen lokalem und überregionalem Angebot',
'image_caption' => 'Das B2In-Erlebnis', 'image_caption' => 'Die Synergie zwischen lokalem und überregionalem Angebot',
'image_description' => 'Eine klare Infografik, die zeigt: [Lokales Angebot] + [Überregionales Angebot] = Das B2In-Erlebnis..', 'image_description' => 'Eine klare Infografik, die zeigt: [Lokales Angebot] + [Überregionales Angebot] = Die Synergie zwischen lokalem und überregionalem Angebot.',
], ],
'ecosystem_result' => [ 'ecosystem_result' => [
'title' => 'Ein Kreislauf, in dem <span class="text-secondary">jeder gewinnt</span>', 'title' => 'Ein Kreislauf, in dem <span class="text-secondary">jeder gewinnt</span>',
@ -393,10 +394,10 @@ return [
'title' => 'Der Makler, der den Kunden ursprünglich vermittelt hat, erhält eine faire Provision und hat seinem Kunden einen unschätzbaren Mehrwert geboten.' 'title' => 'Der Makler, der den Kunden ursprünglich vermittelt hat, erhält eine faire Provision und hat seinem Kunden einen unschätzbaren Mehrwert geboten.'
], ],
], ],
'image' => 'b2in/integriertes-model.jpg', 'image' => 'b2in/ecosystem_result.jpg',
'image_alt' => 'style2own und stileigentum', 'image_alt' => 'Ihr Erfolg und die Partnerschaft mit B2in',
'image_caption' => 'style2own und stileigentum', 'image_caption' => 'Ihr Erfolg und die Partnerschaft mit B2in',
'image_description' => 'Eine stilisierte Grafik, die zeigt, wie von den Logos style2own und stileigentum Pfeile in den nächsten Abschnitt führen.', 'image_description' => 'Eine stilisierte Grafik, die zeigt, wie Ihr Erfolg und die Partnerschaft mit B2in zusammenhängen.',
], ],
'end_customer_section' => [ 'end_customer_section' => [
'tag' => 'Für Endkunden', 'tag' => 'Für Endkunden',
@ -496,18 +497,21 @@ return [
'timeline' => [ 'timeline' => [
[ [
'title' => 'Die Idee', 'title' => 'Die Idee',
'description' => '2024 erkannten wir eine kritische Marktlücke: Unternehmen benötigten intelligente, nachhaltige Konnektivitätslösungen für die digitale Transformation.', 'description' => '2024 erkannten wir eine entscheidende Lücke im Möbelmarkt: Während Online-Riesen wachsen, kämpft der lokale Fachhandel um seine digitale Sichtbarkeit. Gleichzeitig suchen Kunden nach kuratierter Qualität und persönlichem Service.',
'icon' => 'light-bulb',
], ],
[ [
'title' => 'Die Mission', 'title' => 'Die Mission',
'description' => 'Wir entwickelten innovative B2B-Lösungen, die Unternehmen dabei unterstützen, effizienter zu arbeiten und nachhaltiges Wachstum zu erzielen.', 'description' => 'Wir entwickeln eine Plattform, die das Beste aus beiden Welten vereint: die Stärke des lokalen Handels und die Vielfalt des europäischen Designs. Unser Ziel ist es, faire, regionale Ökosysteme zu schaffen, in denen Technologie dem Menschen dient.',
'icon' => 'rocket-launch',
], ],
[ [
'title' => 'Die Zukunft', 'title' => 'Die Zukunft',
'description' => 'Heute sind wir stolz darauf, hunderte Unternehmen dabei zu unterstützen, ihre digitalen Ziele zu erreichen und neue Märkte zu erschließen.', 'description' => 'Heute bauen wir ein wachsendes Netzwerk regionaler Hubs auf. Unsere Vision ist es, in jeder größeren Region Europas der führende digitale Partner für den lokalen Möbel- und Designhandel zu werden.',
'icon' => 'globe-alt',
], ],
], ],
'summary' => 'Was als Vision begann, traditionelle Geschäftsprozesse zu revolutionieren, ist heute eine bewährte Plattform für digitale Innovation. B2in schließt die Lücke zwischen traditionellen Unternehmen und modernen, digitalen Lösungen durch maßgeschneiderte Konnektivitätsservices, die Effizienz steigern und nachhaltiges Wachstum fördern.' 'summary' => 'Was als Vision begann, den lokalen Handel zu stärken, ist heute die zentrale B2B-Plattform für den kuratierten Möbelhandel. Wir schließen die Lücke zwischen Online-Nachfrage und Offline-Expertise und schaffen so nachhaltiges Wachstum für unsere Partner.'
], ],
'our_values' => [ 'our_values' => [
'title' => 'Unsere <span class="text-secondary">Werte</span>', 'title' => 'Unsere <span class="text-secondary">Werte</span>',
@ -515,37 +519,32 @@ return [
'values' => [ 'values' => [
[ [
'title' => 'Innovation', 'title' => 'Innovation',
'description' => 'Wir entwickeln kontinuierlich neue Lösungen, die unseren Kunden einen Wettbewerbsvorteil verschaffen und die Branche voranbringen.', 'description' => 'Wir entwickeln digitale Lösungen, die dem lokalen Möbelhandel einen echten Wettbewerbsvorteil in einer sich schnell verändernden Welt verschaffen.',
'icon' => 'light-bulb', 'icon' => 'light-bulb',
'icon_style' => 'solid',
], ],
[ [
'title' => 'Konnektivität', 'title' => 'Konnektivität',
'description' => 'Wir verbinden Märkte, Technologien und Menschen, um Synergien zu schaffen und ein nahtloses, globales Ökosystem zu bilden.', 'description' => 'Wir verbinden nicht nur Systeme wir verbinden den Online-Kunden wieder mit dem Fachexperten in seiner Stadt und europäische Manufakturen mit neuen Märkten.',
'icon' => 'globe-alt', 'icon' => 'globe-alt',
'icon_style' => 'solid',
], ],
[ [
'title' => 'Qualität', 'title' => 'Qualität',
'description' => 'Wir setzen kompromisslose Standards in allen Bereichen von der Technologie über das Design bis hin zum partnerschaftlichen Service.', 'description' => 'Wir setzen kompromisslose Standards bei der Auswahl unserer Partner, der Kuration der Möbel und der Technologie, die alles zusammenhält.',
'icon' => 'check-badge', 'icon' => 'check-badge',
'icon_style' => 'solid',
], ],
[ [
'title' => 'Vertrauen', 'title' => 'Vertrauen',
'description' => 'Transparente Prozesse und verlässliche Partnerschaften bilden das Fundament unserer Zusammenarbeit und unseres Erfolgs.', 'description' => 'Transparente Provisionsmodelle und verlässliche Partnerschaften sind das Fundament unseres Ökosystems. Wir wachsen nur, wenn unsere Partner wachsen.',
'icon' => 'user-group', 'icon' => 'user-group',
'icon_style' => 'solid',
], ],
[ [
'title' => 'Nachhaltigkeit', 'title' => 'Nachhaltigkeit',
'description' => 'Wir übernehmen Verantwortung, indem wir auf langlebige Qualität, ressourcenschonende Prozesse und zukunftsfähige Konzepte setzen.', 'description' => 'Wir übernehmen Verantwortung, indem wir durch unsere Bündel-Logistik Transportwege optimieren und den lokalen Handel stärken, um lebendige Innenstädte zu erhalten.',
'icon' => 'arrow-path', 'icon' => 'arrow-path',
'icon_style' => 'solid',
], ],
[ [
'title' => 'Design-Exzellenz', 'title' => 'Design-Exzellenz',
'description' => 'Design ist der Kern unserer Wertschöpfung von der kuratierten Auswahl an Immobilien bis zur intuitiven Gestaltung unserer digitalen Plattform.', 'description' => 'Design ist der Kern unserer Wertschöpfung von der kuratierten Auswahl an Möbeln bis zur intuitiven Gestaltung unserer digitalen Plattform.',
'icon' => 'cube-transparent', 'icon' => 'cube-transparent',
], ],
] ]
@ -883,19 +882,19 @@ return [
[ [
'name' => 'Marcel Scheibe', 'name' => 'Marcel Scheibe',
'position' => 'Gründer & CEO', 'position' => 'Gründer & CEO',
'expertise' => 'Visionär für digitale Transformation und strategische Unternehmensführung.', 'expertise' => 'Visionär für die digitale Zukunft des lokalen Handels und strategischer Brückenbauer zwischen den USA und Europa.',
'image' => 'b2in/marcel-scheibe.jpg', 'image' => 'b2in/marcel-scheibe.jpg',
], ],
[ [
'name' => 'Sarah Müller', 'name' => 'Sarah Müller',
'position' => 'Head of Operations', 'position' => 'Head of Operations',
'expertise' => 'Expertin für Prozessoptimierung und operative Exzellenz in B2B-Umgebungen.', 'expertise' => 'Expertin für die Optimierung unserer europaweiten Logistikprozesse und die operative Exzellenz unserer regionalen Hubs.',
'image' => 'b2in/sarah-mueller.jpg', 'image' => 'b2in/sarah-mueller.jpg',
], ],
[ [
'name' => 'Thomas Weber', 'name' => 'Thomas Weber',
'position' => 'Head of Technology', 'position' => 'Head of Technology',
'expertise' => 'Technologieführer mit Fokus auf innovative Konnektivitätslösungen.', 'expertise' => 'Technologieführer mit Fokus auf eine intuitive, skalierbare Plattform-Architektur, die Händler und Hersteller nahtlos verbindet.',
'image' => 'b2in/thomas-weber.jpg', 'image' => 'b2in/thomas-weber.jpg',
], ],
] ]
@ -2223,7 +2222,7 @@ return [
'articles' => [ 'articles' => [
1 => [ 1 => [
'id' => 1, 'id' => 1,
'title' => 'Die Zukunft des Home Staging: Mehr als nur Möbelrücken', 'title' => ' <span class="text-secondary">Die Zukunft des Home Staging:</span><br>Mehr als nur Möbelrücken',
'subtitle' => 'Wie Technologie, Nachhaltigkeit und personalisierte Konzepte die Immobilienvermarktung revolutionieren und den Wert Ihrer Objekte maximieren.', 'subtitle' => 'Wie Technologie, Nachhaltigkeit und personalisierte Konzepte die Immobilienvermarktung revolutionieren und den Wert Ihrer Objekte maximieren.',
'image' => 'b2in/magazin-1.jpg', 'image' => 'b2in/magazin-1.jpg',
'category' => 'Immobilien-Marketing', 'category' => 'Immobilien-Marketing',
@ -2254,7 +2253,7 @@ return [
], ],
2 => [ 2 => [
'id' => 2, 'id' => 2,
'title' => 'Jenseits der Lage: Warum technologiegetriebene Immobilien die Zukunft des Investments sind', 'title' => '<span class="text-secondary">Jenseits der Lage:</span><br>Warum technologiegetriebene Immobilien die Zukunft des Investments sind',
'subtitle' => 'Während "Lage, Lage, Lage" ein Klassiker bleibt, definieren Daten, Konnektivität und flexible Nutzungskonzepte heute die wahre Rendite eines Objekts.', 'subtitle' => 'Während "Lage, Lage, Lage" ein Klassiker bleibt, definieren Daten, Konnektivität und flexible Nutzungskonzepte heute die wahre Rendite eines Objekts.',
'image' => 'b2in/magazin-2.jpg', 'image' => 'b2in/magazin-2.jpg',
'category' => 'Investment & Trends', 'category' => 'Investment & Trends',
@ -2285,7 +2284,7 @@ return [
], ],
3 => [ 3 => [
'id' => 3, 'id' => 3,
'title' => 'Europäisches Design erobert den US-Markt: Eine Chance für visionäre Händler', 'title' => '<span class="text-secondary">Europäisches Design erobert den US-Markt:</span><br>Eine Chance für visionäre Händler',
'subtitle' => 'Minimalismus, Handwerkskunst und Nachhaltigkeit warum amerikanische Konsumenten sich zunehmend für europäische Möbel begeistern und wie Händler davon profitieren können.', 'subtitle' => 'Minimalismus, Handwerkskunst und Nachhaltigkeit warum amerikanische Konsumenten sich zunehmend für europäische Möbel begeistern und wie Händler davon profitieren können.',
'image' => 'b2in/magazin-3.jpg', 'image' => 'b2in/magazin-3.jpg',
'category' => 'B2B & Handel', 'category' => 'B2B & Handel',

View file

@ -38,7 +38,7 @@ return [
| |
*/ */
'layout' => 'components.layouts.auth', 'layout' => 'components.layouts.app',
/* /*
|--------------------------------------------------------------------------- |---------------------------------------------------------------------------
@ -69,9 +69,22 @@ return [
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', 'png',
'mov', 'avi', 'wmv', 'mp3', 'm4a', 'gif',
'jpg', 'jpeg', 'mpga', 'webp', 'wma', 'bmp',
'svg',
'wav',
'mp4',
'mov',
'avi',
'wmv',
'mp3',
'm4a',
'jpg',
'jpeg',
'mpga',
'webp',
'wma',
], ],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...

View file

@ -0,0 +1,136 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), new Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'));
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), new Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'));
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

View file

@ -0,0 +1,26 @@
<?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::create('hubs', function (Blueprint $table) {
$table->id();
$table->string('name'); // z.B. "OWL" oder "Rhein-Main"
$table->string('slug')->unique(); // Für saubere URLs, z.B. "owl"
$table->string('keyvisual_url')->nullable(); // Keyvisual-Bild des Hubs
$table->string('emblem_url')->nullable(); // Wappen-URL des Hubs
$table->boolean('is_active')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('hubs');
}
};

View file

@ -0,0 +1,34 @@
<?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::create('hub_locations', function (Blueprint $table) {
$table->id();
// Beziehung zum Hub
$table->foreignId('hub_id')->constrained()->onDelete('cascade');
$table->string('city_name')->nullable(); // z.B. "Bielefeld"
$table->string('zip_code'); // z.B. "33602"
// Index für schnelle PLZ-Suche
$table->index('zip_code');
// Verhindert doppelte PLZ-Einträge pro Hub
$table->unique(['hub_id', 'zip_code']);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('hub_locations');
}
};

View file

@ -0,0 +1,44 @@
<?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::create('partners', function (Blueprint $table) {
$table->id();
$table->string('company_name');
$table->string('slug')->unique();
// Partner-Typ (gemäß Wissensbasis)
$table->string('type'); // 'Retailer', 'Manufacturer', 'Estate-Agent'
// Hub-Zugehörigkeit (kann NULL sein für "globale" Hersteller)
$table->foreignId('hub_id')->nullable()->constrained()->onDelete('set null');
$table->text('description')->nullable();
$table->string('logo_url')->nullable();
$table->boolean('is_active')->default(false);
// Spezifische Felder für 'Retailer' (Händler)
$table->integer('delivery_radius_km')->nullable();
$table->integer('assembly_radius_km')->nullable();
// Flexible Provisions-Engine (pro Partner)
// Speichert den Wert in Cents, um Rundungsfehler zu vermeiden
$table->integer('provision_fixed_amount')->nullable();
// Speichert als 10.5 für 10.5%
$table->decimal('provision_rate_percentage', 5, 2)->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('partners');
}
};

View file

@ -0,0 +1,28 @@
<?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::table('roles', function (Blueprint $table) {
$table->string('color', 50)->default('zinc')->after('guard_name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('color');
});
}
};

View file

@ -0,0 +1,40 @@
<?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) {
// Verknüpft einen User mit einer Partner-Firma
// 'after' ist optional, aber gut für die DB-Lesbarkeit
$table->foreignId('partner_id')
->nullable()
->after('id')
->constrained('partners')
->onDelete('set null'); // Wenn Partner gelöscht wird, bleibt User bestehen
});
}
public function down(): void
{
// Entferne Foreign Key zuerst
try {
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['partner_id']);
});
} catch (\Exception $e) {
// Foreign Key existiert nicht oder hat anderen Namen - weitermachen
}
// Entferne Spalte
if (Schema::hasColumn('users', 'partner_id')) {
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('partner_id');
});
}
}
};

View file

@ -0,0 +1,28 @@
<?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('attributes', function (Blueprint $table) {
$table->id();
$table->string('name'); // z.B. "Farbe", "Größe", "Material"
$table->string('slug')->unique();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('attributes');
}
};

View file

@ -0,0 +1,29 @@
<?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('attribute_values', function (Blueprint $table) {
$table->id();
$table->foreignId('attribute_id')->constrained('attributes')->onDelete('cascade');
$table->string('value'); // z.B. "Anthrazit", "3-Sitzer", "Eiche"
$table->string('slug')->unique();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('attribute_values');
}
};

View file

@ -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('media', function (Blueprint $table) {
$table->id();
$table->string('model_type'); // "App\Models\Product" ODER "App\Models\ProductVariant"
$table->unsignedBigInteger('model_id');
$table->string('file_path');
$table->string('type')->default('image'); // 'image', 'video', 'pdf', '3d_model'
$table->string('alt_text')->nullable();
$table->integer('order_column')->default(0);
$table->timestamps();
$table->index(['model_type', 'model_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('media');
}
};

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
// database/migrations/xxxx_create_brands_table.php
public function up(): void
{
Schema::create('brands', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('logo_url')->nullable();
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('brands');
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
// database/migrations/xxxx_create_collections_table.php
public function up(): void
{
Schema::create('collections', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('collections');
}
};

View file

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
// database/migrations/xxxx_create_categories_table.php
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
// Für Baumstruktur (z.B. "Sofas" gehört zu "Wohnzimmer")
$table->foreignId('parent_id')
->nullable()
->constrained('categories')
->onDelete('set null');
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('categories');
}
};

View file

@ -0,0 +1,30 @@
<?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('tax_rates', function (Blueprint $table) {
$table->id();
$table->string('name'); // z.B. "Regelsatz", "Ermäßigt"
$table->decimal('rate_percentage', 5, 2); // z.B. 19.00, 7.00
$table->boolean('is_default')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tax_rates');
}
};

View file

@ -0,0 +1,29 @@
<?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('shipping_classes', function (Blueprint $table) {
$table->id();
$table->string('name'); // z.B. "Paketversand", "Spedition (2-Mann)"
$table->text('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('shipping_classes');
}
};

View file

@ -0,0 +1,28 @@
<?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('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tags');
}
};

View file

@ -0,0 +1,61 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
// database/migrations/xxxx_create_products_table.php
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('partner_id')->constrained('partners')->onDelete('cascade');
// NEU: Verknüpfung zu Marke/Hersteller
$table->foreignId('brand_id')->nullable()->constrained('brands')->onDelete('set null');
// NEU: Verknüpfung zu Kollektion/Serie
$table->foreignId('collection_id')->nullable()->constrained('collections')->onDelete('set null');
$table->string('name'); // z.B. "3-Sitzer Sofa 'Stockholm'"
$table->string('slug')->unique();
$table->string('status')->default('draft'); // draft, active, archived
$table->text('description_short')->nullable();
$table->text('description_long')->nullable();
$table->text('care_instructions')->nullable();
// Basis-Maße (können von Varianten überschrieben werden, wenn nötig)
$table->integer('width_cm')->nullable();
$table->integer('height_cm')->nullable();
$table->integer('depth_cm')->nullable();
// PERFEKTER ANWENDUNGSFALL FÜR JSON:
// Spezifische Maße, nach denen nie gefiltert wird.
$table->json('dimensions_specific')->nullable();
// Bsp: {"seat_height_cm": 45, "seat_depth_cm": 60}
// Logistik-Basis
$table->string('assembly_status')->nullable(); // z.B. 'flat_pack', 'partially_assembled', 'fully_assembled'
// SEO
$table->string('meta_title')->nullable();
$table->text('meta_description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};

View file

@ -0,0 +1,62 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
// database/migrations/xxxx_create_product_variants_table.php
public function up(): void
{
Schema::create('product_variants', function (Blueprint $table) {
$table->id();
// Jede Variante gehört zu einem "Parent"-Produkt
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
$table->string('name_suffix')->nullable(); // z.B. "Anthrazit / 3-Sitzer"
$table->boolean('is_master_variant')->default(false); // Die "Standard"-Variante
// Eindeutige IDs
$table->string('sku')->unique(); // Interne SKU
$table->string('han_mpn')->nullable()->index(); // Hersteller-Nr.
$table->string('ean_gtin')->nullable()->index(); // Barcode
// Preis-Logik (in Cents)
$table->integer('selling_price'); // VK
$table->integer('msrp')->nullable(); // UVP (Streichpreis)
$table->integer('purchase_price')->nullable(); // EK
$table->foreignId('tax_rate_id')->constrained('tax_rates');
// Lager-Logik
$table->integer('stock_quantity')->default(0);
$table->integer('stock_min_threshold')->nullable(); // Meldebestand
$table->string('availability_status')->nullable(); // z.B. 'in_stock', 'out_of_stock'
$table->string('delivery_time_text')->nullable(); // z.B. "6-8 Wochen" (ersetzt altes Feld)
// WICHTIG: Re-Integration des Mietmodells aus dem Initial-Briefing
$table->boolean('is_rentable')->default(false);
$table->json('rental_duration_options')->nullable(); // [6, 12, 24]
$table->string('rental_rate_formula')->nullable();
$table->decimal('residual_value_percentage', 5, 2)->nullable();
// Variante-spezifische Maße & Gewicht (falls abweichend vom Parent)
$table->integer('variant_weight_g')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_variants');
}
};

View file

@ -0,0 +1,30 @@
<?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
{
// ... create_product_variant_attributes_table.php
Schema::create('product_variant_attributes', function (Blueprint $table) {
// Composite Primary Key
$table->foreignId('product_variant_id')->constrained('product_variants')->onDelete('cascade');
$table->foreignId('attribute_value_id')->constrained('attribute_values')->onDelete('cascade');
$table->primary(['product_variant_id', 'attribute_value_id'], 'variant_attribute_primary');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_variant_attributes');
}
};

View file

@ -0,0 +1,40 @@
<?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('partner_invitations', function (Blueprint $table) {
$table->id();
$table->string('company_name');
$table->enum('partner_type', ['Retailer', 'Manufacturer', 'Estate-Agent', 'Customer'])->default('Retailer');
$table->string('email');
$table->string('token', 64)->unique();
$table->enum('status', ['pending', 'accepted', 'expired', 'cancelled'])->default('pending');
$table->timestamp('expires_at');
$table->foreignId('invited_by')->constrained('users')->onDelete('cascade');
$table->foreignId('partner_id')->nullable()->constrained('partners')->onDelete('set null');
$table->timestamp('accepted_at')->nullable();
$table->timestamps();
$table->index(['token', 'status']);
$table->index(['email', 'status']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Foreign Keys werden automatisch mit der Tabelle gelöscht
Schema::dropIfExists('partner_invitations');
}
};

View file

@ -0,0 +1,30 @@
<?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('category_product', function (Blueprint $table) {
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->foreignId('product_id')->constrained()->onDelete('cascade');
// Primärschlüssel aus beiden IDs
$table->primary(['category_id', 'product_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('category_product');
}
};

View file

@ -0,0 +1,30 @@
<?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('product_tag', function (Blueprint $table) {
$table->foreignId('product_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
// Primärschlüssel aus beiden IDs
$table->primary(['product_id', 'tag_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_tag');
}
};

View file

@ -0,0 +1,31 @@
<?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('related_products', function (Blueprint $table) {
// Das "Basis"-Produkt
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
// Das "verknüpfte" Produkt
$table->foreignId('related_product_id')->constrained('products')->onDelete('cascade');
$table->primary(['product_id', 'related_product_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('related_products');
}
};

View file

@ -0,0 +1,38 @@
<?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('product_logistics', function (Blueprint $table) {
$table->id();
// 1-zu-1 Beziehung zur Variante
$table->foreignId('product_variant_id')->constrained('product_variants')->onDelete('cascade');
$table->foreignId('shipping_class_id')->nullable()->constrained('shipping_classes');
// Verpackungsmaße
$table->integer('package_width_cm')->nullable();
$table->integer('package_height_cm')->nullable();
$table->integer('package_depth_cm')->nullable();
$table->integer('package_weight_g')->nullable(); // Bruttogewicht
$table->integer('package_count')->default(1); // Anzahl Packstücke
$table->string('location_bin')->nullable(); // Lagerort
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_logistics');
}
};

View file

@ -0,0 +1,29 @@
<?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::table('roles', function (Blueprint $table) {
$table->string('display_name')->nullable()->after('name');
$table->string('icon')->nullable()->after('display_name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn(['display_name', 'icon']);
});
}
};

View file

@ -0,0 +1,29 @@
<?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::table('partner_invitations', function (Blueprint $table) {
$table->string('contact_first_name')->nullable()->after('company_name');
$table->string('contact_last_name')->nullable()->after('contact_first_name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('partner_invitations', function (Blueprint $table) {
$table->dropColumn(['contact_first_name', 'contact_last_name']);
});
}
};

View file

@ -0,0 +1,28 @@
<?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::table('roles', function (Blueprint $table) {
$table->boolean('can_be_invited')->default(false)->after('icon');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('can_be_invited');
});
}
};

View file

@ -0,0 +1,101 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Füge zuerst role_id als nullable hinzu (falls nicht existiert)
if (!Schema::hasColumn('partner_invitations', 'role_id')) {
Schema::table('partner_invitations', function (Blueprint $table) {
$table->unsignedBigInteger('role_id')->nullable()->after('contact_last_name');
});
}
// Migriere bestehende Daten: partner_type -> role_id
if (Schema::hasColumn('partner_invitations', 'partner_type')) {
DB::table('partner_invitations')->get()->each(function ($invitation) {
$roleName = $invitation->partner_type;
$role = DB::table('roles')->where('name', $roleName)->first();
if ($role) {
DB::table('partner_invitations')
->where('id', $invitation->id)
->update(['role_id' => $role->id]);
}
});
// Jetzt partner_type entfernen und role_id als required machen
Schema::table('partner_invitations', function (Blueprint $table) {
$table->dropColumn('partner_type');
});
}
// Mache role_id required und füge Foreign Key hinzu
try {
Schema::table('partner_invitations', function (Blueprint $table) {
$table->unsignedBigInteger('role_id')->nullable(false)->change();
$table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
});
} catch (\Exception $e) {
// Foreign Key existiert bereits, nur die Spalte ändern
Schema::table('partner_invitations', function (Blueprint $table) {
$table->unsignedBigInteger('role_id')->nullable(false)->change();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Füge partner_type wieder hinzu (als nullable, da wir Daten migrieren)
if (!Schema::hasColumn('partner_invitations', 'partner_type')) {
Schema::table('partner_invitations', function (Blueprint $table) {
$table->enum('partner_type', ['Retailer', 'Manufacturer', 'Estate-Agent', 'Customer'])->nullable()->after('email');
});
}
// Migriere Daten zurück: role_id -> partner_type
if (Schema::hasColumn('partner_invitations', 'role_id')) {
DB::table('partner_invitations')->get()->each(function ($invitation) {
if ($invitation->role_id) {
$role = DB::table('roles')->where('id', $invitation->role_id)->first();
if ($role) {
DB::table('partner_invitations')
->where('id', $invitation->id)
->update(['partner_type' => $role->name]);
}
}
});
}
// Entferne role_id Foreign Key und Spalte
try {
Schema::table('partner_invitations', function (Blueprint $table) {
$table->dropForeign(['role_id']);
});
} catch (\Exception $e) {
// Foreign Key existiert nicht oder hat anderen Namen - weitermachen
}
if (Schema::hasColumn('partner_invitations', 'role_id')) {
Schema::table('partner_invitations', function (Blueprint $table) {
$table->dropColumn('role_id');
});
}
// Mache partner_type wieder required mit default
Schema::table('partner_invitations', function (Blueprint $table) {
$table->enum('partner_type', ['Retailer', 'Manufacturer', 'Estate-Agent', 'Customer'])->default('Retailer')->change();
});
}
};

View file

@ -0,0 +1,29 @@
<?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::table('partners', function (Blueprint $table) {
$table->boolean('setup_completed')->default(false)->after('is_active');
$table->timestamp('setup_completed_at')->nullable()->after('setup_completed');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('partners', function (Blueprint $table) {
$table->dropColumn(['setup_completed', 'setup_completed_at']);
});
}
};

View file

@ -5,6 +5,7 @@ namespace Database\Seeders;
use App\Models\User; use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents; // use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
@ -16,8 +17,9 @@ class DatabaseSeeder extends Seeder
// User::factory(10)->create(); // User::factory(10)->create();
User::factory()->create([ User::factory()->create([
'name' => 'Test User', 'name' => 'Kevin Adametz',
'email' => 'test@example.com', 'email' => 'kevin.adametz@me.com',
'password' => Hash::make('xunfew-0Jygjy-minnyt'),
]); ]);
} }
} }

View file

@ -0,0 +1,171 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
class RoleSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Reset cached roles and permissions
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// --- Definiere Permissions ---
$permissions = [
// Hub Management
'view hubs',
'create hubs',
'edit hubs',
'delete hubs',
// Partner Management
'view partners',
'create partners',
'edit partners',
'delete partners',
'manage provisions', // Provisions-Regeln verwalten
// Product Management
'view products',
'create products',
'edit products',
'delete products',
'manage rental options', // Miet-Parameter verwalten
// Order Management (für später)
'view orders',
'manage orders',
// User & Role Management
'view users',
'manage users',
'manage roles',
// Frontend/Customer facing
'access dashboard', // Genereller Backend-Zugriff
'place orders' // Für Kunden
];
// Erstelle Permissions
foreach ($permissions as $permission) {
Permission::create(['name' => $permission]);
}
// --- Definiere Rollen und weise Permissions zu ---
// 1. Customer (Endkunde)
$customerRole = Role::create([
'name' => 'Customer',
'display_name' => 'Customer (Kunde)',
'icon' => 'user',
'color' => 'indigo',
'can_be_invited' => true
]);
$customerRole->givePermissionTo([
'view products',
'place orders',
'view orders' // Eigene Bestellungen sehen
]);
// 2. Estate-Agent (Makler)
$estateAgentRole = Role::create([
'name' => 'Estate-Agent',
'display_name' => 'Estate-Agent (Makler)',
'icon' => 'home',
'color' => 'lime',
'can_be_invited' => true
]);
$estateAgentRole->givePermissionTo([
'access dashboard',
'view partners', // Damit sie sehen können, wen sie empfehlen
'view hubs'
// Makler bekommen KEINE Produkt- oder Order-Rechte
]);
// 3. Retailer (Lokaler Händler)
$retailerRole = Role::create([
'name' => 'Retailer',
'display_name' => 'Retailer (Händler)',
'icon' => 'building-storefront',
'color' => 'teal',
'can_be_invited' => true
]);
$retailerRole->givePermissionTo([
'access dashboard',
'view products',
'create products',
'edit products', // Später eingeschränkt auf EIGENE Produkte
'delete products', // Später eingeschränkt auf EIGENE Produkte
'manage rental options',
'view orders', // Eigene Bestellungen
'manage orders' // Eigene Bestellungen
]);
// 4. Manufacturer (Hersteller)
$manufacturerRole = Role::create([
'name' => 'Manufacturer',
'display_name' => 'Manufacturer (Hersteller)',
'icon' => 'wrench-screwdriver',
'color' => 'orange',
'can_be_invited' => true
]);
$manufacturerRole->givePermissionTo([
'access dashboard',
'view products',
'create products',
'edit products', // Später eingeschränkt auf EIGENE Produkte
'delete products', // Später eingeschränkt auf EIGENE Produkte
'manage rental options',
'view orders', // Eigene Bestellungen
'manage orders' // Eigene Bestellungen
]);
// 5. Admin (B2In Management / Marcel)
$adminRole = Role::create([
'name' => 'Admin',
'display_name' => 'Admin (Administrator)',
'icon' => 'user-circle',
'color' => 'purple',
'can_be_invited' => false // Admins werden NICHT eingeladen
]);
$adminRole->givePermissionTo([
'access dashboard',
'view hubs',
'create hubs',
'edit hubs',
'delete hubs',
'view partners',
'create partners',
'edit partners',
'delete partners',
'manage provisions',
'view products',
'create products',
'edit products',
'delete products',
'manage rental options',
'view orders',
'manage orders',
'view users',
'manage users',
'manage roles'
]);
// 6. Super-Admin (Entwickler)
// Super-Admins bekommen automatisch ALLE Rechte.
// Das Paket erkennt die Rolle 'Super-Admin', wenn wir ein Gate definieren.
$superAdminRole = Role::create([
'name' => 'Super-Admin',
'display_name' => 'Super-Admin (Entwickler)',
'icon' => 'shield-check',
'color' => 'red',
'can_be_invited' => false // Super-Admins werden NICHT eingeladen
]);
}
}

View file

@ -1,76 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RolesAndPermissionsSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// Rollen
Role::create(['name' => 'superadmin']);
Role::create(['name' => 'admin']);
Role::create(['name' => 'trader']);
Role::create(['name' => 'customer']);
// Beispiel-Permissions
// Trader
Permission::create(['name' => 'products_manage']);
// Customer
Permission::create(['name' => 'orders_view']);
// Admin
// CMS
Permission::create(['name' => 'cms_manage']);
Permission::create(['name' => 'cms_view']);
Permission::create(['name' => 'cms_create']);
Permission::create(['name' => 'cms_delete']);
Permission::create(['name' => 'cms_list']);
// User
Permission::create(['name' => 'user_edit']);
Permission::create(['name' => 'user_view']);
Permission::create(['name' => 'user_create']);
Permission::create(['name' => 'user_delete']);
Permission::create(['name' => 'user_list']);
// Superadmin
// alles
/*Permission::create(['name' => 'products_manage']);
Permission::create(['name' => 'orders_view']);
Permission::create(['name' => 'user_settings']);
Permission::create(['name' => 'system_settings']);
Permission::create(['name' => 'user_manage']);
Permission::create(['name' => 'order_manage']);
Permission::create(['name' => 'product_manage']);
Permission::create(['name' => 'user_view']);
Permission::create(['name' => 'order_view']);
Permission::create(['name' => 'product_view']);
Permission::create(['name' => 'system_view']);
Permission::create(['name' => 'user_create']);
Permission::create(['name' => 'order_create']);
Permission::create(['name' => 'product_create']);
Permission::create(['name' => 'system_create']);
Permission::create(['name' => 'order_edit']);
Permission::create(['name' => 'product_edit']);
Permission::create(['name' => 'system_edit']);
Permission::create(['name' => 'user_delete']);
Permission::create(['name' => 'order_delete']);
Permission::create(['name' => 'product_delete']);
Permission::create(['name' => 'system_delete']);
Permission::create(['name' => 'user_list']);
Permission::create(['name' => 'order_list']);
Permission::create(['name' => 'product_list']);
Permission::create(['name' => 'system_list']);*/
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -9,7 +9,7 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--color-zinc-50: #fafafa; --color-zinc-50: #fafafa;
--color-zinc-100: #f5f5f5; --color-zinc-100: #f5f5f5;

View file

@ -9,32 +9,33 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--font-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif, --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--color-zinc-50: #fafafa; --background: 32 20% 97%; /* #f5f4f2 - Light Beige */
--color-zinc-100: #f5f5f5; /* Custom accent color palette based on HSL(199, 74%, 49%) */
--color-zinc-200: #e5e5e5; --color-accent-50: hsl(199 74% 97%);
--color-zinc-300: #d4d4d4; --color-accent-100: hsl(199 74% 92%);
--color-zinc-400: #a3a3a3; --color-accent-200: hsl(199 74% 82%);
--color-zinc-500: #737373; --color-accent-300: hsl(199 74% 70%);
--color-zinc-600: #525252; --color-accent-400: hsl(199 74% 59%);
--color-zinc-700: #404040; --color-accent-500: hsl(199 74% 49%);
--color-zinc-800: #262626; --color-accent-600: hsl(199 74% 39%);
--color-zinc-900: #171717; --color-accent-700: hsl(199 74% 29%);
--color-zinc-950: #0a0a0a; --color-accent-800: hsl(199 74% 19%);
--color-accent-900: hsl(199 74% 12%);
--color-accent-950: hsl(199 74% 7%);
--color-accent: var(--color-neutral-800); /* FluxUI accent variables */
--color-accent-content: var(--color-neutral-800); --color-accent: hsl(199 74% 49%);
--color-accent-content: hsl(199 74% 39%);
--color-accent-foreground: var(--color-white); --color-accent-foreground: var(--color-white);
} }
@layer theme { @layer theme {
.dark { .dark {
--color-accent: var(--color-white); --color-accent: hsl(199 74% 59%);
--color-accent-content: var(--color-white); --color-accent-content: hsl(199 74% 49%);
--color-accent-foreground: var(--color-neutral-800); --color-accent-foreground: var(--color-white);
} }
} }
@ -62,6 +63,13 @@ select:focus[data-flux-control] {
@apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground; @apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground;
} }
.shadow-elegant {
box-shadow: 0 4px 12px -8px rgba(0, 136, 204, 0.4);
}
.bg-background {
background-color: hsl(var(--background));
}
/* \[:where(&)\]:size-4 { /* \[:where(&)\]:size-4 {
@apply size-4; @apply size-4;
} */ } */

View file

@ -32,6 +32,12 @@ h1, h2, h3, h4, h5, h6 {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
.text-hero-alt {
font-size: clamp(2rem, 3vw, 3rem);
line-height: 1.1;
font-weight: 300;
letter-spacing: -0.025em;
}
.text-section-title { .text-section-title {
font-size: clamp(1.6rem, 3vw, 3rem); font-size: clamp(1.6rem, 3vw, 3rem);
line-height: 1.3em; line-height: 1.3em;

View file

@ -177,8 +177,7 @@
/* Cards - Multi-Layer-Schatten für Tiefe (Optimiert) */ /* Cards - Multi-Layer-Schatten für Tiefe (Optimiert) */
& .card, & .card,
& [class*="card"], & [class*="card"],
& .bg-card, & .bg-card {
& article {
background: background:
linear-gradient( linear-gradient(
145deg, 145deg,
@ -206,8 +205,7 @@
& .card:hover, & .card:hover,
& [class*="card"]:hover, & [class*="card"]:hover {
& article:hover {
box-shadow: box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.04),
0 2px 4px rgba(0, 0, 0, 0.03), 0 2px 4px rgba(0, 0, 0, 0.03),

View file

@ -5,7 +5,44 @@
<livewire:notifications /> <livewire:notifications />
</div> </div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"> <div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" /> <flux:table>
<flux:table.columns>
<flux:table.column>Customer</flux:table.column>
<flux:table.column>Date</flux:table.column>
<flux:table.column>Status</flux:table.column>
<flux:table.column>Amount</flux:table.column>
</flux:table.columns>
<flux:table.rows>
<flux:table.row>
<flux:table.cell>Lindsey Aminoff</flux:table.cell>
<flux:table.cell>Jul 29, 10:45 AM</flux:table.cell>
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
<flux:table.cell variant="strong">$49.00</flux:table.cell>
</flux:table.row>
<flux:table.row>
<flux:table.cell>Hanna Lubin</flux:table.cell>
<flux:table.cell>Jul 28, 2:15 PM</flux:table.cell>
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
<flux:table.cell variant="strong">$312.00</flux:table.cell>
</flux:table.row>
<flux:table.row>
<flux:table.cell>Kianna Bushevi</flux:table.cell>
<flux:table.cell>Jul 30, 4:05 PM</flux:table.cell>
<flux:table.cell><flux:badge color="zinc" size="sm" inset="top bottom">Refunded</flux:badge></flux:table.cell>
<flux:table.cell variant="strong">$132.00</flux:table.cell>
</flux:table.row>
<flux:table.row>
<flux:table.cell>Gustavo Geidt</flux:table.cell>
<flux:table.cell>Jul 27, 9:30 AM</flux:table.cell>
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
<flux:table.cell variant="strong">$31.00</flux:table.cell>
</flux:table.row>
</flux:table.rows>
</flux:table>
</div> </div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700"> <div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" /> <x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
@ -16,3 +53,5 @@
</div> </div>
</div> </div>
</x-layouts.app> </x-layouts.app>

View file

@ -1,8 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
<path <img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}" alt="B2IN Logo" class="h-10 w-auto dark:hidden" />
fill="currentColor" <img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}" alt="B2IN Logo" class="h-10 w-auto hidden dark:block" />
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 257 B

Before After
Before After

View file

@ -1,6 +1,5 @@
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground"> <div class="flex size-16 items-center justify-center ml-2 ">
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" /> <img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}" alt="B2IN Logo" class="h-10 w-auto dark:hidden" />
</div> <img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}" alt="B2IN Logo" class="h-10 w-auto hidden dark:block" />
<div class="ms-1 grid flex-1 text-start text-sm">
<span class="mb-0.5 truncate leading-none font-semibold">B2IN</span>
</div> </div>

View file

@ -0,0 +1,20 @@
@props(['title' => null])
@if ($errors->any())
<div {{ $attributes->merge(['class' => 'rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4']) }}>
<div class="flex items-start gap-3">
<flux:icon.exclamation-circle class="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
<div class="flex-1">
<h3 class="text-sm font-semibold text-red-800 dark:text-red-200 mb-2">
{{ $title ?? __('Bitte korrigieren Sie folgende Fehler:') }}
</h3>
<ul class="text-sm text-red-700 dark:text-red-300 space-y-1 list-disc list-inside">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif

View file

@ -3,7 +3,7 @@
<head> <head>
@include('partials.head') @include('partials.head')
</head> </head>
<body class="min-h-screen bg-white dark:bg-zinc-800"> <body class="min-h-screen bg-zinc-50 dark:bg-zinc-800 antialiased">
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900"> <flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" /> <flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ config('domains.domain_main_url') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse"> <a href="{{ config('domains.domain_main_url') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse">
@ -11,21 +11,33 @@
</a> </a>
<flux:navlist variant="outline"> <flux:navlist variant="outline">
<flux:navlist.group :heading="__('Trader')" class="grid mb-4">
<flux:navlist.group :heading="__('Customer')" class="grid mb-4">
<flux:navlist.item icon="user" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Retailer')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item> <flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group> </flux:navlist.group>
<flux:navlist.group :heading="__('Customer')" class="grid mb-4"> <flux:navlist.group :heading="__('Manufacturer')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item> <flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group> </flux:navlist.group>
<flux:navlist.group :heading="__('Estate-Agent')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Admin')" class="grid mb-4"> <flux:navlist.group :heading="__('Admin')" class="grid mb-4">
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item> <flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item>
<flux:navlist.item icon="user-group" :href="route('admin.users.table')" :current="request()->routeIs('admin.users.table')" wire:navigate>{{ __('Users Table') }}</flux:navlist.item>
<flux:navlist.group expandable expanded="false" heading="Favorites" class="hidden lg:grid"> <flux:navlist.group expandable expanded="false" heading="Users" class="hidden lg:grid">
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item> <flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item>
<flux:navlist.item icon="user-group" :href="route('admin.users.table')" :current="request()->routeIs('admin.users.table')" wire:navigate>{{ __('Users Table') }}</flux:navlist.item> <flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')" :current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}</flux:navlist.item>
</flux:navlist.group> </flux:navlist.group>
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')" :current="request()->routeIs('admin.partners.invite')" wire:navigate>{{ __('Partner einladen') }}</flux:navlist.item>
</flux:navlist.group> </flux:navlist.group>
<flux:navlist.group :heading="__('CMS')" class="grid mb-4"> <flux:navlist.group :heading="__('CMS')" class="grid mb-4">

View file

@ -1,3 +1,3 @@
<x-layouts.auth.simple :title="$title ?? null"> <x-layouts.auth.card :title="$title ?? null">
{{ $slot }} {{ $slot }}
</x-layouts.auth.simple> </x-layouts.auth.card>

View file

@ -2,25 +2,31 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head> <head>
@include('partials.head') @include('partials.head')
@include('partials.theme-init-script')
</head> </head>
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900"> <body class="min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-zinc-900 dark:to-zinc-800 px-4 py-12">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"> <div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-md flex-col gap-6"> <div class="flex w-full max-w-md flex-col gap-6">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate> <a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md"> <span class="flex h-20 w-20 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" /> <x-app-logo-icon />
</span> </span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span> <span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a> </a>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> <flux:card class="shadow-2xl">
<div class="px-10 py-8">{{ $slot }}</div> <div class="px-10 py-8">{{ $slot }}</div>
</flux:card>
<div class="flex justify-center mt-4">
<x-theme-toggle />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@livewireScripts
@fluxScripts @fluxScripts
@include('partials.theme-toggle-script')
</body> </body>
</html> </html>

View file

@ -2,23 +2,30 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head> <head>
@include('partials.head') @include('partials.head')
@include('partials.theme-init-script')
</head> </head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900"> <body class="min-h-screen bg-zinc-50 antialiased dark:bg-gradient-to-b dark:from-zinc-950 dark:to-zinc-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10"> <div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-2"> <div class="flex w-full max-w-sm flex-col gap-2">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate> <a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md"> <span class="flex h-12 w-12 mb-1 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" /> <x-app-logo-icon />
</span> </span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span> <span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a> </a>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
{{ $slot }} {{ $slot }}
</div> </div>
<div class="flex justify-center mt-6">
<x-theme-toggle />
</div>
</div> </div>
</div> </div>
@livewireScripts @livewireScripts
@fluxScripts @fluxScripts
@include('partials.theme-toggle-script')
<script src="{{ asset('vendor/livewire/livewire.js') }}"></script> <script src="{{ asset('vendor/livewire/livewire.js') }}"></script>
<!-- Debug: Script-Status --> <!-- Debug: Script-Status -->

View file

@ -2,14 +2,15 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head> <head>
@include('partials.head') @include('partials.head')
@include('partials.theme-init-script')
</head> </head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900"> <body class="min-h-screen bg-zinc-50 antialiased dark:bg-gradient-to-b dark:from-zinc-950 dark:to-zinc-900">
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0"> <div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800"> <div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
<div class="absolute inset-0 bg-neutral-900"></div> <div class="absolute inset-0 bg-neutral-900"></div>
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate> <a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
<span class="flex h-10 w-10 items-center justify-center rounded-md"> <span class="flex h-10 w-10 items-center justify-center rounded-md">
<x-app-logo-icon class="me-2 h-7 fill-current text-white" /> <x-app-logo-icon class="me-2 h-7" />
</span> </span>
{{ config('app.name', 'Laravel') }} {{ config('app.name', 'Laravel') }}
</a> </a>
@ -29,15 +30,20 @@
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"> <div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate> <a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md"> <span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" /> <x-app-logo-icon />
</span> </span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span> <span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a> </a>
{{ $slot }} {{ $slot }}
<div class="flex justify-center mt-6">
<x-theme-toggle />
</div>
</div> </div>
</div> </div>
</div> </div>
@fluxScripts @fluxScripts
@include('partials.theme-toggle-script')
</body> </body>
</html> </html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
@include('partials.theme-init-script')
</head>
<body
class="min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-zinc-900 dark:via-zinc-900 dark:to-zinc-800 px-4 py-12">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
{{ $slot }}
<div class="flex justify-center mt-4">
<x-theme-toggle />
</div>
</div>
@livewireScripts
@fluxScripts
@include('partials.theme-toggle-script')
</body>
</html>

View file

@ -0,0 +1,15 @@
<button
id="theme-toggle"
type="button"
class="flex items-center gap-2 px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-colors"
aria-label="Theme umschalten"
>
<svg id="theme-toggle-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
<svg id="theme-toggle-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
<span id="theme-toggle-text">Hell</span>
</button>

View file

@ -0,0 +1,49 @@
@props(['currentStep', 'totalSteps', 'steps' => []])
<div class="mb-8">
<div class="flex items-center justify-center">
@foreach($steps as $index => $step)
@php
$stepNumber = $index + 1;
$isActive = $stepNumber === $currentStep;
$isCompleted = $stepNumber < $currentStep;
@endphp
{{-- Step Circle --}}
<div class="flex flex-col items-center">
<div class="flex items-center">
<div class="flex items-center justify-center w-10 h-10 rounded-full border-2
{{ $isCompleted ? 'bg-accent-600 border-accent-600' : '' }}
{{ $isActive ? 'bg-accent-600 border-accent-600' : '' }}
{{ !$isActive && !$isCompleted ? 'bg-white dark:bg-zinc-800 border-zinc-300 dark:border-zinc-600' : '' }}">
@if($isCompleted)
<flux:icon.check class="h-5 w-5 text-white" />
@else
<span class="text-sm font-semibold
{{ $isActive ? 'text-white' : 'text-zinc-500 dark:text-zinc-400' }}">
{{ $stepNumber }}
</span>
@endif
</div>
</div>
{{-- Step Label --}}
<div class="mt-2 text-center">
<p class="text-xs font-medium
{{ $isActive ? 'text-accent-600 dark:text-accent-400' : 'text-zinc-500 dark:text-zinc-400' }}">
{{ $step }}
</p>
</div>
</div>
{{-- Connector Line --}}
@if(!$loop->last)
<div class="flex-1 h-0.5 mx-4
{{ $stepNumber < $currentStep ? 'bg-accent-600' : 'bg-zinc-300 dark:bg-zinc-600' }}"
style="min-width: 60px; max-width: 120px;">
</div>
@endif
@endforeach
</div>
</div>

View file

@ -0,0 +1,47 @@
<x-mail::message>
# Willkommen bei B2In!
@if($contactFullName)
Hallo {{ $contactFullName }},
Sie wurden eingeladen, Teil unserer Plattform zu werden.
@else
Sie wurden eingeladen, Teil unserer Plattform zu werden.
@endif
## Firmeninformationen
**Firmenname:** {{ $companyName }}
**Partner-Typ:** {{ $partnerType }}
Wir freuen uns, Sie als **{{ $partnerType }}** in unserem Netzwerk begrüßen zu dürfen!
## Nächste Schritte
Klicken Sie auf den Button unten, um Ihre Registrierung abzuschließen. Sie können dann:
- Ihr Unternehmensprofil vervollständigen
- Zugriff auf unser Partner-Portal erhalten
- Mit anderen Partnern im Netzwerk interagieren
<x-mail::button :url="$invitationUrl" color="primary">
Registrierung abschließen
</x-mail::button>
<x-mail::panel>
**Wichtig:** Diese Einladung ist gültig bis zum {{ $expiresAt->format('d.m.Y H:i') }} Uhr.
</x-mail::panel>
Falls Sie Fragen haben, können Sie uns jederzeit kontaktieren.
Mit freundlichen Grüßen,
{{ config('app.name') }} Team
---
<small>
Falls der Button nicht funktioniert, kopieren Sie bitte den folgenden Link in Ihren Browser:
{{ $invitationUrl }}
</small>
</x-mail::message>

View file

@ -0,0 +1,324 @@
<?php
use App\Models\PartnerInvitation;
use App\Mail\PartnerInvitationMail;
use Illuminate\Support\Facades\Mail;
use Livewire\Volt\Component;
use Spatie\Permission\Models\Role;
use function Livewire\Volt\{layout, title, state};
layout('components.layouts.app');
title('Partner einladen');
new class extends Component {
public string $companyName = '';
public string $contactFirstName = '';
public string $contactLastName = '';
public ?int $roleId = null;
public string $email = '';
public bool $showSuccessMessage = false;
public ?PartnerInvitation $lastInvitation = null;
public function getPartnerRoles()
{
// Lade nur Rollen die eingeladen werden können
return Role::where('can_be_invited', true)
->orderBy('name')
->get();
}
public function mount(): void
{
// Setze default auf die erste einladbare Rolle
$firstRole = $this->getPartnerRoles()->first();
$this->roleId = $firstRole?->id;
}
public function sendInvitation(): void
{
$availableRoleIds = $this->getPartnerRoles()->pluck('id')->toArray();
$this->validate([
'companyName' => 'required|string|max:255',
'contactFirstName' => 'nullable|string|max:255',
'contactLastName' => 'nullable|string|max:255',
'roleId' => 'required|exists:roles,id|in:' . implode(',', $availableRoleIds),
'email' => 'required|email|max:255|unique:users,email',
], [
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
'companyName.max' => __('Der Firmenname darf maximal 255 Zeichen lang sein.'),
'contactFirstName.max' => __('Der Vorname darf maximal 255 Zeichen lang sein.'),
'contactLastName.max' => __('Der Nachname darf maximal 255 Zeichen lang sein.'),
'roleId.required' => __('Bitte wählen Sie einen Partner-Typ aus.'),
'roleId.exists' => __('Der gewählte Partner-Typ ist ungültig.'),
'email.required' => __('Bitte geben Sie eine E-Mail-Adresse ein.'),
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
'email.max' => __('Die E-Mail-Adresse darf maximal 255 Zeichen lang sein.'),
'email.unique' => __('Diese E-Mail-Adresse ist bereits als Benutzer registriert.'),
]);
// Prüfe ob bereits eine aktive Einladung existiert
$existingInvitation = PartnerInvitation::where('email', $this->email)
->pending()
->first();
if ($existingInvitation) {
$this->addError('email', __('Es existiert bereits eine aktive Einladung für diese E-Mail-Adresse.'));
return;
}
// Erstelle Einladung
$invitation = PartnerInvitation::create([
'company_name' => $this->companyName,
'contact_first_name' => $this->contactFirstName ?: null,
'contact_last_name' => $this->contactLastName ?: null,
'role_id' => $this->roleId,
'email' => $this->email,
'token' => PartnerInvitation::generateToken(),
'status' => 'pending',
'expires_at' => now()->addDays(7), // 7 Tage gültig
'invited_by' => auth()->id(),
]);
// Generiere Einladungs-URL
$invitationUrl = route('partner.invitation.accept', ['token' => $invitation->token]);
// Sende E-Mail
try {
Mail::to($this->email)->send(new PartnerInvitationMail($invitation, $invitationUrl));
$this->lastInvitation = $invitation;
$this->showSuccessMessage = true;
// Reset Form
$this->reset(['companyName', 'contactFirstName', 'contactLastName', 'roleId', 'email']);
// Setze default Rolle wieder
$firstRole = $this->getPartnerRoles()->first();
$this->roleId = $firstRole?->id;
session()->flash('message', __('Einladung erfolgreich versendet!'));
} catch (\Exception $e) {
$this->addError('email', __('Fehler beim Versenden der E-Mail: ') . $e->getMessage());
}
}
public function with(): array
{
return [
'partnerRoles' => $this->getPartnerRoles(),
'recentInvitations' => PartnerInvitation::with(['invitedBy', 'role'])
->latest()
->take(10)
->get(),
'pendingCount' => PartnerInvitation::pending()->count(),
'acceptedCount' => PartnerInvitation::where('status', 'accepted')->count(),
'expiredCount' => PartnerInvitation::expired()->count(),
];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Partner einladen') }}</flux:heading>
<flux:subheading>{{ __('Laden Sie neue Partner zu Ihrer Plattform ein') }}</flux:subheading>
</div>
</div>
{{-- Statistics --}}
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Ausstehend') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $pendingCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/20">
<flux:icon.clock class="h-6 w-6 text-orange-600 dark:text-orange-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Akzeptiert') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $acceptedCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/20">
<flux:icon.check-circle class="h-6 w-6 text-green-600 dark:text-green-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Abgelaufen') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $expiredCount }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
<flux:icon.x-circle class="h-6 w-6 text-zinc-600 dark:text-zinc-400" />
</div>
</div>
</flux:card>
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
{{-- Invitation Form --}}
<flux:card class="shadow-elegant">
<form wire:submit="sendInvitation" class="space-y-6">
<div>
<flux:heading size="lg" class="mb-2">{{ __('Neue Einladung senden') }}</flux:heading>
<flux:subheading>{{ __('Füllen Sie die Felder aus, um einen neuen Partner einzuladen') }}</flux:subheading>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Firmenname') }}</flux:label>
<flux:input
wire:model="companyName"
placeholder="{{ __('z.B. Möbelhaus Mustermann') }}"
icon="building-office"
/>
@error('companyName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:field>
<flux:label>{{ __('Vorname (optional)') }}</flux:label>
<flux:input
wire:model="contactFirstName"
placeholder="{{ __('z.B. Max') }}"
icon="user"
/>
@error('contactFirstName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Nachname (optional)') }}</flux:label>
<flux:input
wire:model="contactLastName"
placeholder="{{ __('z.B. Mustermann') }}"
icon="user"
/>
@error('contactLastName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Partner-Typ') }}</flux:label>
<flux:description>{{ __('Wählen Sie den Typ des Partners aus') }}</flux:description>
<flux:select wire:model="roleId">
@foreach($partnerRoles as $role)
<flux:select.option :value="$role->id">
@if($role->icon)
<flux:icon.{{ $role->icon }} class="mr-2" />
@endif
{{ $role->display_name ?? $role->name }}
</flux:select.option>
@endforeach
</flux:select>
@error('roleId') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('E-Mail Adresse') }}</flux:label>
<flux:description>{{ __('E-Mail des Ansprechpartners') }}</flux:description>
<flux:input
type="email"
wire:model="email"
placeholder="{{ __('z.B. einkauf@mustermann.de') }}"
icon="envelope"
/>
@error('email') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:separator />
{{-- Error Alert --}}
<x-error-alert />
<div class="flex justify-end">
<flux:button
type="submit"
variant="primary"
icon="paper-airplane"
wire:loading.attr="disabled"
wire:target="sendInvitation"
>
<span wire:loading.remove wire:target="sendInvitation">
{{ __('Einladung senden') }}
</span>
<span wire:loading wire:target="sendInvitation">
<flux:icon.arrow-path class="animate-spin inline-block mr-2 h-4 w-4" />
{{ __('Wird gesendet...') }}
</span>
</flux:button>
</div>
</form>
</flux:card>
{{-- Recent Invitations --}}
<flux:card class="shadow-elegant">
<div class="mb-4">
<flux:heading size="lg" class="mb-2">{{ __('Letzte Einladungen') }}</flux:heading>
<flux:subheading>{{ __('Übersicht der zuletzt versendeten Einladungen') }}</flux:subheading>
</div>
<flux:separator class="mb-4" />
<div class="space-y-3">
@forelse($recentInvitations as $invitation)
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="font-semibold text-zinc-900 dark:text-white">
{{ $invitation->company_name }}
@if($invitation->contact_full_name)
<span class="text-sm font-normal text-zinc-500"> {{ $invitation->contact_full_name }}</span>
@endif
</div>
<div class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
{{ $invitation->email }}
</div>
<div class="mt-2 flex items-center gap-2">
<flux:badge size="sm"
color="{{ $invitation->status === 'pending' ? 'orange' : ($invitation->status === 'accepted' ? 'green' : 'zinc') }}">
{{ ucfirst($invitation->status) }}
</flux:badge>
<flux:badge size="sm" color="{{ $invitation->role?->color ?? 'zinc' }}">
{{ $invitation->role?->display_name ?? $invitation->role?->name }}
</flux:badge>
</div>
<div class="mt-2 text-xs text-zinc-400">
{{ __('Eingeladen am:') }} {{ $invitation->created_at->format('d.m.Y H:i') }}
@if($invitation->status === 'pending')
<br>{{ __('Gültig bis:') }} {{ $invitation->expires_at->format('d.m.Y H:i') }}
@endif
</div>
</div>
</div>
</div>
@empty
<div class="py-8 text-center text-zinc-500">
<flux:icon.envelope class="mx-auto h-12 w-12 text-zinc-400" />
<div class="mt-2">{{ __('Noch keine Einladungen versendet') }}</div>
</div>
@endforelse
</div>
</flux:card>
</div>
{{-- Success Toast --}}
@if (session()->has('message'))
<flux:toast :variant="'success'">
{{ session('message') }}
</flux:toast>
@endif
</div>

View file

@ -0,0 +1,352 @@
<?php
use App\Models\User;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use function Livewire\Volt\{layout, title, state};
layout('components.layouts.app');
title('Users Management');
new class extends Component {
use WithPagination;
public string $search = '';
public string $roleFilter = '';
public string $sortField = 'name';
public string $sortDirection = 'asc';
// Modal state
public bool $showRoleModal = false;
public ?int $selectedUserId = null;
public array $selectedRoles = [];
public function with(): array
{
$query = User::with('roles')
->when($this->search, fn($q, $search) =>
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
)
->when($this->roleFilter, fn($q, $role) =>
$q->whereHas('roles', fn($roleQuery) =>
$roleQuery->where('name', $role)
)
);
return [
'users' => $query->orderBy($this->sortField, $this->sortDirection)->paginate(15),
'totalUsers' => User::count(),
'verifiedUsers' => User::whereNotNull('email_verified_at')->count(),
'availableRoles' => \Spatie\Permission\Models\Role::orderBy('name')->get(),
'selectedUser' => $this->selectedUserId ? User::find($this->selectedUserId) : null,
];
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
}
public function updatingSearch(): void
{
$this->resetPage();
}
public function updatingRoleFilter(): void
{
$this->resetPage();
}
public function openRoleModal(int $userId): void
{
$user = User::with('roles')->findOrFail($userId);
$this->selectedUserId = $userId;
$this->selectedRoles = $user->roles->pluck('name')->toArray();
$this->showRoleModal = true;
}
public function saveRoles(): void
{
if (!$this->selectedUserId) {
return;
}
$user = User::findOrFail($this->selectedUserId);
$user->syncRoles($this->selectedRoles);
$this->showRoleModal = false;
$this->selectedUserId = null;
$this->selectedRoles = [];
// Optional: Flash message
session()->flash('message', __('Roles updated successfully!'));
}
public function closeRoleModal(): void
{
$this->showRoleModal = false;
$this->selectedUserId = null;
$this->selectedRoles = [];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Users Management') }}</flux:heading>
<flux:subheading>{{ __('Manage users and their roles in your application') }}</flux:subheading>
</div>
<div class="flex gap-2">
<flux:button variant="primary" icon="plus">{{ __('Create User') }}</flux:button>
</div>
</div>
{{-- Statistics --}}
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Total Users') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $totalUsers }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.users class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Verified Users') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $verifiedUsers }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Active Roles') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $availableRoles->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
</div>
{{-- Filters --}}
<flux:card class="shadow-elegant">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Search users...') }}" />
<flux:select wire:model.live="roleFilter" placeholder="{{ __('All Roles') }}">
<flux:select.option value="">{{ __('All Roles') }}</flux:select.option>
@foreach($availableRoles as $role)
<flux:select.option :value="$role->name">{{ $role->display_name ?? $role->name }}</flux:select.option>
@endforeach
</flux:select>
@if($search || $roleFilter)
<flux:button wire:click="$set('search', ''); $set('roleFilter', '')" variant="ghost" icon="x-mark">
{{ __('Clear Filters') }}
</flux:button>
@endif
</div>
</flux:card>
{{-- Users Table --}}
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/4" wire:click="sortBy('name')" class="cursor-pointer">
<div class="flex items-center gap-2">
{{ __('User') }}
@if($sortField === 'name')
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
@endif
</div>
</flux:table.column>
<flux:table.column wire:click="sortBy('email')" class="cursor-pointer">
<div class="flex items-center gap-2">
{{ __('Email') }}
@if($sortField === 'email')
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
@endif
</div>
</flux:table.column>
<flux:table.column>{{ __('Roles') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Status') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($users as $user)
<flux:table.row :key="$user->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-2">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-100 text-accent-600 dark:bg-accent-900/20 dark:text-accent-400 font-semibold">
{{ $user->initials() }}
</div>
<div>
<div class="font-semibold text-zinc-900 dark:text-white">{{ $user->name }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ __('ID:') }} {{ $user->id }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-2">
<flux:icon.envelope variant="micro" class="text-zinc-400" />
<span>{{ $user->email }}</span>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
<button
type="button"
wire:click="openRoleModal({{ $user->id }})"
class="flex flex-wrap gap-1.5 cursor-pointer hover:opacity-80 transition-opacity"
>
@forelse($user->roles as $role)
<flux:badge size="sm" :color="$role->color ?? 'zinc'">
@svg('heroicon-o-'.$role->icon, 'w-5 h-5') &nbsp; {{ $role->display_name ?? $role->name }}
</flux:badge>
@empty
<flux:badge size="sm" color="zinc" icon="plus">{{ __('Assign Role') }}</flux:badge>
@endforelse
</button>
</div>
</flux:table.cell>
<flux:table.cell>
@if($user->email_verified_at)
<flux:badge size="sm" color="green" icon="check-circle">
{{ __('Verified') }}
</flux:badge>
@else
<flux:badge size="sm" color="zinc" icon="exclamation-circle">
{{ __('Unverified') }}
</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="pencil"
tooltip="{{ __('Edit User') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="trash"
tooltip="{{ __('Delete User') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="py-12 text-center">
<flux:icon.users variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No users found') }}</flux:heading>
<flux:subheading class="mt-2">
@if($search || $roleFilter)
{{ __('Try adjusting your filters.') }}
@else
{{ __('Get started by creating a new user.') }}
@endif
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
{{-- Pagination --}}
@if($users->hasPages())
<div class="border-t border-zinc-200 px-6 py-4 dark:border-zinc-700">
{{ $users->links() }}
</div>
@endif
</flux:card>
{{-- Role Assignment Modal --}}
<flux:modal name="role-modal" :variant="'flyout'" wire:model="showRoleModal">
<form wire:submit="saveRoles" class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Assign Roles') }}</flux:heading>
<flux:subheading>
@if($selectedUser)
{{ __('Managing roles for') }} <strong>{{ $selectedUser->name }}</strong>
@endif
</flux:subheading>
</div>
<flux:separator />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Roles') }}</flux:label>
<flux:description>{{ __('Select one or multiple roles for this user') }}</flux:description>
<div class="space-y-2 mt-3">
@foreach($availableRoles as $role)
<flux:checkbox
wire:model="selectedRoles"
:value="$role->name"
:label="$role->display_name ?? $role->name"
/>
@endforeach
</div>
</flux:field>
@if(!empty($selectedRoles))
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Selected Roles:') }}</flux:subheading>
<div class="flex flex-wrap gap-2">
@foreach($selectedRoles as $roleName)
@php
$roleObj = $availableRoles->firstWhere('name', $roleName);
@endphp
<flux:badge size="sm" :color="$roleObj?->color ?? 'zinc'">
{{ $roleName }}
</flux:badge>
@endforeach
</div>
</div>
@endif
</div>
<flux:separator />
<div class="flex justify-between gap-2">
<flux:button type="button" variant="ghost" wire:click="closeRoleModal">
{{ __('Cancel') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Save Roles') }}
</flux:button>
</div>
</form>
</flux:modal>
{{-- Success Message --}}
@if (session()->has('message'))
<flux:toast :variant="'success'">
{{ session('message') }}
</flux:toast>
@endif
</div>

View file

@ -0,0 +1,478 @@
<?php
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Livewire\Volt\Component;
use function Livewire\Volt\{layout, title};
title('Permissions & Roles');
new class extends Component {
public $activeTab = 'roles';
// Modal state for editing roles
public bool $showEditModal = false;
public ?int $selectedRoleId = null;
public string $roleName = '';
public string $roleDisplayName = '';
public string $roleIcon = 'shield-check';
public string $roleColor = 'zinc';
public array $rolePermissions = [];
public function with(): array
{
return [
'roles' => Role::with('permissions')->orderBy('name')->get(),
'permissions' => Permission::with('roles')->orderBy('name')->get(),
'allPermissions' => Permission::orderBy('name')->get(),
'selectedRole' => $this->selectedRoleId ? Role::find($this->selectedRoleId) : null,
];
}
public function switchTab(string $tab): void
{
\Log::info('Switching tab to: ' . $tab);
$this->activeTab = $tab;
\Log::info('Active tab: ' . $this->activeTab);
}
public function openEditModal(int $roleId): void
{
$role = Role::with('permissions')->findOrFail($roleId);
$this->selectedRoleId = $roleId;
$this->roleName = $role->name;
$this->roleDisplayName = $role->display_name ?? $role->name;
$this->roleIcon = $role->icon ?? 'shield-check';
$this->roleColor = $role->color ?? 'zinc';
$this->rolePermissions = $role->permissions->pluck('name')->toArray();
$this->roleIcon = $role->icon ?? 'shield-check';
$this->showEditModal = true;
}
public function saveRole(): void
{
if (!$this->selectedRoleId) {
return;
}
$this->validate([
'roleName' => 'required|string|max:255',
'roleColor' => 'required|string|max:50',
]);
$role = Role::findOrFail($this->selectedRoleId);
$role->update([
'name' => $this->roleName,
'display_name' => $this->roleDisplayName,
'icon' => $this->roleIcon,
'color' => $this->roleColor,
]);
$role->syncPermissions($this->rolePermissions);
$this->showEditModal = false;
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
session()->flash('message', __('Role updated successfully!'));
}
public function closeEditModal(): void
{
$this->showEditModal = false;
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Permissions & Roles Management') }}</flux:heading>
<flux:subheading>{{ __('Manage roles and permissions for your application') }}</flux:subheading>
</div>
<div class="flex gap-2">
<flux:button variant="primary" icon="plus">{{ __('Create Role') }}</flux:button>
<flux:button variant="ghost" icon="shield-check">{{ __('Create Permission') }}</flux:button>
</div>
</div>
{{-- Tabs --}}
<flux:tabs wire:model.live="activeTab">
<flux:tab name="roles" icon="user-group">{{ __('Roles Overview') }}</flux:tab>
<flux:tab name="permissions" icon="shield-check">{{ __('Permissions Overview') }}</flux:tab>
</flux:tabs>
{{-- Roles Tab Content --}}
@if($activeTab === 'roles')
<div class="space-y-6">
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/5">{{ __('Role') }}</flux:table.column>
<flux:table.column>{{ __('Permissions') }}</flux:table.column>
<flux:table.column class="w-24">{{ __('Count') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($roles as $role)
<flux:table.row :key="$role->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-2">
@php
$colorClasses = match($role->color ?? 'zinc') {
'red' => 'bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400',
'accent' => 'bg-accent-100 text-accent-600 dark:bg-accent-900/20 dark:text-accent-400',
'blue' => 'bg-blue-100 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400',
'lime' => 'bg-lime-100 text-lime-600 dark:bg-lime-900/20 dark:text-lime-400',
'teal' => 'bg-teal-100 text-teal-600 dark:bg-teal-900/20 dark:text-teal-400',
'orange' => 'bg-orange-100 text-orange-600 dark:bg-orange-900/20 dark:text-orange-400',
'purple' => 'bg-purple-100 text-purple-600 dark:bg-purple-900/20 dark:text-purple-400',
'indigo' => 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/20 dark:text-indigo-400',
'pink' => 'bg-pink-100 text-pink-600 dark:bg-pink-900/20 dark:text-pink-400',
'green' => 'bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400',
'yellow' => 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/20 dark:text-yellow-400',
default => 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400',
};
@endphp
<div class="flex h-10 w-10 items-center justify-center rounded-lg {{ $colorClasses }}">
@if($role->icon)
@svg('heroicon-o-'.$role->icon, 'w-5 h-5')
@else
@svg('heroicon-o-shield-check', 'w-5 h-5')
@endif
</div>
<div>
<div class="font-semibold text-zinc-900 dark:text-white">
{{ $role->display_name ?? $role->name }}
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $role->name }} {{ __('Guard:') }} {{ $role->guard_name }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
@if($role->name === 'Super-Admin')
<flux:badge size="sm" :color="$role->color ?? 'red'" icon="star">{{ __('All Permissions') }}</flux:badge>
@elseif($role->permissions->isEmpty())
<flux:badge size="sm" color="zinc">{{ __('No permissions') }}</flux:badge>
@else
@foreach($role->permissions->take(5) as $permission)
<flux:badge size="sm" color="zinc">{{ $permission->name }}</flux:badge>
@endforeach
@if($role->permissions->count() > 5)
<flux:badge size="sm" color="accent">
+{{ $role->permissions->count() - 5 }} {{ __('more') }}
</flux:badge>
@endif
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="text-center font-mono text-sm font-semibold text-zinc-900 dark:text-white">
@if($role->name === 'Super-Admin')
@else
{{ $role->permissions->count() }}
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="eye"
tooltip="{{ __('View Details') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="pencil"
wire:click="openEditModal({{ $role->id }})"
tooltip="{{ __('Edit Role') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="py-12 text-center">
<flux:icon.shield-exclamation variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No roles found') }}</flux:heading>
<flux:subheading class="mt-2">
{{ __('Get started by creating a new role.') }}
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
{{-- Role Statistics --}}
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Total Roles') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $roles->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Total Permissions') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $permissions->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Avg. Permissions/Role') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">
{{ $roles->count() > 0 ? number_format($roles->sum(fn($r) => $r->permissions->count()) / $roles->count(), 1) : 0 }}
</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.chart-bar class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
</div>
</div>
@endif
{{-- Permissions Tab Content --}}
@if($activeTab === 'permissions')
<div class="space-y-6">
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/4">{{ __('Permission') }}</flux:table.column>
<flux:table.column>{{ __('Assigned to Roles') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Role Count') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@php
$groupedPermissions = $permissions->groupBy(function($permission) {
return explode(' ', $permission->name)[1] ?? 'other';
});
@endphp
@forelse($groupedPermissions as $group => $groupPermissions)
{{-- Group Header --}}
<flux:table.row>
<flux:table.cell colspan="4" class="bg-zinc-50 dark:bg-zinc-800/50">
<div class="flex items-center gap-2 py-1 pl-2">
<flux:icon.folder class="h-5 w-5 text-zinc-500" />
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
{{ ucfirst($group) }} {{ __('Permissions') }} ({{ $groupPermissions->count() }})
</flux:heading>
</div>
</flux:table.cell>
</flux:table.row>
{{-- Group Permissions --}}
@foreach($groupPermissions as $permission)
<flux:table.row :key="$permission->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-8">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.key variant="micro" class="text-accent-600 dark:text-accent-400" />
</div>
<div>
<div class="font-medium text-zinc-900 dark:text-white">{{ $permission->name }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ __('Guard:') }} {{ $permission->guard_name }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
@if($permission->roles->isEmpty())
<flux:badge size="sm" color="zinc" icon="exclamation-triangle">
{{ __('Not assigned') }}
</flux:badge>
@else
@foreach($permission->roles as $role)
<flux:badge size="sm" :color="$role->color ?? 'zinc'">
{{ $role->name }}
</flux:badge>
@endforeach
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="text-center font-mono text-sm font-semibold text-zinc-900 dark:text-white">
{{ $permission->roles->count() }}
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="pencil"
tooltip="{{ __('Edit Permission') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="trash"
tooltip="{{ __('Delete Permission') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@endforeach
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="py-12 text-center">
<flux:icon.shield-exclamation variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No permissions found') }}</flux:heading>
<flux:subheading class="mt-2">
{{ __('Get started by creating a new permission.') }}
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
</div>
@endif
{{-- Edit Role Modal --}}
<flux:modal name="edit-role-modal" :variant="'flyout'" wire:model="showEditModal">
<form wire:submit="saveRole" class="space-y-6 max-w-2xl">
<div>
<flux:heading size="lg">{{ __('Edit Role') }}</flux:heading>
<flux:subheading>
@if($selectedRole)
{{ __('Editing role') }}: <strong>{{ $selectedRole->name }}</strong>
@endif
</flux:subheading>
</div>
<flux:separator />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Role Name') }}</flux:label>
<flux:input wire:model="roleName" placeholder="{{ __('Enter role name') }}" />
@error('roleName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Role Icon') }}</flux:label>
<flux:input wire:model="roleIcon" placeholder="{{ __('Enter role icon') }}" />
@error('roleIcon') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Role Display Name') }}</flux:label>
<flux:input wire:model="roleDisplayName" placeholder="{{ __('Enter role display name') }}" />
@error('roleDisplayName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Color') }}</flux:label>
<flux:description>{{ __('Select a color for this role') }}</flux:description>
<flux:select wire:model.live="roleColor">
<flux:select.option value="red">{{ __('Red') }}</flux:select.option>
<flux:select.option value="orange">{{ __('Orange') }}</flux:select.option>
<flux:select.option value="lime">{{ __('Lime') }}</flux:select.option>
<flux:select.option value="teal">{{ __('Teal') }}</flux:select.option>
<flux:select.option value="indigo">{{ __('Indigo') }}</flux:select.option>
<flux:select.option value="purple">{{ __('Purple') }}</flux:select.option>
<flux:select.option value="pink">{{ __('Pink') }}</flux:select.option>
<flux:select.option value="accent">{{ __('Accent (Cyan)') }}</flux:select.option>
<flux:select.option value="yellow">{{ __('Yellow') }}</flux:select.option>
<flux:select.option value="green">{{ __('Green') }}</flux:select.option>
<flux:select.option value="zinc">{{ __('Gray') }}</flux:select.option>
</flux:select>
@error('roleColor') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
@if($roleColor)
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Preview:') }}</flux:subheading>
<flux:badge size="md" :color="$roleColor">
{{ $roleName ?: __('Role Name') }}
</flux:badge>
</div>
@endif
<flux:field>
<flux:label>{{ __('Permissions') }}</flux:label>
<flux:description>{{ __('Select permissions for this role') }}</flux:description>
<div class="mt-3 max-h-96 space-y-2 overflow-y-auto rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
@php
$groupedPerms = $allPermissions->groupBy(function($permission) {
return explode(' ', $permission->name)[1] ?? 'other';
});
@endphp
@foreach($groupedPerms as $group => $perms)
<div class="mb-4">
<flux:subheading class="mb-2 uppercase text-xs">{{ ucfirst($group) }}</flux:subheading>
<div class="ml-2 space-y-2">
@foreach($perms as $permission)
<flux:checkbox
wire:model="rolePermissions"
:value="$permission->name"
:label="$permission->name"
/>
@endforeach
</div>
</div>
@endforeach
</div>
</flux:field>
@if(!empty($rolePermissions))
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Selected Permissions:') }} ({{ count($rolePermissions) }})</flux:subheading>
<div class="flex flex-wrap gap-2">
@foreach($rolePermissions as $permName)
<flux:badge size="sm" color="zinc">
{{ $permName }}
</flux:badge>
@endforeach
</div>
</div>
@endif
</div>
<flux:separator />
<div class="flex justify-between gap-2">
<flux:button type="button" variant="ghost" wire:click="closeEditModal">
{{ __('Cancel') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Save Role') }}
</flux:button>
</div>
</form>
</flux:modal>
{{-- Success Message --}}
@if (session()->has('message'))
<flux:toast :variant="'success'">
{{ session('message') }}
</flux:toast>
@endif
</div>

View file

@ -0,0 +1,254 @@
<?php
use App\Models\User;
use App\Models\Partner;
use App\Models\PartnerInvitation;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.guest'), Title('Willkommen bei B2In')] class extends Component {
public PartnerInvitation $invitation;
public string $firstName = '';
public string $lastName = '';
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public bool $acceptTerms = false;
public function mount(string $token): void
{
$this->invitation = PartnerInvitation::with('role')->where('token', $token)->firstOrFail();
// Prüfe ob Einladung abgelaufen ist
if ($this->invitation->isExpired()) {
$this->invitation->markAsExpired();
$this->redirect('/partner/invitation/expired/' . $token);
}
// Prüfe ob Einladung bereits verwendet wurde
if ($this->invitation->status !== 'pending') {
$this->redirect('/partner/invitation/used/' . $token);
}
$this->email = $this->invitation->email;
$this->firstName = $this->invitation->contact_first_name ?? '';
$this->lastName = $this->invitation->contact_last_name ?? '';
}
public function createAccount(): void
{
$this->validate([
'firstName' => 'required|string|max:255',
'lastName' => 'required|string|max:255',
'email' => 'required|email',
'password' => 'required|string|min:8|confirmed',
'acceptTerms' => 'accepted',
], [
'firstName.required' => __('Bitte geben Sie Ihren Vornamen ein.'),
'firstName.max' => __('Der Vorname darf maximal 255 Zeichen lang sein.'),
'lastName.required' => __('Bitte geben Sie Ihren Nachnamen ein.'),
'lastName.max' => __('Der Nachname darf maximal 255 Zeichen lang sein.'),
'email.required' => __('Bitte geben Sie Ihre E-Mail-Adresse ein.'),
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
'password.required' => __('Bitte geben Sie ein Passwort ein.'),
'password.min' => __('Das Passwort muss mindestens 8 Zeichen lang sein.'),
'password.confirmed' => __('Die Passwörter stimmen nicht überein.'),
'acceptTerms.accepted' => __('Sie müssen die AGB und Datenschutzbestimmungen akzeptieren.'),
]);
try {
\DB::beginTransaction();
// 1. Erstelle Partner-Firma
$partner = Partner::create([
'company_name' => $this->invitation->company_name,
'slug' => Str::slug($this->invitation->company_name),
'type' => $this->invitation->role->name,
'is_active' => false, // Wird später im Setup-Wizard aktiviert
]);
// 2. Erstelle User
$user = User::create([
'partner_id' => $partner->id,
'name' => $this->firstName . ' ' . $this->lastName,
'email' => $this->email,
'password' => Hash::make($this->password),
'email_verified_at' => now(), // Auto-verifiziert durch Einladung
]);
// 3. Weise Rolle zu
$user->assignRole($this->invitation->role);
// 4. Markiere Einladung als akzeptiert
$this->invitation->markAsAccepted($partner);
// 5. Logge User ein
Auth::login($user);
\DB::commit();
// 6. Weiterleitung zum Setup-Wizard
session()->flash('message', __('Willkommen bei B2In! Vervollständigen Sie nun Ihr Profil.'));
$this->redirect(route('partner.setup.wizard'), navigate: true);
} catch (\Exception $e) {
\DB::rollBack();
$this->addError('email', __('Fehler beim Erstellen des Kontos: ') . $e->getMessage());
}
}
}; ?>
<div class="w-full max-w-2xl">
{{-- Header --}}
<div class="text-center mb-8">
@include('partials.logo-head')
<h1 class="text-4xl font-bold text-zinc-900 dark:text-white mb-2">
{{ __('Willkommen, :company!', ['company' => $invitation->company_name]) }}
</h1>
<p class="text-lg text-zinc-600 dark:text-zinc-400">
{{ __('Erstellen Sie Ihr persönliches Konto, um Ihr Partner-Profil einzurichten.') }}
</p>
</div>
{{-- Card mit Formular --}}
<flux:card class="shadow-2xl">
<form wire:submit="createAccount" class="space-y-6">
{{-- Partner Info Badge --}}
<div class="flex items-center justify-center gap-3 p-4 bg-accent-50 dark:bg-accent-900/20 rounded-lg">
<flux:icon.briefcase class="h-6 w-6 text-accent-600 dark:text-accent-400" />
<div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Partner-Typ') }}</div>
<div class="font-semibold text-zinc-900 dark:text-white">
{{ $invitation->role?->display_name ?? $invitation->role?->name }}
</div>
</div>
</div>
<flux:separator />
{{-- Name Felder --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:field>
<flux:label>{{ __('Ihr Vorname') }} <span class="text-red-500">*</span></flux:label>
<flux:input
wire:model="firstName"
placeholder="{{ __('z.B. Max') }}"
icon="user"
autofocus
/>
@error('firstName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Ihr Nachname') }} <span class="text-red-500">*</span></flux:label>
<flux:input
wire:model="lastName"
placeholder="{{ __('z.B. Mustermann') }}"
icon="user"
/>
@error('lastName') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
{{-- E-Mail (gesperrt) --}}
<flux:field>
<flux:label>{{ __('E-Mail-Adresse') }}</flux:label>
<flux:input
wire:model="email"
type="email"
icon="envelope"
disabled
/>
<flux:description>{{ __('Diese E-Mail-Adresse wurde in der Einladung festgelegt und kann nicht geändert werden.') }}</flux:description>
@error('email') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:separator />
{{-- Passwort --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:field>
<flux:label>{{ __('Passwort festlegen') }} <span class="text-red-500">*</span></flux:label>
<flux:input
wire:model="password"
type="password"
placeholder="{{ __('Mindestens 8 Zeichen') }}"
icon="lock-closed"
/>
@error('password') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Passwort bestätigen') }} <span class="text-red-500">*</span></flux:label>
<flux:input
wire:model="password_confirmation"
type="password"
placeholder="{{ __('Passwort wiederholen') }}"
icon="lock-closed"
/>
@error('password_confirmation') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
</div>
{{-- AGB Checkbox --}}
<flux:field>
<div class="flex items-start gap-3">
<flux:checkbox wire:model="acceptTerms" />
<div class="text-sm text-zinc-600 dark:text-zinc-400">
{{ __('Ich akzeptiere die') }}
<a href="#" class="text-accent-600 hover:text-accent-700 dark:text-accent-400">{{ __('AGB') }}</a>
{{ __('und') }}
<a href="#" class="text-accent-600 hover:text-accent-700 dark:text-accent-400">{{ __('Datenschutzbestimmungen') }}</a>.
</div>
</div>
@error('acceptTerms') <flux:error>{{ $message }}</flux:error> @enderror
</flux:field>
<flux:separator />
{{-- Error Alert --}}
<x-error-alert />
{{-- Submit Button --}}
<div class="flex justify-end">
<flux:button
type="submit"
variant="primary"
icon="arrow-right"
class="w-full md:w-auto"
wire:loading.attr="disabled"
wire:target="createAccount"
>
<span wire:loading.remove wire:target="createAccount">
{{ __('Konto erstellen & Setup starten') }}
</span>
<span wire:loading wire:target="createAccount">
<flux:icon.arrow-path class="animate-spin inline-block mr-2 h-4 w-4" />
{{ __('Wird erstellt...') }}
</span>
</flux:button>
</div>
</form>
</flux:card>
{{-- Footer Hinweis --}}
<div class="mt-6 text-center">
<p class="text-sm text-zinc-500 dark:text-zinc-400">
{{ __('Diese Einladung ist gültig bis zum') }}
@if($invitation->expires_at)
<strong>{{ $invitation->expires_at->format('d.m.Y H:i') }}</strong> {{ __('Uhr') }}.
@else
<strong>{{ __('unbegrenzt') }}</strong> {{ __('Uhr') }}.
@endif
</p>
</div>
</div>

View file

@ -0,0 +1,549 @@
<?php
use App\Models\Partner;
use App\Models\Brand;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\WithFileUploads;
use Livewire\Volt\Component;
new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends Component {
use WithFileUploads;
public Partner $partner;
public string $partnerType;
public int $currentStep = 1;
public int $totalSteps;
public array $steps = [];
// Schritt 1: Stammdaten (alle Rollen)
public string $companyName = '';
public $logo = null;
public string $description = '';
public string $street = '';
public string $zip = '';
public string $city = '';
public string $website = '';
// Schritt 2: Retailer - Liefergebiete
public ?int $deliveryRadius = null;
public ?int $assemblyRadius = null;
// Schritt 2: Manufacturer - Marke
public string $brandName = '';
public $brandLogo = null;
public string $brandDescription = '';
public string $roleIcon = 'shield-check';
public string $roleName = '-';
public function mount(): void
{
$user = Auth::user();
if (!$user->partner_id) {
$this->redirect(route('dashboard'), navigate: true);
return;
}
$role = $user->roles->first();
if ($role) {
$this->roleIcon = $role->icon ?? 'shield-check';
$this->roleName = $role->display_name ?? $role->name;
}
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
$this->partnerType = $this->partner->type;
// Vorausfüllen
$this->companyName = $this->partner->company_name;
$this->description = $this->partner->description ?? '';
$this->website = '';
// Definiere Schritte basierend auf Rolle
$this->defineSteps();
}
protected function defineSteps(): void
{
switch ($this->partnerType) {
case 'Retailer':
$this->steps = ['Stammdaten', 'Liefergebiete', 'Fertig'];
$this->totalSteps = 3;
break;
case 'Manufacturer':
$this->steps = ['Stammdaten', 'Marke anlegen', 'Fertig'];
$this->totalSteps = 3;
break;
case 'Estate-Agent':
$this->steps = ['Profil', 'Fertig'];
$this->totalSteps = 2;
break;
default:
$this->steps = ['Stammdaten', 'Fertig'];
$this->totalSteps = 2;
break;
}
}
public function saveStep1(): void
{
$this->validate(
[
'companyName' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'street' => 'required|string|max:255',
'zip' => 'required|string|max:10',
'city' => 'required|string|max:255',
'website' => 'nullable|url|max:255',
'logo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
],
[
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
'street.required' => __('Bitte geben Sie eine Straße ein.'),
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
'logo.image' => __('Das Logo muss eine Bilddatei sein.'),
'logo.mimes' => __('Das Logo muss im Format JPG, PNG oder WebP sein.'),
'logo.max' => __('Das Logo darf maximal 2 MB groß sein.'),
],
);
// Speichere Logo falls hochgeladen
$logoPath = null;
if ($this->logo) {
$logoPath = $this->logo->store('partner-logos', 'public');
}
// Update Partner
$this->partner->update([
'company_name' => $this->companyName,
'description' => $this->description,
'logo_url' => $logoPath ?? $this->partner->logo_url,
]);
// TODO: Adresse speichern (separates Address-Model oder JSON-Feld)
$this->currentStep = 2;
}
public function saveStep2Retailer(): void
{
$this->validate(
[
'deliveryRadius' => 'required|integer|min:1|max:500',
'assemblyRadius' => 'required|integer|min:1|max:500',
],
[
'deliveryRadius.required' => __('Bitte geben Sie einen Lieferradius ein.'),
'deliveryRadius.min' => __('Der Lieferradius muss mindestens 1 km betragen.'),
'assemblyRadius.required' => __('Bitte geben Sie einen Montageradius ein.'),
'assemblyRadius.min' => __('Der Montageradius muss mindestens 1 km betragen.'),
],
);
$this->partner->update([
'delivery_radius_km' => $this->deliveryRadius,
'assembly_radius_km' => $this->assemblyRadius,
]);
$this->completeSetup();
}
public function saveStep2Manufacturer(): void
{
$this->validate(
[
'brandName' => 'required|string|max:255',
'brandDescription' => 'nullable|string|max:1000',
'brandLogo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
],
[
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
'brandLogo.image' => __('Das Marken-Logo muss eine Bilddatei sein.'),
'brandLogo.mimes' => __('Das Marken-Logo muss im Format JPG, PNG oder WebP sein.'),
'brandLogo.max' => __('Das Marken-Logo darf maximal 2 MB groß sein.'),
],
);
// Speichere Brand-Logo falls hochgeladen
$brandLogoPath = null;
if ($this->brandLogo) {
$brandLogoPath = $this->brandLogo->store('brand-logos', 'public');
}
// Erstelle Brand
Brand::create([
'partner_id' => $this->partner->id,
'name' => $this->brandName,
'slug' => Str::slug($this->brandName),
'description' => $this->brandDescription,
'logo_url' => $brandLogoPath,
'is_active' => true,
]);
$this->completeSetup();
}
protected function completeSetup(): void
{
$this->partner->update([
'is_active' => true,
'setup_completed' => true,
'setup_completed_at' => now(),
]);
$this->currentStep = $this->totalSteps;
}
public function goToDashboard(): void
{
$this->redirect(route('dashboard'), navigate: true);
}
public function logout(): void
{
Auth::logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
$this->redirect(route('home'), navigate: true);
}
}; ?>
<div class="w-full max-w-3xl">
{{-- Header --}}
<div class="text-center mb-8">
@include('partials.logo-head')
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white mb-2">
{{ __('Vervollständigen Sie Ihr Profil') }}
</h1>
</div>
{{-- Progress Indicator --}}
<x-wizard-progress :currentStep="$currentStep" :totalSteps="$totalSteps" :steps="$steps" />
{{-- Wizard Content --}}
<flux:card class="shadow-2xl">
{{-- Step 1: Stammdaten (für alle Rollen) --}}
@if ($currentStep === 1)
<form wire:submit="saveStep1" class="space-y-6">
<div>
<flux:heading size="lg" class="mb-2">
<div class="flex items-center gap-2">
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5')
@if ($partnerType === 'Retailer')
{{ __('Ihre Stammdaten') }}
@elseif($partnerType === 'Manufacturer')
{{ __('Ihre Stammdaten') }}
@else
{{ __('Ihr Profil') }}
@endif
/ {{ $roleName }}
</div>
</flux:heading>
<flux:subheading>
{{ __('Diese Informationen helfen uns, Ihr Profil zu vervollständigen.') }}
</flux:subheading>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="companyName" icon="building-office" />
@error('companyName')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Logo (optional)') }}</flux:label>
<flux:description>{{ __('Laden Sie Ihr Firmenlogo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
<div class="space-y-2">
<input type="file" wire:model.live="logo" accept="image/jpeg,image/png,image/jpg,image/webp"
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
@error('logo')
<flux:error>{{ $message }}</flux:error>
@enderror
<div wire:loading wire:target="logo" class="text-sm text-accent-600 dark:text-accent-400">
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
{{ __('Wird hochgeladen...') }}
</div>
@if ($logo)
<div wire:loading.remove wire:target="logo">
@try
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<div class="flex items-center gap-3">
<img src="{{ $logo->temporaryUrl() }}" class="h-16 w-16 object-contain rounded border"
alt="Logo Preview">
<div class="flex-1">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
<p class="text-xs text-green-600 dark:text-green-400">{{ $logo->getClientOriginalName() }}</p>
</div>
</div>
</div>
@catch(\Exception $e)
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
</div>
@endtry
</div>
@endif
</div>
</flux:field>
<flux:field>
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
<flux:textarea wire:model="description" rows="4"
placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
@error('description')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:separator />
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<flux:field class="md:col-span-2">
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="street" wire:/>
@error('street')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:field>
<flux:label>{{ __('PLZ') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="zip" />
@error('zip')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="city" icon="map-pin" />
@error('city')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Website (optional)') }}</flux:label>
<flux:input wire:model="website" type="url" icon="globe-alt" placeholder="https://..." />
@error('website')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:separator />
<x-error-alert />
<div class="flex justify-between">
<flux:button type="button" variant="outline" icon="arrow-left-start-on-rectangle"
wire:click="logout">
{{ __('Logout') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="arrow-right">
@if ($partnerType === 'Retailer')
{{ __('Weiter zu Liefergebiete') }}
@elseif($partnerType === 'Manufacturer')
{{ __('Weiter zu Marke') }}
@else
{{ __('Setup abschließen') }}
@endif
</flux:button>
</div>
</form>
@endif
{{-- Step 2: Retailer - Liefergebiete --}}
@if ($currentStep === 2 && $partnerType === 'Retailer')
<form wire:submit="saveStep2Retailer" class="space-y-6">
<div>
<flux:heading size="lg" class="mb-2">
🚚 {{ __('Ihre Liefergebiete') }}
</flux:heading>
<flux:subheading>
{{ __('Wie weit liefern und montieren Sie von Ihrer Adresse (:zip :city) aus?', ['zip' => $zip, 'city' => $city]) }}
</flux:subheading>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Lieferradius') }} <span class="text-red-500">*</span></flux:label>
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
<div class="flex items-center gap-4">
<flux:input wire:model="deliveryRadius" type="number" min="1" max="500"
class="flex-1" />
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
</div>
@error('deliveryRadius')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Montageradius') }} <span class="text-red-500">*</span></flux:label>
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
<div class="flex items-center gap-4">
<flux:input wire:model="assemblyRadius" type="number" min="1" max="500"
class="flex-1" />
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
</div>
@error('assemblyRadius')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:separator />
<x-error-alert />
<div class="flex justify-end">
<flux:button type="submit" variant="primary" icon="check">
{{ __('Setup abschließen') }}
</flux:button>
</div>
</form>
@endif
{{-- Step 2: Manufacturer - Marke anlegen --}}
@if ($currentStep === 2 && $partnerType === 'Manufacturer')
<form wire:submit="saveStep2Manufacturer" class="space-y-6">
<div>
<flux:heading size="lg" class="mb-2">
™️ {{ __('Ihre Marke') }}
</flux:heading>
<flux:subheading>
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }}
</flux:subheading>
</div>
<flux:separator />
<flux:field>
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="brandName" icon="tag"
placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
@error('brandName')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:field>
<flux:label>{{ __('Marken-Logo (optional)') }}</flux:label>
<flux:description>{{ __('Laden Sie Ihr Marken-Logo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
<div class="space-y-2">
<input type="file" wire:model.live="brandLogo" accept="image/jpeg,image/png,image/jpg,image/webp"
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
@error('brandLogo')
<flux:error>{{ $message }}</flux:error>
@enderror
<div wire:loading wire:target="brandLogo" class="text-sm text-accent-600 dark:text-accent-400">
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
{{ __('Wird hochgeladen...') }}
</div>
@if ($brandLogo)
<div wire:loading.remove wire:target="brandLogo">
@try
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<div class="flex items-center gap-3">
<img src="{{ $brandLogo->temporaryUrl() }}"
class="h-16 w-16 object-contain rounded border" alt="Brand Logo Preview">
<div class="flex-1">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
<p class="text-xs text-green-600 dark:text-green-400">{{ $brandLogo->getClientOriginalName() }}</p>
</div>
</div>
</div>
@catch(\Exception $e)
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
</div>
@endtry
</div>
@endif
</div>
</flux:field>
<flux:field>
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
<flux:textarea wire:model="brandDescription" rows="4"
placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
@error('brandDescription')
<flux:error>{{ $message }}</flux:error>
@enderror
</flux:field>
<flux:separator />
<x-error-alert />
<div class="flex justify-end">
<flux:button type="submit" variant="primary" icon="check">
{{ __('Setup abschließen') }}
</flux:button>
</div>
</form>
@endif
{{-- Final Step: Fertig! --}}
@if ($currentStep === $totalSteps)
<div class="text-center space-y-6 py-8">
<div class="flex justify-center">
<div
class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/20">
<flux:icon.check-circle class="h-12 w-12 text-green-600 dark:text-green-400" />
</div>
</div>
<div>
<flux:heading size="xl" class="mb-2">
{{ __('Einrichtung abgeschlossen!') }}
</flux:heading>
<flux:subheading>
@if ($partnerType === 'Retailer')
{{ __('Sie sind nun ein aktiver Händler. Sie können jetzt Ihr Sortiment pflegen.') }}
@elseif($partnerType === 'Manufacturer')
{{ __('Sie sind nun ein aktiver Hersteller. Sie können jetzt Ihre Produkte anlegen.') }}
@else
{{ __('Ihr Profil ist aktiv. In Ihrem Dashboard finden Sie Ihre persönlichen Einladungslinks.') }}
@endif
</flux:subheading>
</div>
<flux:separator />
<div class="flex flex-col sm:flex-row gap-4 justify-center">
@if ($partnerType !== 'Estate-Agent')
<flux:button variant="primary" icon="plus" size="lg">
{{ __('Erstes Produkt anlegen') }}
</flux:button>
@endif
<flux:button wire:click="goToDashboard" variant="outline" icon="home" size="lg">
{{ __('Zum Dashboard') }}
</flux:button>
</div>
</div>
@endif
</flux:card>
</div>

View file

@ -1,189 +0,0 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Livewire\Volt\Component;
use Livewire\WithPagination; // Wichtig für Paginierung
new class extends Component {
use WithPagination;
// Optional: Such- und Filter-Properties
public string $search = '';
public string $statusFilter = '';
public string $roleFilter = '';
// Optional: Sortierung
public string $sortField = 'name';
public string $sortDirection = 'asc';
/**
* Mount the component.
*/
public function mount(): void
{
// Standardwerte für Sortierung setzen
$this->sortField = 'name';
$this->sortDirection = 'asc';
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortDirection = 'asc';
}
$this->sortField = $field;
}
// Die Hauptmethode, um Daten zu laden
public function users()
{
return User::query()
->when($this->search, fn($query, $search) =>
$query->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%')
)
->when($this->statusFilter, fn($query, $status) =>
$query->where('status', $status)
)
->when($this->roleFilter, fn($query, $role) =>
$query->where('role', $role) // oder 'role_id' wenn du IDs verwendest
)
->orderBy($this->sortField, $this->sortDirection)
->paginate(10); // 10 Einträge pro Seite
}
// Optional: Lifecycle hook für das Zurücksetzen der Paginierung bei Suche/Filterung
public function updatedSearch() { $this->resetPage(); }
public function updatedStatusFilter() { $this->resetPage(); }
public function updatedRoleFilter() { $this->resetPage(); }
// Wird für die Paginierung mit Tailwind benötigt (Standard in Livewire 3)
// Wenn FluxUI Bootstrap-basierte Paginierung braucht, musst du das anpassen
// public function paginationView()
// {
// return 'vendor.livewire.tailwind'; // oder 'livewire::bootstrap'
// }
// Wenn du mit Relationen arbeitest (z.B. user->role->name)
// public function with(): array
// {
// return [
// 'users' => User::with(['role', 'group']) // Eager loading
// // ... deine Query-Logik von oben ...
// ->paginate(10),
// ];
// }
}; ?>
<div>
{{-- Filter und Suchleiste (Beispiel, FluxUI Klassen anpassen) --}}
<div class="mb-4 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-100 rounded">
<div>
<label for="search" class="block text-sm font-medium text-gray-700">Suche</label>
<input wire:model.live.debounce.300ms="search" id="search" type="text" placeholder="Name oder E-Mail..."
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
</div>
<div>
<label for="statusFilter" class="block text-sm font-medium text-gray-700">Status</label>
<select wire:model.live="statusFilter" id="statusFilter"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="">Alle</option>
<option value="active">Aktiv</option>
<option value="inactive">Inaktiv</option>
<option value="pending">Ausstehend</option>
</select>
</div>
<div>
<label for="roleFilter" class="block text-sm font-medium text-gray-700">Rolle</label>
<input wire:model.live.debounce.300ms="roleFilter" id="roleFilter" type="text" placeholder="Rolle..."
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
</div>
</div>
{{-- Tabelle (FluxUI Klassen anpassen!) --}}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 shadow">
<thead class="bg-gray-50"> {{-- FluxUI Klasse für Thead --}}
<tr>
<th scope="col" wire:click="sortBy('name')"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
Name @if($sortField === 'name')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
</th>
<th scope="col" wire:click="sortBy('email')"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
E-Mail @if($sortField === 'email')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
</th>
<th scope="col" wire:click="sortBy('status')"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
Status @if($sortField === 'status')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
</th>
<th scope="col" wire:click="sortBy('role')"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
Rolle @if($sortField === 'role')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Gruppe
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">
Rechte
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">Aktionen</span>
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200"> {{-- FluxUI Klasse für Tbody --}}
@forelse ($this->users() as $user)
<tr wire:key="{{ $user->id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ $user->name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $user->email }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ $user->status === 'active' ? 'bg-green-100 text-green-800' : ($user->status === 'inactive' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800') }}">
{{ ucfirst($user->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $user->role }} {{-- Oder $user->role->name, wenn es eine Relation ist --}}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $user->group }} {{-- Oder $user->group->name --}}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{-- Darstellung der Rechte. Wenn es eine Many-to-Many Relation ist: --}}
{{-- @foreach($user->permissions as $permission)
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
{{ $permission->name }}
</span>
@endforeach --}}
{{-- Oder wenn es ein JSON-Feld ist, musst du es parsen und anzeigen --}}
Einfache Rechte-Anzeige (Todo)
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="#" class="text-indigo-600 hover:text-indigo-900">Bearbeiten</a> {{-- FluxUI Button-Klassen --}}
<button wire:click="$dispatch('openModal', { component: 'admin.delete-user-modal', arguments: { userId: {{ $user->id }} }})"
class="text-red-600 hover:text-red-900 ml-2">Löschen</button> {{-- FluxUI Button-Klassen --}}
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
Keine Benutzer gefunden.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">
{{ $this->users()->links() }} {{-- Stellt sicher, dass die Paginierungs-Views für dein UI-Kit konfiguriert sind --}}
</div>
</div>

View file

@ -1,144 +0,0 @@
<?php
use App\Models\User;
use Livewire\Volt\Component;
use Livewire\WithPagination; // Wichtig für Paginierung
new class extends Component {
use WithPagination;
// Optional: Such- und Filter-Properties
public string $search = '';
public string $statusFilter = '';
public string $roleFilter = '';
// Optional: Sortierung
public $sortBy = 'name';
public $sortDirection = 'asc';
/**
* Mount the component.
*/
public function mount(): void
{
// Standardwerte für Sortierung setzen
$this->sortBy = 'name';
$this->sortDirection = 'asc';
}
public function sort($column) {
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
}
/*public function orders()
{
return \App\Models\Order::query()
->tap(fn ($query) => $this->sortBy ? $query->orderBy($this->sortBy, $this->sortDirection) : $query)
->paginate(5);
}*/
// Die Hauptmethode, um Daten zu laden
#[\Livewire\Attributes\Computed]
public function users()
{
return User::query()
->when($this->search, fn($query, $search) =>
$query->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%')
)
->when($this->statusFilter, fn($query, $status) =>
$query->where('status', $status)
)
->when($this->roleFilter, fn($query, $role) =>
$query->where('role', $role) // oder 'role_id' wenn du IDs verwendest
)
->orderBy($this->sortBy, $this->sortDirection)
->paginate(10); // 10 Einträge pro Seite
}
// Optional: Lifecycle hook für das Zurücksetzen der Paginierung bei Suche/Filterung
public function updatedSearch() { $this->resetPage(); }
public function updatedStatusFilter() { $this->resetPage(); }
public function updatedRoleFilter() { $this->resetPage(); }
// Wird für die Paginierung mit Tailwind benötigt (Standard in Livewire 3)
// Wenn FluxUI Bootstrap-basierte Paginierung braucht, musst du das anpassen
// public function paginationView()
// {
// return 'vendor.livewire.tailwind'; // oder 'livewire::bootstrap'
// }
// Wenn du mit Relationen arbeitest (z.B. user->role->name)
// public function with(): array
// {
// return [
// 'users' => User::with(['role', 'group']) // Eager loading
// // ... deine Query-Logik von oben ...
// ->paginate(10),
// ];
// }
}; ?>
<flux:table :paginate="$this->users">
<flux:table.columns>
<flux:table.column>Customer</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'date'" :direction="$sortDirection" wire:click="sort('date')">Date</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDirection" wire:click="sort('status')">Status</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'amount'" :direction="$sortDirection" wire:click="sort('amount')">Amount</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->users as $user)
<flux:table.row :key="$user->id">
<flux:table.cell class="flex items-center gap-3">
<flux:avatar size="xs" src="{{ $user->avatar }}" />
{{ $user->name }}
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">{{ $user->email }}</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="lime" inset="top bottom">RTest</flux:badge>
</flux:table.cell>
<flux:table.cell variant="strong">{{ $user->role }}</flux:table.cell>
<flux:table.cell>
<flux:dropdown>
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" inset="top bottom"></flux:button>
<flux:menu>
<flux:menu.item icon="plus">New post</flux:menu.item>
<flux:menu.separator />
<flux:menu.submenu heading="Sort by">
<flux:menu.radio.group>
<flux:menu.radio checked>Name</flux:menu.radio>
<flux:menu.radio>Date</flux:menu.radio>
<flux:menu.radio>Popularity</flux:menu.radio>
</flux:menu.radio.group>
</flux:menu.submenu>
<flux:menu.submenu heading="Filter">
<flux:menu.checkbox checked>Draft</flux:menu.checkbox>
<flux:menu.checkbox checked>Published</flux:menu.checkbox>
<flux:menu.checkbox>Archived</flux:menu.checkbox>
</flux:menu.submenu>
<flux:menu.separator />
<flux:menu.item variant="danger" icon="trash">Delete</flux:menu.item>
</flux:menu>
</flux:dropdown>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>

View file

@ -69,46 +69,46 @@ new class extends Component {
} }
}; ?> }; ?>
<section class="w-full"> <section class="w-full">
@include('partials.settings-heading') @include('partials.settings-heading')
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')"> <x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6"> <form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" /> <flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
<div> <div>
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" /> <flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail()) @if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
<div> <div>
<flux:text class="mt-4"> <flux:text class="mt-4">
{{ __('Your email address is unverified.') }} {{ __('Your email address is unverified.') }}
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification"> <flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
{{ __('Click here to re-send the verification email.') }} {{ __('Click here to re-send the verification email.') }}
</flux:link> </flux:link>
</flux:text>
@if (session('status') === 'verification-link-sent')
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
{{ __('A new verification link has been sent to your email address.') }}
</flux:text> </flux:text>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-4"> @if (session('status') === 'verification-link-sent')
<div class="flex items-center justify-end"> <flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button> {{ __('A new verification link has been sent to your email address.') }}
</flux:text>
@endif
</div>
@endif
</div> </div>
<x-action-message class="me-3" on="profile-updated"> <div class="flex items-center gap-4">
{{ __('Saved.') }} <div class="flex items-center justify-end">
</x-action-message> <flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
</div> </div>
</form>
<livewire:settings.delete-user-form /> <x-action-message class="me-3" on="profile-updated">
</x-settings.layout> {{ __('Saved.') }}
</section> </x-action-message>
</div>
</form>
<livewire:settings.delete-user-form />
</x-settings.layout>
</section>

View file

@ -1,13 +1,13 @@
<section class="section-padding flex items-center relative overflow-hidden"> <section class="section-padding flex items-center relative border-b border-border/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]">
<div class="grid lg:grid-cols-2 gap-16 items-center"> <div class="grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8"> <div class="space-y-8 slide-right delay-200">
<h1 class="text-hero"> <h1 class="text-hero">
{!! $content['title'] !!} {!! $content['title'] !!}
</h1> </h1>
<blockquote class="text-large text-muted-foreground italic leading-relaxed border-l-4 border-secondary pl-6"> <blockquote class="text-large text-muted-foreground italic leading-relaxed border-l-4 border-secondary pl-6">
{{ $content['quote'] }} {!! $content['quote'] !!}
</blockquote> </blockquote>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
@ -20,17 +20,19 @@
</div> </div>
<div class="relative"> <div class="relative">
<div class="card-elevated rounded-3xl overflow-hidden"> <div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
<img <img src="{{ asset('img/assets/' . $content['image']) }}"
src="{{ asset('img/assets/' . $content['image']) }}"
alt="{{ $content['image_alt'] }}" alt="{{ $content['image_alt'] }}"
class="w-full h-96 lg:h-[500px] object-cover" class="w-full h-[600px] object-cover" />
/> <div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<div class="absolute -bottom-6 -right-6 bg-secondary text-secondary-foreground p-6 rounded-2xl">
<div class="text-3xl font-bold">{{ $content['year'] }}</div>
<p class="text-sm opacity-90">{{ $content['year_text'] }}</p>
</div> </div>
{{-- Floating info card --}}
<div
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
<div class="text-xl font-medium text-muted-foreground">{{ $content['card_title'] }}</div>
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -11,8 +11,8 @@
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
@foreach($content['features'] as $index => $feature) @foreach($content['features'] as $index => $feature)
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}"> <div class="group h-full {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
<div class="card-elevated p-8 overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col slide-up delay-{{ $index * 200 }}"> <div class="card-elevated p-8 overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
<div class="text-center space-y-6"> <div class="text-center space-y-6">
<div class="mx-auto w-16 h-16 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300"> <div class="mx-auto w-16 h-16 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
@if($feature['icon']) @if($feature['icon'])

View file

@ -4,10 +4,10 @@
{{-- Left Content --}} {{-- Left Content --}}
<div class="space-y-8"> <div class="space-y-8">
<div class="space-y-6 slide-right delay-200"> <div class="space-y-6 slide-right delay-200">
<h1 class="text-hero"> <h1 class="text-hero-alt">
{!! $content['title'] !!} {!! $content['title'] !!}
</h1> </h1>
<p class="text-lg text-muted-foreground max-w-md leading-relaxed"> <p class="text-xl text-muted-foreground max-w-md leading-relaxed">
{!! $content['subtitle'] !!} {!! $content['subtitle'] !!}
</p> </p>
</div> </div>
@ -21,15 +21,16 @@
</a> </a>
</div> </div>
<div class="flex flex-wrap items-center gap-6 pt-10 border-t border-border/80 slide-right delay-300"> @if(isset($content['stats']))
@foreach ($content['stats'] as $stat) <div class="flex flex-wrap items-center gap-6 pt-10 mt-10 border-t border-border/80 slide-right delay-300">
<div class="flex items-center gap-2 text-md text-muted-foreground"> @foreach ($content['stats'] as $stat)
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary') <div class="flex items-center gap-2 text-md text-muted-foreground font-light">
<span>{{ $stat }}</span> @svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
</div> <span>{{ $stat }}</span>
@endforeach </div>
</div> @endforeach
</div>
@endif
</div> </div>
@ -43,11 +44,10 @@
{{-- Floating info card --}} {{-- Floating info card --}}
<div <div
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400"> class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
<div class="text-sm text-muted-foreground">{{ $content['card_title'] }}</div> <div class="text-xl font-medium text-muted-foreground">{{ $content['card_title'] }}</div>
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!} <div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
<div> <div>
<!-- Back Navigation --> <!-- Back Navigation -->
<div class="pt-20 pb-4"> <div class="pt-4 pb-4 border-b border-border/30 ">
<div class="container-narrow"> <div class="container-narrow ">
<a href="/magazin" class="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"> <a href="/magazin" class="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
@ -10,13 +10,12 @@
</a> </a>
</div> </div>
</div> </div>
<!-- Article Header --> <!-- Article Header -->
<article class="pb-16"> <article class="pb-16 pt-16">
<div class="container-narrow"> <div class="container-narrow">
<header class="text-center mb-12"> <header class="text-center mb-12 slide-up delay-200">
<h1 class="text-section-title mb-6 leading-tight"> <h1 class="text-section-title mb-6 leading-tight">
{{ $article['title'] }} {!! $article['title'] !!}
</h1> </h1>
<p class="text-large text-muted-foreground mb-8 max-w-3xl mx-auto"> <p class="text-large text-muted-foreground mb-8 max-w-3xl mx-auto">
{{ $article['subtitle'] }} {{ $article['subtitle'] }}
@ -38,7 +37,7 @@
</header> </header>
<!-- Featured Image --> <!-- Featured Image -->
<div class="mb-12 overflow-hidden rounded-lg"> <div class="mb-12 overflow-hidden rounded-lg shadow-md slide-up delay-200">
<img <img
src="{{ asset('img/assets/' . $article['image']) }}" src="{{ asset('img/assets/' . $article['image']) }}"
alt="{{ $article['title'] }}" alt="{{ $article['title'] }}"
@ -50,12 +49,12 @@
<!-- Main Content --> <!-- Main Content -->
<div class="md:col-span-3"> <div class="md:col-span-3">
<div class="prose prose-lg max-w-none"> <div class="prose prose-lg max-w-none">
<p class="text-large text-muted-foreground leading-relaxed mb-8"> <p class="text-lg text-muted-foreground leading-relaxed mb-8 slide-up delay-200">
{{ $article['content']['intro'] }} {{ $article['content']['intro'] }}
</p> </p>
@foreach($article['content']['sections'] as $index => $section) @foreach($article['content']['sections'] as $index => $section)
<section class="mb-10" id="section-{{ $index }}"> <section class="mb-10 bg-light-muted shadow-md p-6 rounded-lg slide-up delay-400" id="section-{{ $index }}">
<h2 class="text-2xl font-medium text-foreground mb-4"> <h2 class="text-2xl font-medium text-foreground mb-4">
{{ $index + 1 }}. {{ $section['title'] }} {{ $index + 1 }}. {{ $section['title'] }}
</h2> </h2>
@ -71,21 +70,25 @@
<div class="md:col-span-1"> <div class="md:col-span-1">
<div class="sticky top-24"> <div class="sticky top-24">
<div class="card-elevated rounded-lg p-6"> <div class="card-elevated rounded-lg p-2 slide-left delay-400">
<h3 class="font-medium text-foreground mb-4">{{ $content['share_article'] }}</h3> <h3 class="font-medium text-foreground mb-4 text-center font-2xl py-2">{{ $content['share_article'] }}</h3>
<div class="space-y-3"> <div class="space-y-3">
<button class="btn-secondary-accent w-full"> <button class="btn-secondary-accent w-full">
<div class="flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/> <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg> </svg>
<span class="font-medium">LinkedIn</span> <span class="font-medium text-sm">LinkedIn</span>
</div>
</button> </button>
<button class="btn-secondary-accent w-full"> <button class="btn-secondary-accent w-full">
<div class="flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/> <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg> </svg>
<span class="font-medium text-sm">Facebook</span> <span class="font-medium text-sm">Facebook</span>
</div>
</button> </button>
</div> </div>
</div> </div>

View file

@ -12,7 +12,7 @@
<div class="space-y-8"> <div class="space-y-8">
@foreach($this->posts as $post) @foreach($this->posts as $post)
<article class="group"> <article class="group">
<div class="card-elevated rounded-3xl overflow-hidden h-full hover:scale-[1.02] transition-all duration-300"> <div class="card-elevated rounded-3xl overflow-hidden h-full transition-all duration-300">
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
<div class="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]"> <div class="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]">
<img <img
@ -33,7 +33,7 @@
<h3 class="text-xl lg:text-2xl font-semibold text-foreground leading-tight group-hover:text-secondary transition-colors duration-200"> <h3 class="text-xl lg:text-2xl font-semibold text-foreground leading-tight group-hover:text-secondary transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}" class="stretched-link"> <a href="/magazin/{{ $post['id'] }}" class="stretched-link">
{{ $post['title'] }} {!! $post['title'] !!}
</a> </a>
</h3> </h3>

View file

@ -1,22 +1,42 @@
<section class="section-padding"> <section class="section-padding">
<div class="container-padding text-center"> <div class="container-padding text-center">
<h2 class="text-section-title text-foreground mb-12"> <div class="text-center mb-16 slide-up delay-200">
{!! $content['title'] !!} <h2 class="text-section-title text-foreground mb-12">
</h2> {!! $content['title'] !!}
</h2>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
@foreach($content['timeline'] as $index => $item) @foreach($content['timeline'] as $index => $card)
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}"> <div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
<div class="card-elevated p-8 rounded-3xl h-full hover:scale-105 transition-all duration-300 relative overflow-hidden"> <div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
<div class="text-center space-y-6">
<div class="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
<div class="w-6 h-6 bg-secondary rounded-full"></div> @if(isset($card['icon']))
</div> <div class="relative pt-12 pb-8">
<h3 class="text-xl font-semibold text-foreground">{{ $item['title'] }}</h3> <div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
@svg('heroicon-o-'.$card['icon'], 'w-10 h-10 text-secondary-foreground')
</div>
</div>
@endif
<div class="p-6 spacing-small flex flex-col justify-between flex-grow">
<div class="mb-4">
@if (isset($card['logo']))
<img src="{{ asset($card['logo']) }}" alt="{{ $card['title'] }}"
class="{{ $card['logo_width'] }} h-18 object-contain" />
@endif
@if(isset($card['title']))
<h3 class="text-2xl font-medium text-center">{{ $card['title'] }}</h3>
@endif
@if(isset($card['description']))
<p class="text-muted-foreground leading-relaxed mt-4 text-center">
{{ $card['description'] }}
</p>
@endif
</div>
<p class="text-muted-foreground text-sm leading-relaxed">
{{ $item['description'] }}
</p>
</div> </div>
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-secondary/20 via-secondary to-secondary/20 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></div> <div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-secondary/20 via-secondary to-secondary/20 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></div>

View file

@ -21,35 +21,41 @@ function renderHeroIcon($iconName, $style = 'outline') {
@endphp @endphp
<section class="section-padding"> <section class="section-padding">
<div class="container-padding"> <div class="container-padding">
<div class="text-center mb-16"> <div class="text-center mb-16 slide-up delay-300">
<h2 class="text-section-title mb-6"> <h2 class="text-section-title">{!! $content['title'] !!}</h2>
{!! $content['title'] !!} <p class="text-large text-muted-foreground mt-4 max-w-3xl mx-auto">
</h2> {!! $content['subtitle'] !!}
<p class="text-large text-muted-foreground max-w-2xl mx-auto">
{{ $content['subtitle'] }}
</p> </p>
</div> </div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
@foreach($content['values'] as $index => $value) @foreach($content['values'] as $index => $value)
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}"> <div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
<div class="card-elevated p-8 rounded-3xl h-full hover:scale-105 transition-all duration-300 relative overflow-hidden"> <div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
<div class="text-center space-y-6"> @if(isset($value['icon']))
<div class="mx-auto w-20 h-20 bg-secondary/10 rounded-2xl flex items-center justify-center group-hover:bg-secondary/20 transition-colors duration-300"> <div class="relative pt-12 pb-8">
{!! renderHeroIcon($value['icon'], $value['icon_style'] ?? 'outline') !!} <div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
@svg('heroicon-o-'.$value['icon'], 'w-10 h-10 text-secondary-foreground')
</div>
</div>
@endif
<div class="p-6 spacing-small flex flex-col justify-between flex-grow">
<div class="mb-4">
@if (isset($value['logo']))
<img src="{{ asset($value['logo']) }}" alt="{{ $value['title'] }}"
class="{{ $value['logo_width'] }} h-18 object-contain" />
@endif
@if(isset($value['title']))
<h3 class="text-xl font-medium text-center">{{ $value['title'] }}</h3>
@endif
@if(isset($value['description']))
<p class="text-muted-foreground leading-relaxed mt-2 text-center">
{{ $value['description'] }}
</p>
@endif
</div>
</div> </div>
<h3 class="text-2xl font-semibold text-foreground">
{{ $value['title'] }}
</h3>
<div class="w-12 h-px bg-secondary mx-auto"></div>
<p class="text-muted-foreground leading-relaxed">
{{ $value['description'] }}
</p>
</div>
<div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-secondary/20 via-secondary to-secondary/20 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></div> <div class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-secondary/20 via-secondary to-secondary/20 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></div>
</div> </div>
</div> </div>

View file

@ -27,6 +27,16 @@
@endforeach @endforeach
</div> </div>
@endif @endif
@if(isset($content['stats']))
<div class="flex flex-wrap items-center gap-6 pt-10 border-t border-border/80 slide-right delay-300">
@foreach ($content['stats'] as $stat)
<div class="flex items-center gap-2 text-md text-muted-foreground">
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
<span>{{ $stat }}</span>
</div>
@endforeach
</div>
@endif
</div> </div>
@ -39,15 +49,13 @@
</div> </div>
{{-- Floating info card --}} {{-- Floating info card --}}
<div <div
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400"> class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-400">
<div class="text-center slide-left delay-400"> <div class="text-xl font-medium text-muted-foreground">{!! $content['hub']['title'] !!}</div>
<h3 class="text-xl font-semibold text-foreground">{{ $content['hub']['title'] }}</h3> <div class="text-lg font-medium font-secondary">{!! $content['hub']['subtitle'] !!}</div>
<p class="text-sm text-muted-foreground">{{ $content['hub']['subtitle'] }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -1,17 +1,7 @@
<section class="section-padding {{ $bg }}"> <section class="section-padding {{ $bg }}">
<div class="container-padding"> <div class="container-padding">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center"> <div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{{-- Content --}}
<div class="spacing-section">
<div class="spacing-content slide-right delay-300">
<h2 class="text-section-title">{{ $content['title'] }}</h2>
<div class="spacing-small text-large text-muted-foreground leading-relaxed">
@foreach ($content['paragraphs'] as $paragraph)
<p>{!! $paragraph !!}</p>
@endforeach
</div>
</div>
</div>
{{-- Image --}} {{-- Image --}}
<div class="relative"> <div class="relative">
@ -24,6 +14,19 @@
{{ $content['image_caption'] }}</div> {{ $content['image_caption'] }}</div>
</div> </div>
</div> </div>
{{-- Content --}}
<div class="spacing-section">
<div class="spacing-content slide-right delay-300">
<h2 class="text-section-title">{{ $content['title'] }}</h2>
<div class="spacing-small text-large text-muted-foreground leading-relaxed">
@foreach ($content['paragraphs'] as $paragraph)
<p>{!! $paragraph !!}</p>
@endforeach
</div>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -4,7 +4,7 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]">
<div class="grid lg:grid-cols-2 gap-12 items-center"> <div class="grid lg:grid-cols-2 gap-12 items-center">
<!-- Left Side - Hero Text --> <!-- Left Side - Hero Text -->
<div> <div class="slide-right delay-200">
<h1 class="text-hero mb-6 tracking-wide"> <h1 class="text-hero mb-6 tracking-wide">
{!! $content['hero']['title'] ?? 'Send us a<br /><span class="text-secondary font-medium">message.</span>' !!} {!! $content['hero']['title'] ?? 'Send us a<br /><span class="text-secondary font-medium">message.</span>' !!}
</h1> </h1>
@ -14,7 +14,7 @@
</div> </div>
<!-- Right Side - Contact Form --> <!-- Right Side - Contact Form -->
<div class="card-elevated p-8"> <div class="card-elevated p-8 slide-left delay-200">
@if (session()->has('message')) @if (session()->has('message'))
<div class="mb-6 p-4 bg-gray-50 border border-secondary/20 rounded-lg text-secondary"> <div class="mb-6 p-4 bg-gray-50 border border-secondary/20 rounded-lg text-secondary">
{{ session('message') }} {{ session('message') }}
@ -153,8 +153,8 @@
<section class="section-padding"> <section class="section-padding">
<div class="container-padding"> <div class="container-padding">
<div class="grid md:grid-cols-3 gap-8"> <div class="grid md:grid-cols-3 gap-8">
@foreach($this->contactInfo as $info) @foreach($this->contactInfo as $index => $info)
<div class="card-elevated p-8 rounded-xl text-center"> <div class="card-elevated p-8 rounded-xl text-center slide-up delay-{{ $index * 200 }}">
<div class="w-12 h-12 bg-secondary/20 rounded-full flex items-center justify-center mx-auto mb-4"> <div class="w-12 h-12 bg-secondary/20 rounded-full flex items-center justify-center mx-auto mb-4">
@if($info['icon'] === 'map-pin') @if($info['icon'] === 'map-pin')
<svg class="w-6 h-6 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -187,7 +187,7 @@
<section class="section-padding bg-accent"> <section class="section-padding bg-accent">
<div class="container-padding"> <div class="container-padding">
<div class="grid lg:grid-cols-2 gap-12 items-center"> <div class="grid lg:grid-cols-2 gap-12 items-center">
<div> <div class="slide-right delay-200">
<h2 class="text-section-title mb-6"> <h2 class="text-section-title mb-6">
{!! $content['social_media']['title'] ?? 'Follow for<br /><span class="text-secondary font-medium">exclusives</span>' !!} {!! $content['social_media']['title'] ?? 'Follow for<br /><span class="text-secondary font-medium">exclusives</span>' !!}
</h2> </h2>
@ -197,8 +197,8 @@
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
@foreach($this->socialMedia as $social) @foreach($this->socialMedia as $index => $social)
<div class="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer"> <div class="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors slide-left delay-{{ $index * 200 }} cursor-pointer">
<div> <div>
<h3 class="text-xl text-foreground">{{ $social['name'] }}</h3> <h3 class="text-xl text-foreground">{{ $social['name'] }}</h3>
<p class="text-sm text-muted-foreground">{{ $social['handle'] }}</p> <p class="text-sm text-muted-foreground">{{ $social['handle'] }}</p>

View file

@ -1,5 +1,6 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('app.name') }}</title> <title>{{ $title ?? config('app.name') }}</title>
@ -8,9 +9,13 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
rel="stylesheet" />
@vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal') @vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal')
@livewireStyles
@fluxAppearance @fluxAppearance

View file

@ -0,0 +1,8 @@
<div class="mb-6 mx-auto flex justify-center">
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}"
alt="B2IN Logo"
class="h-14 w-auto dark:hidden" />
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}"
alt="B2IN Logo"
class="h-14 w-auto hidden dark:block" />
</div>

Some files were not shown because too many files have changed in this diff Show more