21-11-2025
|
|
@ -149,6 +149,9 @@ source ~/.bashrc
|
|||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@ class BasicAuthMiddleware
|
|||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Skip Basic Auth für Livewire-Requests komplett
|
||||
// Diese sind bereits durch Laravel Session/CSRF geschützt
|
||||
$path = $request->path();
|
||||
|
||||
if (
|
||||
str_starts_with($path, 'livewire/') ||
|
||||
str_contains($path, '/livewire/') ||
|
||||
$request->is('livewire/*') ||
|
||||
$request->is('*/livewire/*')
|
||||
) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Credentials from .env file
|
||||
$user = config('auth.basic.user');
|
||||
$pass = config('auth.basic.password');
|
||||
|
|
|
|||
40
app/Http/Middleware/EnsurePartnerSetupCompleted.php
Normal 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');
|
||||
}
|
||||
}
|
||||
61
app/Mail/PartnerInvitationMail.php
Normal 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
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Attribute extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Models/AttributeValue.php
Normal 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
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Models/Collection.php
Normal 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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
26
app/Models/HubLocation.php
Normal 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
|
|
@ -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);
|
||||
// }
|
||||
}
|
||||
144
app/Models/PartnerInvitation.php
Normal 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());
|
||||
}
|
||||
}
|
||||
10
app/Models/ProductVariant.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProductVariant extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Models/ShippingClass.php
Normal 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
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Models/TaxRate.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TaxRate extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Support\Str;
|
|||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
|
@ -22,9 +23,11 @@ class User extends Authenticatable
|
|||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'partner_id',
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'email_verified_at',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -50,6 +53,10 @@ class User extends Authenticatable
|
|||
];
|
||||
}
|
||||
|
||||
public function partner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Partner::class);
|
||||
}
|
||||
/**
|
||||
* Get the user's initials
|
||||
*/
|
||||
|
|
@ -57,7 +64,7 @@ class User extends Authenticatable
|
|||
{
|
||||
return Str::of($this->name)
|
||||
->explode(' ')
|
||||
->map(fn (string $name) => Str::of($name)->substr(0, 1))
|
||||
->map(fn(string $name) => Str::of($name)->substr(0, 1))
|
||||
->implode('');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
|
|
@ -19,6 +20,14 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
// Always force HTTPS when request comes via HTTPS
|
||||
// Important for Livewire signed URLs (file uploads) behind Traefik proxy
|
||||
$scheme = request()->header('X-Forwarded-Proto')
|
||||
?? request()->server('HTTP_X_FORWARDED_PROTO')
|
||||
?? (request()->secure() ? 'https' : 'http');
|
||||
|
||||
if ($scheme === 'https') {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
health: '/up',
|
||||
)
|
||||
->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)) {
|
||||
$middleware->web(\App\Http\Middleware\BasicAuthMiddleware::class);
|
||||
$middleware->append(\App\Http\Middleware\BasicAuthMiddleware::class);
|
||||
}
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ return [
|
|||
App\Providers\ThemeServiceProvider::class,
|
||||
App\Providers\VoltServiceProvider::class,
|
||||
Barryvdh\Debugbar\ServiceProvider::class,
|
||||
FluxPro\FluxProServiceProvider::class,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"laravel/sanctum": "^4.1",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"livewire/flux": "^2.1.1",
|
||||
"livewire/flux-pro": "^2.1",
|
||||
"livewire/flux-pro": "^2.6",
|
||||
"livewire/volt": "^1.7.0",
|
||||
"spatie/laravel-permission": "^6.17"
|
||||
},
|
||||
|
|
@ -88,14 +88,18 @@
|
|||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"repositories": [
|
||||
{
|
||||
"repositories": {
|
||||
"flux-pro": {
|
||||
"type": "composer",
|
||||
"url": "https://composer.fluxui.dev"
|
||||
},
|
||||
"0": {
|
||||
"type": "path",
|
||||
"url": "packages/*/*"
|
||||
},
|
||||
{
|
||||
"1": {
|
||||
"type": "composer",
|
||||
"url": "https://composer.fluxui.dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
391
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "10a392ebee126b2fc1bc1946158acb90",
|
||||
"content-hash": "dec4bfb2c36983f51725d04db995a549",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
|
|
@ -633,29 +633,28 @@
|
|||
},
|
||||
{
|
||||
"name": "dragonmantank/cron-expression",
|
||||
"version": "v3.4.0",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dragonmantank/cron-expression.git",
|
||||
"reference": "8c784d071debd117328803d86b2097615b457500"
|
||||
"reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500",
|
||||
"reference": "8c784d071debd117328803d86b2097615b457500",
|
||||
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013",
|
||||
"reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0",
|
||||
"webmozart/assert": "^1.0"
|
||||
"php": "^8.2|^8.3|^8.4|^8.5"
|
||||
},
|
||||
"replace": {
|
||||
"mtdowling/cron-expression": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpunit/phpunit": "^7.0|^8.0|^9.0"
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^1.12.32|^2.1.31",
|
||||
"phpunit/phpunit": "^8.5.48|^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
|
|
@ -686,7 +685,7 @@
|
|||
],
|
||||
"support": {
|
||||
"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": [
|
||||
{
|
||||
|
|
@ -694,7 +693,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-10-09T13:47:03+00:00"
|
||||
"time": "2025-10-31T18:51:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "egulias/email-validator",
|
||||
|
|
@ -1309,16 +1308,16 @@
|
|||
},
|
||||
{
|
||||
"name": "laravel/fortify",
|
||||
"version": "v1.31.1",
|
||||
"version": "v1.31.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/fortify.git",
|
||||
"reference": "e39a49592e1440508337a765cdc913ff5bcba66f"
|
||||
"reference": "a046d52ee087ee52c9852b840cf4bbad19f10934"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/fortify/zipball/e39a49592e1440508337a765cdc913ff5bcba66f",
|
||||
"reference": "e39a49592e1440508337a765cdc913ff5bcba66f",
|
||||
"url": "https://api.github.com/repos/laravel/fortify/zipball/a046d52ee087ee52c9852b840cf4bbad19f10934",
|
||||
"reference": "a046d52ee087ee52c9852b840cf4bbad19f10934",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1370,20 +1369,20 @@
|
|||
"issues": "https://github.com/laravel/fortify/issues",
|
||||
"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",
|
||||
"version": "v12.34.0",
|
||||
"version": "v12.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687"
|
||||
"reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687",
|
||||
"reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125",
|
||||
"reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -1589,7 +1588,7 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"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",
|
||||
|
|
@ -2032,16 +2031,16 @@
|
|||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.30.0",
|
||||
"version": "3.30.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "2203e3151755d874bb2943649dae1eb8533ac93e"
|
||||
"reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e",
|
||||
"reference": "2203e3151755d874bb2943649dae1eb8533ac93e",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da",
|
||||
"reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2109,9 +2108,9 @@
|
|||
],
|
||||
"support": {
|
||||
"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",
|
||||
|
|
@ -2394,16 +2393,16 @@
|
|||
},
|
||||
{
|
||||
"name": "livewire/flux",
|
||||
"version": "v2.6.0",
|
||||
"version": "v2.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/flux.git",
|
||||
"reference": "3cb2ea40978449da74b3814eeef75f0388124224"
|
||||
"reference": "227b88db0a02db91666af2303ea6727a3af78c51"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/flux/zipball/3cb2ea40978449da74b3814eeef75f0388124224",
|
||||
"reference": "3cb2ea40978449da74b3814eeef75f0388124224",
|
||||
"url": "https://api.github.com/repos/livewire/flux/zipball/227b88db0a02db91666af2303ea6727a3af78c51",
|
||||
"reference": "227b88db0a02db91666af2303ea6727a3af78c51",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2411,7 +2410,7 @@
|
|||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"illuminate/view": "^10.0|^11.0|^12.0",
|
||||
"laravel/prompts": "^0.1|^0.2|^0.3",
|
||||
"livewire/livewire": "^3.5.19",
|
||||
"livewire/livewire": "^3.5.19|^4.0",
|
||||
"php": "^8.1",
|
||||
"symfony/console": "^6.0|^7.0"
|
||||
},
|
||||
|
|
@ -2454,26 +2453,26 @@
|
|||
],
|
||||
"support": {
|
||||
"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",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.1",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://composer.fluxui.dev/download/a01b7791-4494-476d-bfd6-9605f43121a7/flux-pro-2.6.0.zip",
|
||||
"reference": "0b2f0c4523bded72b06a47532cf5db248cfa3072",
|
||||
"shasum": "7741f0b5bb2e9a69cf3ec20976f40a9f396b770d"
|
||||
"url": "https://composer.fluxui.dev/download/a0397651-df75-43ac-b21a-8a5ac8ad46b4/flux-pro-2.6.1.zip",
|
||||
"reference": "12a6570b061c858739b40a9509424c4b4cc42b62",
|
||||
"shasum": "10e8f4dad0b0232e5b47ce291ef1c55610be5298"
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"illuminate/view": "^10.0|^11.0|^12.0",
|
||||
"laravel/prompts": "^0.1.24|^0.2|^0.3",
|
||||
"livewire/flux": "2.6.0|dev-main",
|
||||
"livewire/livewire": "^3.6.2",
|
||||
"livewire/flux": "2.6.1|dev-main",
|
||||
"livewire/livewire": "^3.6.2|^4.0",
|
||||
"php": "^8.1",
|
||||
"symfony/console": "^6.0|^7.0"
|
||||
},
|
||||
|
|
@ -2513,7 +2512,7 @@
|
|||
"livewire",
|
||||
"ui"
|
||||
],
|
||||
"time": "2025-10-13T23:31:47+00:00"
|
||||
"time": "2025-10-28T21:23:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "livewire/livewire",
|
||||
|
|
@ -2593,21 +2592,21 @@
|
|||
},
|
||||
{
|
||||
"name": "livewire/volt",
|
||||
"version": "v1.7.2",
|
||||
"version": "v1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/volt.git",
|
||||
"reference": "91ba934e72bbd162442840862959ade24dbe728a"
|
||||
"reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/volt/zipball/91ba934e72bbd162442840862959ade24dbe728a",
|
||||
"reference": "91ba934e72bbd162442840862959ade24dbe728a",
|
||||
"url": "https://api.github.com/repos/livewire/volt/zipball/4b289eef2f15398987a923d9f813cad6a6a19ea4",
|
||||
"reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"laravel/framework": "^10.38.2|^11.0|^12.0",
|
||||
"livewire/livewire": "^3.6.1",
|
||||
"livewire/livewire": "^3.6.1|^4.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
|
@ -2661,7 +2660,7 @@
|
|||
"issues": "https://github.com/livewire/volt/issues",
|
||||
"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",
|
||||
|
|
@ -2873,25 +2872,25 @@
|
|||
},
|
||||
{
|
||||
"name": "nette/schema",
|
||||
"version": "v1.3.2",
|
||||
"version": "v1.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nette/schema.git",
|
||||
"reference": "da801d52f0354f70a638673c4a0f04e16529431d"
|
||||
"reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d",
|
||||
"reference": "da801d52f0354f70a638673c4a0f04e16529431d",
|
||||
"url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
|
||||
"reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"nette/utils": "^4.0",
|
||||
"php": "8.1 - 8.4"
|
||||
"php": "8.1 - 8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.5.2",
|
||||
"phpstan/phpstan-nette": "^1.0",
|
||||
"phpstan/phpstan-nette": "^2.0@stable",
|
||||
"tracy/tracy": "^2.8"
|
||||
},
|
||||
"type": "library",
|
||||
|
|
@ -2901,6 +2900,9 @@
|
|||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nette\\": "src"
|
||||
},
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
|
|
@ -2929,9 +2931,9 @@
|
|||
],
|
||||
"support": {
|
||||
"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",
|
||||
|
|
@ -3024,16 +3026,16 @@
|
|||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.6.1",
|
||||
"version": "v5.6.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
|
||||
"reference": "3a454ca033b9e06b63282ce19562e892747449bb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
|
||||
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
|
||||
"reference": "3a454ca033b9e06b63282ce19562e892747449bb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -3076,37 +3078,37 @@
|
|||
],
|
||||
"support": {
|
||||
"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",
|
||||
"version": "v2.3.1",
|
||||
"version": "v2.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nunomaduro/termwind.git",
|
||||
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123"
|
||||
"reference": "eb61920a53057a7debd718a5b89c2178032b52c0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123",
|
||||
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123",
|
||||
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0",
|
||||
"reference": "eb61920a53057a7debd718a5b89c2178032b52c0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^8.2",
|
||||
"symfony/console": "^7.2.6"
|
||||
"symfony/console": "^7.3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"illuminate/console": "^11.44.7",
|
||||
"laravel/pint": "^1.22.0",
|
||||
"illuminate/console": "^11.46.1",
|
||||
"laravel/pint": "^1.25.1",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"pestphp/pest": "^2.36.0 || ^3.8.2",
|
||||
"phpstan/phpstan": "^1.12.25",
|
||||
"pestphp/pest": "^2.36.0 || ^3.8.4",
|
||||
"phpstan/phpstan": "^1.12.32",
|
||||
"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"
|
||||
},
|
||||
"type": "library",
|
||||
|
|
@ -3149,7 +3151,7 @@
|
|||
],
|
||||
"support": {
|
||||
"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": [
|
||||
{
|
||||
|
|
@ -3165,7 +3167,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-05-08T08:14:37+00:00"
|
||||
"time": "2025-10-18T11:10:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/constant_time_encoding",
|
||||
|
|
@ -3777,16 +3779,16 @@
|
|||
},
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
"version": "v0.12.12",
|
||||
"version": "v0.12.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/psysh.git",
|
||||
"reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7"
|
||||
"reference": "95c29b3756a23855a30566b745d218bee690bef2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/cd23863404a40ccfaf733e3af4db2b459837f7e7",
|
||||
"reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7",
|
||||
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2",
|
||||
"reference": "95c29b3756a23855a30566b745d218bee690bef2",
|
||||
"shasum": ""
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.2"
|
||||
"bamarni/composer-bin-plugin": "^1.2",
|
||||
"composer/class-map-generator": "^1.6"
|
||||
},
|
||||
"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-pdo-sqlite": "The doc command requires SQLite to work.",
|
||||
"ext-posix": "If you have PCNTL, you'll want the POSIX extension as well."
|
||||
},
|
||||
"bin": [
|
||||
|
|
@ -3849,9 +3852,9 @@
|
|||
],
|
||||
"support": {
|
||||
"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",
|
||||
|
|
@ -4053,16 +4056,16 @@
|
|||
},
|
||||
{
|
||||
"name": "spatie/laravel-permission",
|
||||
"version": "6.21.0",
|
||||
"version": "6.23.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-permission.git",
|
||||
"reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3"
|
||||
"reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3",
|
||||
"reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
|
||||
"reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4124,7 +4127,7 @@
|
|||
],
|
||||
"support": {
|
||||
"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": [
|
||||
{
|
||||
|
|
@ -4132,7 +4135,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-23T16:08:05+00:00"
|
||||
"time": "2025-11-03T20:16:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
|
|
@ -4210,16 +4213,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.3.4",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
|
||||
"reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
|
||||
"reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7",
|
||||
"reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4284,7 +4287,7 @@
|
|||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.3.4"
|
||||
"source": "https://github.com/symfony/console/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -4304,7 +4307,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-22T15:31:00+00:00"
|
||||
"time": "2025-10-14T15:46:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
|
|
@ -4681,16 +4684,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.3.2",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "2a6614966ba1074fa93dae0bc804227422df4dfe"
|
||||
"reference": "9f696d2f1e340484b4683f7853b273abff94421f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe",
|
||||
"reference": "2a6614966ba1074fa93dae0bc804227422df4dfe",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f",
|
||||
"reference": "9f696d2f1e340484b4683f7853b273abff94421f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4725,7 +4728,7 @@
|
|||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v7.3.2"
|
||||
"source": "https://github.com/symfony/finder/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -4745,20 +4748,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-15T13:41:35+00:00"
|
||||
"time": "2025-10-15T18:45:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v7.3.4",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6"
|
||||
"reference": "ce31218c7cac92eab280762c4375fb70a6f4f897"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6",
|
||||
"reference": "c061c7c18918b1b64268771aad04b40be41dd2e6",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce31218c7cac92eab280762c4375fb70a6f4f897",
|
||||
"reference": "ce31218c7cac92eab280762c4375fb70a6f4f897",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4808,7 +4811,7 @@
|
|||
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.3.4"
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -4828,20 +4831,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-16T08:38:17+00:00"
|
||||
"time": "2025-10-24T21:42:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-kernel",
|
||||
"version": "v7.3.4",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-kernel.git",
|
||||
"reference": "b796dffea7821f035047235e076b60ca2446e3cf"
|
||||
"reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf",
|
||||
"reference": "b796dffea7821f035047235e076b60ca2446e3cf",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/24fd3f123532e26025f49f1abefcc01a69ef15ab",
|
||||
"reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4926,7 +4929,7 @@
|
|||
"description": "Provides a structured process for converting a Request into a Response",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v7.3.4"
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -4946,20 +4949,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-27T12:32:17+00:00"
|
||||
"time": "2025-10-28T10:19:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mailer",
|
||||
"version": "v7.3.4",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mailer.git",
|
||||
"reference": "ab97ef2f7acf0216955f5845484235113047a31d"
|
||||
"reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d",
|
||||
"reference": "ab97ef2f7acf0216955f5845484235113047a31d",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba",
|
||||
"reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -5010,7 +5013,7 @@
|
|||
"description": "Helps sending emails",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mailer/tree/v7.3.4"
|
||||
"source": "https://github.com/symfony/mailer/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -5030,7 +5033,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-17T05:51:54+00:00"
|
||||
"time": "2025-10-24T14:27:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
|
|
@ -6526,16 +6529,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v7.3.4",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
|
||||
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
|
||||
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
|
||||
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -6589,7 +6592,7 @@
|
|||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -6609,7 +6612,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-11T10:12:26+00:00"
|
||||
"time": "2025-09-27T09:00:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
|
|
@ -6823,64 +6826,6 @@
|
|||
}
|
||||
],
|
||||
"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": [
|
||||
|
|
@ -7820,16 +7765,16 @@
|
|||
},
|
||||
{
|
||||
"name": "laravel/sail",
|
||||
"version": "v1.46.0",
|
||||
"version": "v1.47.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/sail.git",
|
||||
"reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e"
|
||||
"reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/sail/zipball/eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e",
|
||||
"reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e",
|
||||
"url": "https://api.github.com/repos/laravel/sail/zipball/9a11e822238167ad8b791e4ea51155d25cf4d8f2",
|
||||
"reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -7842,7 +7787,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
"phpstan/phpstan": "^2.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/sail"
|
||||
|
|
@ -7879,7 +7824,7 @@
|
|||
"issues": "https://github.com/laravel/sail/issues",
|
||||
"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",
|
||||
|
|
@ -11008,16 +10953,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.3.3",
|
||||
"version": "v7.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
|
||||
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
|
||||
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc",
|
||||
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -11060,7 +11005,7 @@
|
|||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.3.3"
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.3.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -11080,7 +11025,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-27T11:34:33+00:00"
|
||||
"time": "2025-09-27T09:00:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ta-tikoma/phpunit-architecture-test",
|
||||
|
|
@ -11190,6 +11135,64 @@
|
|||
}
|
||||
],
|
||||
"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": [],
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ return [
|
|||
]
|
||||
],
|
||||
'hero' => [
|
||||
'title' => 'Lokaler Handel trifft auf <span class="text-secondary">europäisches Design</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.',
|
||||
'title' => '<span class="text-secondary">B2in</span> – Brücken zwischen lokalen Kunden, lokalem Handel und <span class="text-secondary">inspirierenden Designs.</span>',
|
||||
'subtitle' => 'Wo lokaler Handel und namhafte Lieferanten digital, aber lokal, in einem Netzwerk zusammenarbeiten.',
|
||||
'image' => 'b2in/hero-room.jpg',
|
||||
'image_alt' => 'Modern international skyline showcasing architectural design',
|
||||
'cta1_text' => 'Für lokale Händler',
|
||||
|
|
@ -30,7 +30,7 @@ return [
|
|||
'Persönlicher Service',
|
||||
'Werte die bleiben'
|
||||
],
|
||||
'card_title' => '',
|
||||
'card_title' => 'B2in',
|
||||
'card_text' => 'Connecting Design and <span class="text-secondary">Property</span>'
|
||||
],
|
||||
'ecosystem_core' => [
|
||||
|
|
@ -57,9 +57,9 @@ return [
|
|||
'vision_section' => [
|
||||
'title' => 'Gebaut auf Vertrauen',
|
||||
'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.',
|
||||
'Gleichzeitig bieten wir visionären Herstellern einen kuratierten, direkten Zugang zu regionalen Märkten.',
|
||||
'Wir glauben an die Kraft der Synergie, nicht an den Wettbewerb zwischen Online und Offline.'
|
||||
'<strong>B2in (Bridges2international)</strong> verbindet Immobilienmakler, Möbelfachhändler, Möbelhersteller und Markenpartner auf einer gemeinsamen Plattform.',
|
||||
'<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.',
|
||||
'<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_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.',
|
||||
'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_alt' => 'Integriertes Modell',
|
||||
'image_caption' => 'Integriertes Modell'
|
||||
'image' => 'b2in/best-of-two-worlds.jpg',
|
||||
'image_alt' => 'Das Ergebnis für den Kunden, das perfekte Zuhause',
|
||||
'image_caption' => 'Das Ergebnis für den Kunden, das perfekte Zuhause'
|
||||
],
|
||||
'cta_section' => [
|
||||
'title' => ' Werden <span class="text-primary">Sie Partner</span><br>im führenden Möbel-Netzwerk',
|
||||
|
|
@ -203,14 +203,16 @@ return [
|
|||
]
|
||||
],
|
||||
'about_hero' => [
|
||||
'title' => 'Über <span class="text-secondary">B2in</span>',
|
||||
'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."',
|
||||
'title' => '<span class="text-secondary">Über B2in:</span> Unsere Mission',
|
||||
'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_title' => 'Gründer & CEO, B2in',
|
||||
'image' => 'b2in/about-hero.jpg',
|
||||
'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' => [
|
||||
'title' => 'Lifetime <span class="text-secondary">Vergütung</span> für Makler',
|
||||
|
|
@ -287,7 +289,7 @@ return [
|
|||
'image_alt' => 'Luxury interior design'
|
||||
],
|
||||
'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.',
|
||||
'features' => [
|
||||
[
|
||||
|
|
@ -314,16 +316,15 @@ return [
|
|||
'image' => 'b2in/ecosystem-hero.jpg',
|
||||
'image_alt' => 'Ecosystem Hero Image',
|
||||
'card_title' => 'B2in Portal',
|
||||
'card_text' => 'Zentrale Plattform',
|
||||
'card_text' => 'Die Technologie dahinter, die das Ökosystem ermöglicht',
|
||||
'hub' => [
|
||||
'title' => 'B2in Portal',
|
||||
'subtitle' => 'Zentrale Plattform'
|
||||
'subtitle' => 'Die Technologie dahinter, die das Ökosystem ermöglicht'
|
||||
],
|
||||
'connection_points' => [
|
||||
['name' => 'Endkunden'],
|
||||
['name' => 'Makler'],
|
||||
['name' => 'Lieferanten'],
|
||||
['name' => 'B2A']
|
||||
'stats' => [
|
||||
'Exklusive Auswahl',
|
||||
'Persönlicher Service',
|
||||
'Werte die bleiben'
|
||||
]
|
||||
],
|
||||
'ecosystem_stats' => [
|
||||
|
|
@ -358,9 +359,9 @@ return [
|
|||
'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.',
|
||||
],
|
||||
'image' => 'b2in/integriertes-model.jpg',
|
||||
'image_alt' => 'Die Marken',
|
||||
'image_caption' => 'Die Marken',
|
||||
'image' => 'b2in/ecosystem_start.jpg',
|
||||
'image_alt' => 'Die Marken für den Endkunden',
|
||||
'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.',
|
||||
],
|
||||
'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.',
|
||||
'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_alt' => 'Das B2In-Erlebnis',
|
||||
'image_caption' => 'Das B2In-Erlebnis',
|
||||
'image_description' => 'Eine klare Infografik, die zeigt: [Lokales Angebot] + [Überregionales Angebot] = Das B2In-Erlebnis..',
|
||||
'image' => 'b2in/ecosystem_hub.jpg',
|
||||
'image_alt' => 'Die Synergie zwischen lokalem und überregionalem Angebot',
|
||||
'image_caption' => 'Die Synergie zwischen lokalem und überregionalem Angebot',
|
||||
'image_description' => 'Eine klare Infografik, die zeigt: [Lokales Angebot] + [Überregionales Angebot] = Die Synergie zwischen lokalem und überregionalem Angebot.',
|
||||
],
|
||||
'ecosystem_result' => [
|
||||
'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.'
|
||||
],
|
||||
],
|
||||
'image' => 'b2in/integriertes-model.jpg',
|
||||
'image_alt' => 'style2own und stileigentum',
|
||||
'image_caption' => 'style2own und stileigentum',
|
||||
'image_description' => 'Eine stilisierte Grafik, die zeigt, wie von den Logos style2own und stileigentum Pfeile in den nächsten Abschnitt führen.',
|
||||
'image' => 'b2in/ecosystem_result.jpg',
|
||||
'image_alt' => 'Ihr Erfolg und die Partnerschaft mit B2in',
|
||||
'image_caption' => 'Ihr Erfolg und die Partnerschaft mit B2in',
|
||||
'image_description' => 'Eine stilisierte Grafik, die zeigt, wie Ihr Erfolg und die Partnerschaft mit B2in zusammenhängen.',
|
||||
],
|
||||
'end_customer_section' => [
|
||||
'tag' => 'Für Endkunden',
|
||||
|
|
@ -496,18 +497,21 @@ return [
|
|||
'timeline' => [
|
||||
[
|
||||
'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',
|
||||
'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',
|
||||
'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' => [
|
||||
'title' => 'Unsere <span class="text-secondary">Werte</span>',
|
||||
|
|
@ -515,37 +519,32 @@ return [
|
|||
'values' => [
|
||||
[
|
||||
'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_style' => 'solid',
|
||||
],
|
||||
[
|
||||
'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_style' => 'solid',
|
||||
],
|
||||
[
|
||||
'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_style' => 'solid',
|
||||
],
|
||||
[
|
||||
'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_style' => 'solid',
|
||||
],
|
||||
[
|
||||
'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_style' => 'solid',
|
||||
],
|
||||
[
|
||||
'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',
|
||||
],
|
||||
]
|
||||
|
|
@ -883,19 +882,19 @@ return [
|
|||
[
|
||||
'name' => 'Marcel Scheibe',
|
||||
'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',
|
||||
],
|
||||
[
|
||||
'name' => 'Sarah Müller',
|
||||
'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',
|
||||
],
|
||||
[
|
||||
'name' => 'Thomas Weber',
|
||||
'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',
|
||||
],
|
||||
]
|
||||
|
|
@ -2223,7 +2222,7 @@ return [
|
|||
'articles' => [
|
||||
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.',
|
||||
'image' => 'b2in/magazin-1.jpg',
|
||||
'category' => 'Immobilien-Marketing',
|
||||
|
|
@ -2254,7 +2253,7 @@ return [
|
|||
],
|
||||
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.',
|
||||
'image' => 'b2in/magazin-2.jpg',
|
||||
'category' => 'Investment & Trends',
|
||||
|
|
@ -2285,7 +2284,7 @@ return [
|
|||
],
|
||||
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.',
|
||||
'image' => 'b2in/magazin-3.jpg',
|
||||
'category' => 'B2B & Handel',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||
'png',
|
||||
'gif',
|
||||
'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...
|
||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
};
|
||||
26
database/migrations/2025_11_06_115527_create_hubs_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
35
database/migrations/2025_11_06_153100_create_media_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
28
database/migrations/2025_11_06_153520_create_tags_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ namespace Database\Seeders;
|
|||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
|
|
@ -16,8 +17,9 @@ class DatabaseSeeder extends Seeder
|
|||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'name' => 'Kevin Adametz',
|
||||
'email' => 'kevin.adametz@me.com',
|
||||
'password' => Hash::make('xunfew-0Jygjy-minnyt'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
171
database/seeders/RoleSeeder.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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']);*/
|
||||
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 136 KiB |
BIN
public/img/assets/b2in/best-of-two-world.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
public/img/assets/b2in/best-of-two-worlds.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 133 KiB |
BIN
public/img/assets/b2in/ecosystem_hub.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
public/img/assets/b2in/ecosystem_result.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/img/assets/b2in/ecosystem_start.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
|
@ -9,7 +9,7 @@
|
|||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@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-100: #f5f5f5;
|
||||
|
|
|
|||
|
|
@ -9,32 +9,33 @@
|
|||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@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-100: #f5f5f5;
|
||||
--color-zinc-200: #e5e5e5;
|
||||
--color-zinc-300: #d4d4d4;
|
||||
--color-zinc-400: #a3a3a3;
|
||||
--color-zinc-500: #737373;
|
||||
--color-zinc-600: #525252;
|
||||
--color-zinc-700: #404040;
|
||||
--color-zinc-800: #262626;
|
||||
--color-zinc-900: #171717;
|
||||
--color-zinc-950: #0a0a0a;
|
||||
--background: 32 20% 97%; /* #f5f4f2 - Light Beige */
|
||||
/* Custom accent color palette based on HSL(199, 74%, 49%) */
|
||||
--color-accent-50: hsl(199 74% 97%);
|
||||
--color-accent-100: hsl(199 74% 92%);
|
||||
--color-accent-200: hsl(199 74% 82%);
|
||||
--color-accent-300: hsl(199 74% 70%);
|
||||
--color-accent-400: hsl(199 74% 59%);
|
||||
--color-accent-500: hsl(199 74% 49%);
|
||||
--color-accent-600: hsl(199 74% 39%);
|
||||
--color-accent-700: hsl(199 74% 29%);
|
||||
--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);
|
||||
--color-accent-content: var(--color-neutral-800);
|
||||
/* FluxUI accent variables */
|
||||
--color-accent: hsl(199 74% 49%);
|
||||
--color-accent-content: hsl(199 74% 39%);
|
||||
--color-accent-foreground: var(--color-white);
|
||||
}
|
||||
|
||||
@layer theme {
|
||||
.dark {
|
||||
--color-accent: var(--color-white);
|
||||
--color-accent-content: var(--color-white);
|
||||
--color-accent-foreground: var(--color-neutral-800);
|
||||
--color-accent: hsl(199 74% 59%);
|
||||
--color-accent-content: hsl(199 74% 49%);
|
||||
--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;
|
||||
}
|
||||
|
||||
.shadow-elegant {
|
||||
box-shadow: 0 4px 12px -8px rgba(0, 136, 204, 0.4);
|
||||
}
|
||||
.bg-background {
|
||||
background-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
/* \[:where(&)\]:size-4 {
|
||||
@apply size-4;
|
||||
} */
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ h1, h2, h3, h4, h5, h6 {
|
|||
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 {
|
||||
font-size: clamp(1.6rem, 3vw, 3rem);
|
||||
line-height: 1.3em;
|
||||
|
|
|
|||
|
|
@ -177,8 +177,7 @@
|
|||
/* Cards - Multi-Layer-Schatten für Tiefe (Optimiert) */
|
||||
& .card,
|
||||
& [class*="card"],
|
||||
& .bg-card,
|
||||
& article {
|
||||
& .bg-card {
|
||||
background:
|
||||
linear-gradient(
|
||||
145deg,
|
||||
|
|
@ -206,8 +205,7 @@
|
|||
|
||||
|
||||
& .card:hover,
|
||||
& [class*="card"]:hover,
|
||||
& article:hover {
|
||||
& [class*="card"]:hover {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04),
|
||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,44 @@
|
|||
<livewire:notifications />
|
||||
</div>
|
||||
<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 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" />
|
||||
|
|
@ -16,3 +53,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
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>
|
||||
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}" alt="B2IN Logo" class="h-10 w-auto dark:hidden" />
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}" alt="B2IN Logo" class="h-10 w-auto hidden dark:block" />
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 257 B |
|
|
@ -1,6 +1,5 @@
|
|||
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
|
||||
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
|
||||
</div>
|
||||
<div class="ms-1 grid flex-1 text-start text-sm">
|
||||
<span class="mb-0.5 truncate leading-none font-semibold">B2IN</span>
|
||||
<div class="flex size-16 items-center justify-center ml-2 ">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}" alt="B2IN Logo" class="h-10 w-auto dark:hidden" />
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}" alt="B2IN Logo" class="h-10 w-auto hidden dark:block" />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
20
resources/views/components/error-alert.blade.php
Normal 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
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
@include('partials.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.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">
|
||||
|
|
@ -11,21 +11,33 @@
|
|||
</a>
|
||||
|
||||
<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.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.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.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.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.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 :heading="__('CMS')" class="grid mb-4">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<x-layouts.auth.simple :title="$title ?? null">
|
||||
<x-layouts.auth.card :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts.auth.simple>
|
||||
</x-layouts.auth.card>
|
||||
|
|
|
|||
|
|
@ -2,25 +2,31 @@
|
|||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</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="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>
|
||||
<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" />
|
||||
<span class="flex h-20 w-20 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
|
||||
<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>
|
||||
</flux:card>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,23 +2,30 @@
|
|||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</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="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>
|
||||
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
<span class="flex h-12 w-12 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon />
|
||||
</span>
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
<div class="flex flex-col gap-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
<script src="{{ asset('vendor/livewire/livewire.js') }}"></script>
|
||||
|
||||
<!-- Debug: Script-Status -->
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
@include('partials.theme-init-script')
|
||||
</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="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>
|
||||
<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">
|
||||
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
|
||||
<x-app-logo-icon class="me-2 h-7" />
|
||||
</span>
|
||||
{{ config('app.name', 'Laravel') }}
|
||||
</a>
|
||||
|
|
@ -29,15 +30,20 @@
|
|||
<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>
|
||||
<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 class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
{{ $slot }}
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
@include('partials.theme-toggle-script')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
25
resources/views/components/layouts/guest.blade.php
Normal 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>
|
||||
15
resources/views/components/theme-toggle.blade.php
Normal 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>
|
||||
|
||||
49
resources/views/components/wizard-progress.blade.php
Normal 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>
|
||||
|
||||
47
resources/views/emails/partner-invitation.blade.php
Normal 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>
|
||||
324
resources/views/livewire/admin/partners/invite.blade.php
Normal 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>
|
||||
|
||||
352
resources/views/livewire/admin/users.blade.php
Normal 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') {{ $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>
|
||||
478
resources/views/livewire/admin/users/permissions.blade.php
Normal 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>
|
||||
254
resources/views/livewire/partner/invitation-accept.blade.php
Normal 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>
|
||||
|
||||
549
resources/views/livewire/partner/setup-wizard.blade.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ new class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
|
|
@ -111,4 +111,4 @@ new class extends Component {
|
|||
|
||||
<livewire:settings.delete-user-form />
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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="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">
|
||||
{!! $content['title'] !!}
|
||||
</h1>
|
||||
|
||||
<blockquote class="text-large text-muted-foreground italic leading-relaxed border-l-4 border-secondary pl-6">
|
||||
{{ $content['quote'] }}
|
||||
{!! $content['quote'] !!}
|
||||
</blockquote>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -20,16 +20,18 @@
|
|||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="card-elevated rounded-3xl overflow-hidden">
|
||||
<img
|
||||
src="{{ asset('img/assets/' . $content['image']) }}"
|
||||
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
|
||||
<img src="{{ asset('img/assets/' . $content['image']) }}"
|
||||
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>
|
||||
|
||||
{{-- 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>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@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="card-elevated p-8 overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col slide-up delay-{{ $index * 200 }}">
|
||||
<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 h-full slide-up delay-{{ $index * 200 }}">
|
||||
<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">
|
||||
@if($feature['icon'])
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
{{-- Left Content --}}
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-6 slide-right delay-200">
|
||||
<h1 class="text-hero">
|
||||
<h1 class="text-hero-alt">
|
||||
{!! $content['title'] !!}
|
||||
</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'] !!}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -21,15 +21,16 @@
|
|||
</a>
|
||||
</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']))
|
||||
<div class="flex flex-wrap items-center gap-6 pt-10 mt-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">
|
||||
<div class="flex items-center gap-2 text-md text-muted-foreground font-light">
|
||||
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
|
||||
<span>{{ $stat }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -44,9 +45,8 @@
|
|||
{{-- 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-sm text-muted-foreground">{{ $content['card_title'] }}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div>
|
||||
<!-- Back Navigation -->
|
||||
<div class="pt-20 pb-4">
|
||||
<div class="container-narrow">
|
||||
<div class="pt-4 pb-4 border-b border-border/30 ">
|
||||
<div class="container-narrow ">
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
|
|
@ -10,13 +10,12 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Header -->
|
||||
<article class="pb-16">
|
||||
<article class="pb-16 pt-16">
|
||||
<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">
|
||||
{{ $article['title'] }}
|
||||
{!! $article['title'] !!}
|
||||
</h1>
|
||||
<p class="text-large text-muted-foreground mb-8 max-w-3xl mx-auto">
|
||||
{{ $article['subtitle'] }}
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
</header>
|
||||
|
||||
<!-- 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
|
||||
src="{{ asset('img/assets/' . $article['image']) }}"
|
||||
alt="{{ $article['title'] }}"
|
||||
|
|
@ -50,12 +49,12 @@
|
|||
<!-- Main Content -->
|
||||
<div class="md:col-span-3">
|
||||
<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'] }}
|
||||
</p>
|
||||
|
||||
@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">
|
||||
{{ $index + 1 }}. {{ $section['title'] }}
|
||||
</h2>
|
||||
|
|
@ -71,21 +70,25 @@
|
|||
<div class="md:col-span-1">
|
||||
<div class="sticky top-24">
|
||||
|
||||
<div class="card-elevated rounded-lg p-6">
|
||||
<h3 class="font-medium text-foreground mb-4">{{ $content['share_article'] }}</h3>
|
||||
<div class="card-elevated rounded-lg p-2 slide-left delay-400">
|
||||
<h3 class="font-medium text-foreground mb-4 text-center font-2xl py-2">{{ $content['share_article'] }}</h3>
|
||||
<div class="space-y-3">
|
||||
<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">
|
||||
<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>
|
||||
<span class="font-medium">LinkedIn</span>
|
||||
<span class="font-medium text-sm">LinkedIn</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<span class="font-medium text-sm">Facebook</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<div class="space-y-8">
|
||||
@foreach($this->posts as $post)
|
||||
<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="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]">
|
||||
<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">
|
||||
<a href="/magazin/{{ $post['id'] }}" class="stretched-link">
|
||||
{{ $post['title'] }}
|
||||
{!! $post['title'] !!}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,42 @@
|
|||
<section class="section-padding">
|
||||
<div class="container-padding text-center">
|
||||
<div class="text-center mb-16 slide-up delay-200">
|
||||
<h2 class="text-section-title text-foreground mb-12">
|
||||
{!! $content['title'] !!}
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@foreach($content['timeline'] as $index => $item)
|
||||
<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="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>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-foreground">{{ $item['title'] }}</h3>
|
||||
|
||||
<p class="text-muted-foreground text-sm leading-relaxed">
|
||||
{{ $item['description'] }}
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@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="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
|
||||
|
||||
|
||||
@if(isset($card['icon']))
|
||||
<div class="relative pt-12 pb-8">
|
||||
<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>
|
||||
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -21,35 +21,41 @@ function renderHeroIcon($iconName, $style = 'outline') {
|
|||
@endphp
|
||||
<section class="section-padding">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-section-title mb-6">
|
||||
{!! $content['title'] !!}
|
||||
</h2>
|
||||
<p class="text-large text-muted-foreground max-w-2xl mx-auto">
|
||||
{{ $content['subtitle'] }}
|
||||
<div class="text-center mb-16 slide-up delay-300">
|
||||
<h2 class="text-section-title">{!! $content['title'] !!}</h2>
|
||||
<p class="text-large text-muted-foreground mt-4 max-w-3xl mx-auto">
|
||||
{!! $content['subtitle'] !!}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@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="card-elevated p-8 rounded-3xl h-full hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="text-center space-y-6">
|
||||
<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">
|
||||
{!! renderHeroIcon($value['icon'], $value['icon_style'] ?? 'outline') !!}
|
||||
<div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">
|
||||
@if(isset($value['icon']))
|
||||
<div class="relative pt-12 pb-8">
|
||||
<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
|
||||
|
||||
<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">
|
||||
<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 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>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@
|
|||
@endforeach
|
||||
</div>
|
||||
@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>
|
||||
|
||||
|
||||
|
|
@ -39,15 +49,13 @@
|
|||
</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-center slide-left delay-400">
|
||||
<h3 class="text-xl font-semibold text-foreground">{{ $content['hub']['title'] }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ $content['hub']['subtitle'] }}</p>
|
||||
<div class="text-xl font-medium text-muted-foreground">{!! $content['hub']['title'] !!}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['hub']['subtitle'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding">
|
||||
<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 --}}
|
||||
<div class="relative">
|
||||
|
|
@ -24,6 +14,19 @@
|
|||
{{ $content['image_caption'] }}</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>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<!-- Left Side - Hero Text -->
|
||||
<div>
|
||||
<div class="slide-right delay-200">
|
||||
<h1 class="text-hero mb-6 tracking-wide">
|
||||
{!! $content['hero']['title'] ?? 'Send us a<br /><span class="text-secondary font-medium">message.</span>' !!}
|
||||
</h1>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Right Side - Contact Form -->
|
||||
<div class="card-elevated p-8">
|
||||
<div class="card-elevated p-8 slide-left delay-200">
|
||||
@if (session()->has('message'))
|
||||
<div class="mb-6 p-4 bg-gray-50 border border-secondary/20 rounded-lg text-secondary">
|
||||
{{ session('message') }}
|
||||
|
|
@ -153,8 +153,8 @@
|
|||
<section class="section-padding">
|
||||
<div class="container-padding">
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
@foreach($this->contactInfo as $info)
|
||||
<div class="card-elevated p-8 rounded-xl text-center">
|
||||
@foreach($this->contactInfo as $index => $info)
|
||||
<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">
|
||||
@if($info['icon'] === 'map-pin')
|
||||
<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">
|
||||
<div class="container-padding">
|
||||
<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">
|
||||
{!! $content['social_media']['title'] ?? 'Follow for<br /><span class="text-secondary font-medium">exclusives</span>' !!}
|
||||
</h2>
|
||||
|
|
@ -197,8 +197,8 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach($this->socialMedia as $social)
|
||||
<div class="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer">
|
||||
@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 slide-left delay-{{ $index * 200 }} cursor-pointer">
|
||||
<div>
|
||||
<h3 class="text-xl text-foreground">{{ $social['name'] }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ $social['handle'] }}</p>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? config('app.name') }}</title>
|
||||
|
||||
|
|
@ -8,9 +9,13 @@
|
|||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<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')
|
||||
|
||||
@livewireStyles
|
||||
@fluxAppearance
|
||||
|
|
|
|||
8
resources/views/partials/logo-head.blade.php
Normal 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>
|
||||