diff --git a/.devcontainer/Readme.md b/.devcontainer/Readme.md index 5306eaa..4d59a27 100644 --- a/.devcontainer/Readme.md +++ b/.devcontainer/Readme.md @@ -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 diff --git a/app/Http/Middleware/BasicAuthMiddleware.php b/app/Http/Middleware/BasicAuthMiddleware.php index 8e8a6da..5767e29 100644 --- a/app/Http/Middleware/BasicAuthMiddleware.php +++ b/app/Http/Middleware/BasicAuthMiddleware.php @@ -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'); diff --git a/app/Http/Middleware/EnsurePartnerSetupCompleted.php b/app/Http/Middleware/EnsurePartnerSetupCompleted.php new file mode 100644 index 0000000..2286bfc --- /dev/null +++ b/app/Http/Middleware/EnsurePartnerSetupCompleted.php @@ -0,0 +1,40 @@ +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'); + } +} diff --git a/app/Mail/PartnerInvitationMail.php b/app/Mail/PartnerInvitationMail.php new file mode 100644 index 0000000..9081776 --- /dev/null +++ b/app/Mail/PartnerInvitationMail.php @@ -0,0 +1,61 @@ +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 + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/Attribute.php b/app/Models/Attribute.php new file mode 100644 index 0000000..f1d50c3 --- /dev/null +++ b/app/Models/Attribute.php @@ -0,0 +1,10 @@ + 'boolean', + ]; + + /** + * Eine Brand gehört zu einem Partner (Manufacturer) + */ + public function partner(): BelongsTo + { + return $this->belongsTo(Partner::class); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..6a8a406 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,10 @@ +hasMany(HubLocation::class); + } + + /** + * Ein Hub hat viele Partner (Händler, Makler). + */ + public function partners(): HasMany + { + return $this->hasMany(Partner::class); + } +} diff --git a/app/Models/HubLocation.php b/app/Models/HubLocation.php new file mode 100644 index 0000000..1f2c57d --- /dev/null +++ b/app/Models/HubLocation.php @@ -0,0 +1,26 @@ +belongsTo(Hub::class); + } +} diff --git a/app/Models/Partner.php b/app/Models/Partner.php new file mode 100644 index 0000000..c3a0afa --- /dev/null +++ b/app/Models/Partner.php @@ -0,0 +1,58 @@ + '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); + // } +} diff --git a/app/Models/PartnerInvitation.php b/app/Models/PartnerInvitation.php new file mode 100644 index 0000000..64f59cb --- /dev/null +++ b/app/Models/PartnerInvitation.php @@ -0,0 +1,144 @@ + '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()); + } +} diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php new file mode 100644 index 0000000..ac6b298 --- /dev/null +++ b/app/Models/ProductVariant.php @@ -0,0 +1,10 @@ + */ 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(''); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..a67357c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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'); + } } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 9414478..879db7d 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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) { diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 9dc5183..d6f82d0 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -6,4 +6,5 @@ return [ App\Providers\ThemeServiceProvider::class, App\Providers\VoltServiceProvider::class, Barryvdh\Debugbar\ServiceProvider::class, + FluxPro\FluxProServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 0226437..afa3286 100644 --- a/composer.json +++ b/composer.json @@ -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" } - ] + } } diff --git a/composer.lock b/composer.lock index aeada30..2d3672f 100644 --- a/composer.lock +++ b/composer.lock @@ -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": [], diff --git a/config/content.php b/config/content.php index 47d79e7..62d278a 100644 --- a/config/content.php +++ b/config/content.php @@ -17,8 +17,8 @@ return [ ] ], 'hero' => [ - 'title' => 'Lokaler Handel trifft auf europäisches Design', - 'subtitle' => 'B2in (Brigdes 2 international) ist die zentrale B2B-Plattform, die kuratierte Möbel-Hersteller, lokale Fachexperten und kaufkräftige Kunden zum gegenseitigen Erfolg verbindet.', + 'title' => 'B2in – Brücken zwischen lokalen Kunden, lokalem Handel und inspirierenden Designs.', + '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 Property' ], 'ecosystem_core' => [ @@ -57,9 +57,9 @@ return [ 'vision_section' => [ 'title' => 'Gebaut auf Vertrauen', 'paragraphs' => [ - 'Unsere Mission ist es, eine Brücke zu bauen: 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.' + 'B2in (Bridges2international) verbindet Immobilienmakler, Möbelfachhändler, Möbelhersteller und Markenpartner auf einer gemeinsamen Plattform.', + 'Unser Ziel: 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.', + 'So entsteht ein Netzwerk, das Nähe schafft – 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 maximale Auswahl für den Kunden und maximaler Erfolg für unsere Partner.', ], - '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 Sie Partner
im führenden Möbel-Netzwerk', @@ -203,14 +203,16 @@ return [ ] ], 'about_hero' => [ - 'title' => 'Über B2in', - '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' => 'Über B2in: 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.

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 Property' + ], 'broker_section' => [ 'title' => 'Lifetime Vergütung für Makler', @@ -287,7 +289,7 @@ return [ 'image_alt' => 'Luxury interior design' ], 'ecosystem_hero' => [ - 'title' => 'B2in: Wie unser Ökosystem Wachstum für alle Partner generiert', + 'title' => 'Wie unser Ökosystem Wachstum für alle Partner 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 style2own und stileigentum 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 "Local First"-Logik werden die Angebote unserer lokalen Händler prominent platziert.', 'Gleichzeitig wird das Sortiment durch die exklusiven Produkte unserer europäischen Hersteller 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 jeder gewinnt', @@ -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 Werte', @@ -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' => ' Die Zukunft des Home Staging:
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' => 'Jenseits der Lage:
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' => 'Europäisches Design erobert den US-Markt:
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', diff --git a/config/livewire.php b/config/livewire.php index c9a4e6c..8a4be98 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -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... diff --git a/database/migrations/2025_11_05_164539_create_permission_tables.php b/database/migrations/2025_11_05_164539_create_permission_tables.php new file mode 100644 index 0000000..ce4d9d2 --- /dev/null +++ b/database/migrations/2025_11_05_164539_create_permission_tables.php @@ -0,0 +1,136 @@ +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']); + } +}; diff --git a/database/migrations/2025_11_06_115527_create_hubs_table.php b/database/migrations/2025_11_06_115527_create_hubs_table.php new file mode 100644 index 0000000..087655e --- /dev/null +++ b/database/migrations/2025_11_06_115527_create_hubs_table.php @@ -0,0 +1,26 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_115535_create_hub_locations_table.php b/database/migrations/2025_11_06_115535_create_hub_locations_table.php new file mode 100644 index 0000000..68d6b78 --- /dev/null +++ b/database/migrations/2025_11_06_115535_create_hub_locations_table.php @@ -0,0 +1,34 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_115640_create_partners_table.php b/database/migrations/2025_11_06_115640_create_partners_table.php new file mode 100644 index 0000000..a7832e2 --- /dev/null +++ b/database/migrations/2025_11_06_115640_create_partners_table.php @@ -0,0 +1,44 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_142927_add_color_to_roles_table.php b/database/migrations/2025_11_06_142927_add_color_to_roles_table.php new file mode 100644 index 0000000..8fb652a --- /dev/null +++ b/database/migrations/2025_11_06_142927_add_color_to_roles_table.php @@ -0,0 +1,28 @@ +string('color', 50)->default('zinc')->after('guard_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('color'); + }); + } +}; diff --git a/database/migrations/2025_11_06_151340_add_partner_id_to_users_table.php b/database/migrations/2025_11_06_151340_add_partner_id_to_users_table.php new file mode 100644 index 0000000..6c196a6 --- /dev/null +++ b/database/migrations/2025_11_06_151340_add_partner_id_to_users_table.php @@ -0,0 +1,40 @@ +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'); + }); + } + } +}; diff --git a/database/migrations/2025_11_06_152910_create_attributes_table.php b/database/migrations/2025_11_06_152910_create_attributes_table.php new file mode 100644 index 0000000..2337c8a --- /dev/null +++ b/database/migrations/2025_11_06_152910_create_attributes_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name'); // z.B. "Farbe", "Größe", "Material" + $table->string('slug')->unique(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('attributes'); + } +}; diff --git a/database/migrations/2025_11_06_152911_create_attribute_values_table.php b/database/migrations/2025_11_06_152911_create_attribute_values_table.php new file mode 100644 index 0000000..0f2e72a --- /dev/null +++ b/database/migrations/2025_11_06_152911_create_attribute_values_table.php @@ -0,0 +1,29 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_153100_create_media_table.php b/database/migrations/2025_11_06_153100_create_media_table.php new file mode 100644 index 0000000..aefe18f --- /dev/null +++ b/database/migrations/2025_11_06_153100_create_media_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_153241_create_brands_table.php b/database/migrations/2025_11_06_153241_create_brands_table.php new file mode 100644 index 0000000..4820a9f --- /dev/null +++ b/database/migrations/2025_11_06_153241_create_brands_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_153245_create_collections_table.php b/database/migrations/2025_11_06_153245_create_collections_table.php new file mode 100644 index 0000000..0719259 --- /dev/null +++ b/database/migrations/2025_11_06_153245_create_collections_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('collections'); + } +}; diff --git a/database/migrations/2025_11_06_153250_create_categories_table.php b/database/migrations/2025_11_06_153250_create_categories_table.php new file mode 100644 index 0000000..6745cb3 --- /dev/null +++ b/database/migrations/2025_11_06_153250_create_categories_table.php @@ -0,0 +1,39 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_153254_create_tax_rates_table.php b/database/migrations/2025_11_06_153254_create_tax_rates_table.php new file mode 100644 index 0000000..ea9cd79 --- /dev/null +++ b/database/migrations/2025_11_06_153254_create_tax_rates_table.php @@ -0,0 +1,30 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_153259_create_shipping_classes_table.php b/database/migrations/2025_11_06_153259_create_shipping_classes_table.php new file mode 100644 index 0000000..288e9f6 --- /dev/null +++ b/database/migrations/2025_11_06_153259_create_shipping_classes_table.php @@ -0,0 +1,29 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_153520_create_tags_table.php b/database/migrations/2025_11_06_153520_create_tags_table.php new file mode 100644 index 0000000..c1d4f01 --- /dev/null +++ b/database/migrations/2025_11_06_153520_create_tags_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tags'); + } +}; diff --git a/database/migrations/2025_11_06_154757_create_products_table.php b/database/migrations/2025_11_06_154757_create_products_table.php new file mode 100644 index 0000000..dce0b39 --- /dev/null +++ b/database/migrations/2025_11_06_154757_create_products_table.php @@ -0,0 +1,61 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_154835_create_product_variants_table.php b/database/migrations/2025_11_06_154835_create_product_variants_table.php new file mode 100644 index 0000000..b01b02b --- /dev/null +++ b/database/migrations/2025_11_06_154835_create_product_variants_table.php @@ -0,0 +1,62 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_154906_create_product_variant_attributes_table.php b/database/migrations/2025_11_06_154906_create_product_variant_attributes_table.php new file mode 100644 index 0000000..9581855 --- /dev/null +++ b/database/migrations/2025_11_06_154906_create_product_variant_attributes_table.php @@ -0,0 +1,30 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_155516_create_partner_invitations_table.php b/database/migrations/2025_11_06_155516_create_partner_invitations_table.php new file mode 100644 index 0000000..4f98a2b --- /dev/null +++ b/database/migrations/2025_11_06_155516_create_partner_invitations_table.php @@ -0,0 +1,40 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_155526_create_category_product_table.php b/database/migrations/2025_11_06_155526_create_category_product_table.php new file mode 100644 index 0000000..9322518 --- /dev/null +++ b/database/migrations/2025_11_06_155526_create_category_product_table.php @@ -0,0 +1,30 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_155530_create_product_tag_table.php b/database/migrations/2025_11_06_155530_create_product_tag_table.php new file mode 100644 index 0000000..dca40af --- /dev/null +++ b/database/migrations/2025_11_06_155530_create_product_tag_table.php @@ -0,0 +1,30 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_155534_create_related_products_table.php b/database/migrations/2025_11_06_155534_create_related_products_table.php new file mode 100644 index 0000000..4f59990 --- /dev/null +++ b/database/migrations/2025_11_06_155534_create_related_products_table.php @@ -0,0 +1,31 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_155852_create_product_logistics_table.php b/database/migrations/2025_11_06_155852_create_product_logistics_table.php new file mode 100644 index 0000000..94bcde6 --- /dev/null +++ b/database/migrations/2025_11_06_155852_create_product_logistics_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/database/migrations/2025_11_06_160618_add_display_name_to_roles_table.php b/database/migrations/2025_11_06_160618_add_display_name_to_roles_table.php new file mode 100644 index 0000000..0df8785 --- /dev/null +++ b/database/migrations/2025_11_06_160618_add_display_name_to_roles_table.php @@ -0,0 +1,29 @@ +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']); + }); + } +}; diff --git a/database/migrations/2025_11_06_162747_add_contact_name_to_partner_invitations_table.php b/database/migrations/2025_11_06_162747_add_contact_name_to_partner_invitations_table.php new file mode 100644 index 0000000..a03653c --- /dev/null +++ b/database/migrations/2025_11_06_162747_add_contact_name_to_partner_invitations_table.php @@ -0,0 +1,29 @@ +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']); + }); + } +}; diff --git a/database/migrations/2025_11_06_170546_add_can_be_invited_to_roles_table.php b/database/migrations/2025_11_06_170546_add_can_be_invited_to_roles_table.php new file mode 100644 index 0000000..51a332f --- /dev/null +++ b/database/migrations/2025_11_06_170546_add_can_be_invited_to_roles_table.php @@ -0,0 +1,28 @@ +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'); + }); + } +}; diff --git a/database/migrations/2025_11_06_170610_change_partner_type_to_role_id_in_partner_invitations.php b/database/migrations/2025_11_06_170610_change_partner_type_to_role_id_in_partner_invitations.php new file mode 100644 index 0000000..6515154 --- /dev/null +++ b/database/migrations/2025_11_06_170610_change_partner_type_to_role_id_in_partner_invitations.php @@ -0,0 +1,101 @@ +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(); + }); + } +}; diff --git a/database/migrations/2025_11_21_153912_add_setup_completed_to_partners_table.php b/database/migrations/2025_11_21_153912_add_setup_completed_to_partners_table.php new file mode 100644 index 0000000..b1e4916 --- /dev/null +++ b/database/migrations/2025_11_21_153912_add_setup_completed_to_partners_table.php @@ -0,0 +1,29 @@ +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']); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..0f83fd4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -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'), ]); } } diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php new file mode 100644 index 0000000..1581007 --- /dev/null +++ b/database/seeders/RoleSeeder.php @@ -0,0 +1,171 @@ +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 + ]); + } +} diff --git a/database/seeders/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php deleted file mode 100644 index e685872..0000000 --- a/database/seeders/RolesAndPermissionsSeeder.php +++ /dev/null @@ -1,76 +0,0 @@ - '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']);*/ - - } -} diff --git a/public/img/assets/b2in/about-hero.jpg b/public/img/assets/b2in/about-hero.jpg index 24112aa..51ea530 100644 Binary files a/public/img/assets/b2in/about-hero.jpg and b/public/img/assets/b2in/about-hero.jpg differ diff --git a/public/img/assets/b2in/best-of-two-world.jpg b/public/img/assets/b2in/best-of-two-world.jpg new file mode 100644 index 0000000..3b66cb2 Binary files /dev/null and b/public/img/assets/b2in/best-of-two-world.jpg differ diff --git a/public/img/assets/b2in/best-of-two-worlds.jpg b/public/img/assets/b2in/best-of-two-worlds.jpg new file mode 100644 index 0000000..8815b64 Binary files /dev/null and b/public/img/assets/b2in/best-of-two-worlds.jpg differ diff --git a/public/img/assets/b2in/ecosystem-hero.jpg b/public/img/assets/b2in/ecosystem-hero.jpg index 97dff1b..94c1e0c 100644 Binary files a/public/img/assets/b2in/ecosystem-hero.jpg and b/public/img/assets/b2in/ecosystem-hero.jpg differ diff --git a/public/img/assets/b2in/ecosystem_hub.jpg b/public/img/assets/b2in/ecosystem_hub.jpg new file mode 100644 index 0000000..738674e Binary files /dev/null and b/public/img/assets/b2in/ecosystem_hub.jpg differ diff --git a/public/img/assets/b2in/ecosystem_result.jpg b/public/img/assets/b2in/ecosystem_result.jpg new file mode 100644 index 0000000..4bf1aa9 Binary files /dev/null and b/public/img/assets/b2in/ecosystem_result.jpg differ diff --git a/public/img/assets/b2in/ecosystem_start.jpg b/public/img/assets/b2in/ecosystem_start.jpg new file mode 100644 index 0000000..bade792 Binary files /dev/null and b/public/img/assets/b2in/ecosystem_start.jpg differ diff --git a/resources/css/app.css b/resources/css/app.css index ad6eeed..368fbc5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; diff --git a/resources/css/portal.css b/resources/css/portal.css index d00d98a..511bcb8 100644 --- a/resources/css/portal.css +++ b/resources/css/portal.css @@ -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; } */ diff --git a/resources/css/web/shared-styles.css b/resources/css/web/shared-styles.css index 0430524..915c755 100644 --- a/resources/css/web/shared-styles.css +++ b/resources/css/web/shared-styles.css @@ -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; diff --git a/resources/css/web/theme-b2in.css b/resources/css/web/theme-b2in.css index 648157c..015c589 100644 --- a/resources/css/web/theme-b2in.css +++ b/resources/css/web/theme-b2in.css @@ -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), diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 3980892..2efab36 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -5,7 +5,44 @@
- + + + Customer + Date + Status + Amount + + + + + Lindsey Aminoff + Jul 29, 10:45 AM + Paid + $49.00 + + + + Hanna Lubin + Jul 28, 2:15 PM + Paid + $312.00 + + + + Kianna Bushevi + Jul 30, 4:05 PM + Refunded + $132.00 + + + + Gustavo Geidt + Jul 27, 9:30 AM + Paid + $31.00 + + +
@@ -16,3 +53,5 @@
+ + diff --git a/resources/views/components/app-logo-icon.blade.php b/resources/views/components/app-logo-icon.blade.php index 0adc3a2..d406889 100644 --- a/resources/views/components/app-logo-icon.blade.php +++ b/resources/views/components/app-logo-icon.blade.php @@ -1,8 +1,3 @@ - - - + +B2IN Logo + diff --git a/resources/views/components/app-logo.blade.php b/resources/views/components/app-logo.blade.php index 60f37d9..e43918f 100644 --- a/resources/views/components/app-logo.blade.php +++ b/resources/views/components/app-logo.blade.php @@ -1,6 +1,5 @@ -
- -
-
- B2IN +
+ B2IN Logo +
+ diff --git a/resources/views/components/error-alert.blade.php b/resources/views/components/error-alert.blade.php new file mode 100644 index 0000000..99b4085 --- /dev/null +++ b/resources/views/components/error-alert.blade.php @@ -0,0 +1,20 @@ +@props(['title' => null]) + +@if ($errors->any()) +
merge(['class' => 'rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4']) }}> +
+ +
+

+ {{ $title ?? __('Bitte korrigieren Sie folgende Fehler:') }} +

+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+
+
+@endif + diff --git a/resources/views/components/layouts/app/sidebar.blade.php b/resources/views/components/layouts/app/sidebar.blade.php index 1177ee2..7a7f42a 100644 --- a/resources/views/components/layouts/app/sidebar.blade.php +++ b/resources/views/components/layouts/app/sidebar.blade.php @@ -3,7 +3,7 @@ @include('partials.head') - + @@ -11,21 +11,33 @@ - + + + {{ __('Dashboard') }} + + {{ __('Dashboard') }} - + {{ __('Dashboard') }} + + {{ __('Dashboard') }} + + + + {{ __('Users') }} - {{ __('Users Table') }} - diff --git a/resources/views/components/layouts/auth.blade.php b/resources/views/components/layouts/auth.blade.php index 56d6cb6..85a28f9 100644 --- a/resources/views/components/layouts/auth.blade.php +++ b/resources/views/components/layouts/auth.blade.php @@ -1,3 +1,3 @@ - + {{ $slot }} - + diff --git a/resources/views/components/layouts/auth/card.blade.php b/resources/views/components/layouts/auth/card.blade.php index db94716..84ea103 100644 --- a/resources/views/components/layouts/auth/card.blade.php +++ b/resources/views/components/layouts/auth/card.blade.php @@ -2,25 +2,31 @@ @include('partials.head') + @include('partials.theme-init-script') - +
+ @livewireScripts @fluxScripts + @include('partials.theme-toggle-script') diff --git a/resources/views/components/layouts/auth/simple.blade.php b/resources/views/components/layouts/auth/simple.blade.php index dd25d11..91b7ccb 100644 --- a/resources/views/components/layouts/auth/simple.blade.php +++ b/resources/views/components/layouts/auth/simple.blade.php @@ -2,23 +2,30 @@ @include('partials.head') + @include('partials.theme-init-script') - + @livewireScripts @fluxScripts + @include('partials.theme-toggle-script') diff --git a/resources/views/components/layouts/auth/split.blade.php b/resources/views/components/layouts/auth/split.blade.php index 4e9788b..0627b6c 100644 --- a/resources/views/components/layouts/auth/split.blade.php +++ b/resources/views/components/layouts/auth/split.blade.php @@ -2,14 +2,15 @@ @include('partials.head') + @include('partials.theme-init-script') - + @fluxScripts + @include('partials.theme-toggle-script') diff --git a/resources/views/components/layouts/guest.blade.php b/resources/views/components/layouts/guest.blade.php new file mode 100644 index 0000000..c8e932e --- /dev/null +++ b/resources/views/components/layouts/guest.blade.php @@ -0,0 +1,25 @@ + + + + + @include('partials.head') + @include('partials.theme-init-script') + + + +
+ + {{ $slot }} + +
+ +
+ +
+ @livewireScripts + @fluxScripts + @include('partials.theme-toggle-script') + + + diff --git a/resources/views/components/theme-toggle.blade.php b/resources/views/components/theme-toggle.blade.php new file mode 100644 index 0000000..5cbac7c --- /dev/null +++ b/resources/views/components/theme-toggle.blade.php @@ -0,0 +1,15 @@ + + diff --git a/resources/views/components/wizard-progress.blade.php b/resources/views/components/wizard-progress.blade.php new file mode 100644 index 0000000..cbb7c28 --- /dev/null +++ b/resources/views/components/wizard-progress.blade.php @@ -0,0 +1,49 @@ +@props(['currentStep', 'totalSteps', 'steps' => []]) + +
+
+ @foreach($steps as $index => $step) + @php + $stepNumber = $index + 1; + $isActive = $stepNumber === $currentStep; + $isCompleted = $stepNumber < $currentStep; + @endphp + + {{-- Step Circle --}} +
+
+
+ @if($isCompleted) + + @else + + {{ $stepNumber }} + + @endif +
+
+ + {{-- Step Label --}} +
+

+ {{ $step }} +

+
+
+ + {{-- Connector Line --}} + @if(!$loop->last) +
+
+ @endif + @endforeach +
+
+ diff --git a/resources/views/emails/partner-invitation.blade.php b/resources/views/emails/partner-invitation.blade.php new file mode 100644 index 0000000..bb95f08 --- /dev/null +++ b/resources/views/emails/partner-invitation.blade.php @@ -0,0 +1,47 @@ + +# 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 + + +Registrierung abschließen + + + + **Wichtig:** Diese Einladung ist gültig bis zum {{ $expiresAt->format('d.m.Y H:i') }} Uhr. + + +Falls Sie Fragen haben, können Sie uns jederzeit kontaktieren. + +Mit freundlichen Grüßen, +{{ config('app.name') }} Team + +--- + + +Falls der Button nicht funktioniert, kopieren Sie bitte den folgenden Link in Ihren Browser: +{{ $invitationUrl }} + + diff --git a/resources/views/livewire/admin/partners/invite.blade.php b/resources/views/livewire/admin/partners/invite.blade.php new file mode 100644 index 0000000..47b4a97 --- /dev/null +++ b/resources/views/livewire/admin/partners/invite.blade.php @@ -0,0 +1,324 @@ +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(), + ]; + } +}; ?> + +
+ {{-- Header --}} +
+
+ {{ __('Partner einladen') }} + {{ __('Laden Sie neue Partner zu Ihrer Plattform ein') }} +
+
+ + {{-- Statistics --}} +
+ +
+
+ {{ __('Ausstehend') }} + {{ $pendingCount }} +
+
+ +
+
+
+ + +
+
+ {{ __('Akzeptiert') }} + {{ $acceptedCount }} +
+
+ +
+
+
+ + +
+
+ {{ __('Abgelaufen') }} + {{ $expiredCount }} +
+
+ +
+
+
+
+ +
+ {{-- Invitation Form --}} + +
+
+ {{ __('Neue Einladung senden') }} + {{ __('Füllen Sie die Felder aus, um einen neuen Partner einzuladen') }} +
+ + + + + {{ __('Firmenname') }} + + @error('companyName') {{ $message }} @enderror + + +
+ + {{ __('Vorname (optional)') }} + + @error('contactFirstName') {{ $message }} @enderror + + + + {{ __('Nachname (optional)') }} + + @error('contactLastName') {{ $message }} @enderror + +
+ + + + {{ __('Partner-Typ') }} + {{ __('Wählen Sie den Typ des Partners aus') }} + + @foreach($partnerRoles as $role) + + @if($role->icon) + icon }} class="mr-2" /> + @endif + {{ $role->display_name ?? $role->name }} + + @endforeach + + @error('roleId') {{ $message }} @enderror + + + + {{ __('E-Mail Adresse') }} + {{ __('E-Mail des Ansprechpartners') }} + + @error('email') {{ $message }} @enderror + + + + + {{-- Error Alert --}} + + +
+ + + {{ __('Einladung senden') }} + + + + {{ __('Wird gesendet...') }} + + +
+ +
+ + {{-- Recent Invitations --}} + +
+ {{ __('Letzte Einladungen') }} + {{ __('Übersicht der zuletzt versendeten Einladungen') }} +
+ + + +
+ @forelse($recentInvitations as $invitation) +
+
+
+
+ {{ $invitation->company_name }} + @if($invitation->contact_full_name) + • {{ $invitation->contact_full_name }} + @endif +
+
+ {{ $invitation->email }} +
+
+ + {{ ucfirst($invitation->status) }} + + + {{ $invitation->role?->display_name ?? $invitation->role?->name }} + +
+
+ {{ __('Eingeladen am:') }} {{ $invitation->created_at->format('d.m.Y H:i') }} + @if($invitation->status === 'pending') +
{{ __('Gültig bis:') }} {{ $invitation->expires_at->format('d.m.Y H:i') }} + @endif +
+
+
+
+ @empty +
+ +
{{ __('Noch keine Einladungen versendet') }}
+
+ @endforelse +
+
+
+ + {{-- Success Toast --}} + @if (session()->has('message')) + + {{ session('message') }} + + @endif +
+ diff --git a/resources/views/livewire/admin/users.blade.php b/resources/views/livewire/admin/users.blade.php new file mode 100644 index 0000000..ba03678 --- /dev/null +++ b/resources/views/livewire/admin/users.blade.php @@ -0,0 +1,352 @@ +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 = []; + } +}; ?> + +
+ {{-- Header --}} +
+
+ {{ __('Users Management') }} + {{ __('Manage users and their roles in your application') }} +
+
+ {{ __('Create User') }} +
+
+ + {{-- Statistics --}} +
+ +
+
+ {{ __('Total Users') }} + {{ $totalUsers }} +
+
+ +
+
+
+ + +
+
+ {{ __('Verified Users') }} + {{ $verifiedUsers }} +
+
+ +
+
+
+ + +
+
+ {{ __('Active Roles') }} + {{ $availableRoles->count() }} +
+
+ +
+
+
+
+ + {{-- Filters --}} + +
+ + + + {{ __('All Roles') }} + @foreach($availableRoles as $role) + {{ $role->display_name ?? $role->name }} + @endforeach + + + @if($search || $roleFilter) + + {{ __('Clear Filters') }} + + @endif +
+
+ + {{-- Users Table --}} + + + + +
+ {{ __('User') }} + @if($sortField === 'name') + + @endif +
+
+ +
+ {{ __('Email') }} + @if($sortField === 'email') + + @endif +
+
+ {{ __('Roles') }} + {{ __('Status') }} + {{ __('Actions') }} +
+ + + @forelse($users as $user) + + +
+
+ {{ $user->initials() }} +
+
+
{{ $user->name }}
+
+ {{ __('ID:') }} {{ $user->id }} +
+
+
+
+ + +
+ + {{ $user->email }} +
+
+ + +
+ +
+
+ + + @if($user->email_verified_at) + + {{ __('Verified') }} + + @else + + {{ __('Unverified') }} + + @endif + + + +
+ + +
+
+
+ @empty + + +
+ + {{ __('No users found') }} + + @if($search || $roleFilter) + {{ __('Try adjusting your filters.') }} + @else + {{ __('Get started by creating a new user.') }} + @endif + +
+
+
+ @endforelse +
+
+ + {{-- Pagination --}} + @if($users->hasPages()) +
+ {{ $users->links() }} +
+ @endif +
+ + {{-- Role Assignment Modal --}} + +
+
+ {{ __('Assign Roles') }} + + @if($selectedUser) + {{ __('Managing roles for') }} {{ $selectedUser->name }} + @endif + +
+ + + +
+ + {{ __('Roles') }} + {{ __('Select one or multiple roles for this user') }} + +
+ @foreach($availableRoles as $role) + + @endforeach +
+
+ + @if(!empty($selectedRoles)) +
+ {{ __('Selected Roles:') }} +
+ @foreach($selectedRoles as $roleName) + @php + $roleObj = $availableRoles->firstWhere('name', $roleName); + @endphp + + {{ $roleName }} + + @endforeach +
+
+ @endif +
+ + + +
+ + {{ __('Cancel') }} + + + {{ __('Save Roles') }} + +
+ +
+ + {{-- Success Message --}} + @if (session()->has('message')) + + {{ session('message') }} + + @endif +
diff --git a/resources/views/livewire/admin/users/permissions.blade.php b/resources/views/livewire/admin/users/permissions.blade.php new file mode 100644 index 0000000..fd66278 --- /dev/null +++ b/resources/views/livewire/admin/users/permissions.blade.php @@ -0,0 +1,478 @@ + 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']); + } +}; ?> + +
+ {{-- Header --}} +
+
+ {{ __('Permissions & Roles Management') }} + {{ __('Manage roles and permissions for your application') }} +
+
+ {{ __('Create Role') }} + {{ __('Create Permission') }} +
+
+ + {{-- Tabs --}} + + {{ __('Roles Overview') }} + {{ __('Permissions Overview') }} + + + {{-- Roles Tab Content --}} + @if($activeTab === 'roles') +
+ + + + {{ __('Role') }} + {{ __('Permissions') }} + {{ __('Count') }} + {{ __('Actions') }} + + + + @forelse($roles as $role) + + +
+ @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 +
+ @if($role->icon) + @svg('heroicon-o-'.$role->icon, 'w-5 h-5') + @else + @svg('heroicon-o-shield-check', 'w-5 h-5') + @endif + +
+
+
+ {{ $role->display_name ?? $role->name }} +
+
+ {{ $role->name }} • {{ __('Guard:') }} {{ $role->guard_name }} +
+
+
+
+ + +
+ @if($role->name === 'Super-Admin') + {{ __('All Permissions') }} + @elseif($role->permissions->isEmpty()) + {{ __('No permissions') }} + @else + @foreach($role->permissions->take(5) as $permission) + {{ $permission->name }} + @endforeach + @if($role->permissions->count() > 5) + + +{{ $role->permissions->count() - 5 }} {{ __('more') }} + + @endif + @endif +
+
+ + +
+ @if($role->name === 'Super-Admin') + ∞ + @else + {{ $role->permissions->count() }} + @endif +
+
+ + +
+ + +
+
+
+ @empty + + +
+ + {{ __('No roles found') }} + + {{ __('Get started by creating a new role.') }} + +
+
+
+ @endforelse +
+
+
+ + {{-- Role Statistics --}} +
+ +
+
+ {{ __('Total Roles') }} + {{ $roles->count() }} +
+
+ +
+
+
+ + +
+
+ {{ __('Total Permissions') }} + {{ $permissions->count() }} +
+
+ +
+
+
+ + +
+
+ {{ __('Avg. Permissions/Role') }} + + {{ $roles->count() > 0 ? number_format($roles->sum(fn($r) => $r->permissions->count()) / $roles->count(), 1) : 0 }} + +
+
+ +
+
+
+
+
+ @endif + + {{-- Permissions Tab Content --}} + @if($activeTab === 'permissions') +
+ + + + {{ __('Permission') }} + {{ __('Assigned to Roles') }} + {{ __('Role Count') }} + {{ __('Actions') }} + + + + @php + $groupedPermissions = $permissions->groupBy(function($permission) { + return explode(' ', $permission->name)[1] ?? 'other'; + }); + @endphp + + @forelse($groupedPermissions as $group => $groupPermissions) + {{-- Group Header --}} + + +
+ + + {{ ucfirst($group) }} {{ __('Permissions') }} ({{ $groupPermissions->count() }}) + +
+
+
+ + {{-- Group Permissions --}} + @foreach($groupPermissions as $permission) + + +
+
+ +
+
+
{{ $permission->name }}
+
+ {{ __('Guard:') }} {{ $permission->guard_name }} +
+
+
+
+ + +
+ @if($permission->roles->isEmpty()) + + {{ __('Not assigned') }} + + @else + @foreach($permission->roles as $role) + + {{ $role->name }} + + @endforeach + @endif +
+
+ + +
+ {{ $permission->roles->count() }} +
+
+ + +
+ + +
+
+
+ @endforeach + @empty + + +
+ + {{ __('No permissions found') }} + + {{ __('Get started by creating a new permission.') }} + +
+
+
+ @endforelse +
+
+
+
+ @endif + + {{-- Edit Role Modal --}} + +
+
+ {{ __('Edit Role') }} + + @if($selectedRole) + {{ __('Editing role') }}: {{ $selectedRole->name }} + @endif + +
+ + + +
+ + {{ __('Role Name') }} + + @error('roleName') {{ $message }} @enderror + + + + {{ __('Role Icon') }} + + @error('roleIcon') {{ $message }} @enderror + + + + {{ __('Role Display Name') }} + + @error('roleDisplayName') {{ $message }} @enderror + + + + {{ __('Color') }} + {{ __('Select a color for this role') }} + + {{ __('Red') }} + {{ __('Orange') }} + {{ __('Lime') }} + {{ __('Teal') }} + {{ __('Indigo') }} + {{ __('Purple') }} + {{ __('Pink') }} + {{ __('Accent (Cyan)') }} + {{ __('Yellow') }} + {{ __('Green') }} + {{ __('Gray') }} + + @error('roleColor') {{ $message }} @enderror + + + @if($roleColor) +
+ {{ __('Preview:') }} + + {{ $roleName ?: __('Role Name') }} + +
+ @endif + + + {{ __('Permissions') }} + {{ __('Select permissions for this role') }} + +
+ @php + $groupedPerms = $allPermissions->groupBy(function($permission) { + return explode(' ', $permission->name)[1] ?? 'other'; + }); + @endphp + + @foreach($groupedPerms as $group => $perms) +
+ {{ ucfirst($group) }} +
+ @foreach($perms as $permission) + + @endforeach +
+
+ @endforeach +
+
+ + @if(!empty($rolePermissions)) +
+ {{ __('Selected Permissions:') }} ({{ count($rolePermissions) }}) +
+ @foreach($rolePermissions as $permName) + + {{ $permName }} + + @endforeach +
+
+ @endif +
+ + + +
+ + {{ __('Cancel') }} + + + {{ __('Save Role') }} + +
+ +
+ + {{-- Success Message --}} + @if (session()->has('message')) + + {{ session('message') }} + + @endif +
diff --git a/resources/views/livewire/partner/invitation-accept.blade.php b/resources/views/livewire/partner/invitation-accept.blade.php new file mode 100644 index 0000000..59e9f3f --- /dev/null +++ b/resources/views/livewire/partner/invitation-accept.blade.php @@ -0,0 +1,254 @@ +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()); + } + } +}; ?> + +
+ {{-- Header --}} +
+ @include('partials.logo-head') +

+ {{ __('Willkommen, :company!', ['company' => $invitation->company_name]) }} +

+

+ {{ __('Erstellen Sie Ihr persönliches Konto, um Ihr Partner-Profil einzurichten.') }} +

+
+ + {{-- Card mit Formular --}} + +
+ {{-- Partner Info Badge --}} +
+ +
+
{{ __('Partner-Typ') }}
+
+ {{ $invitation->role?->display_name ?? $invitation->role?->name }} +
+
+
+ + + + {{-- Name Felder --}} +
+ + {{ __('Ihr Vorname') }} * + + @error('firstName') {{ $message }} @enderror + + + + {{ __('Ihr Nachname') }} * + + @error('lastName') {{ $message }} @enderror + +
+ + {{-- E-Mail (gesperrt) --}} + + {{ __('E-Mail-Adresse') }} + + {{ __('Diese E-Mail-Adresse wurde in der Einladung festgelegt und kann nicht geändert werden.') }} + @error('email') {{ $message }} @enderror + + + + + {{-- Passwort --}} +
+ + {{ __('Passwort festlegen') }} * + + @error('password') {{ $message }} @enderror + + + + {{ __('Passwort bestätigen') }} * + + @error('password_confirmation') {{ $message }} @enderror + +
+ + {{-- AGB Checkbox --}} + +
+ +
+ {{ __('Ich akzeptiere die') }} + {{ __('AGB') }} + {{ __('und') }} + {{ __('Datenschutzbestimmungen') }}. +
+
+ @error('acceptTerms') {{ $message }} @enderror +
+ + + + {{-- Error Alert --}} + + + {{-- Submit Button --}} +
+ + + {{ __('Konto erstellen & Setup starten') }} + + + + {{ __('Wird erstellt...') }} + + +
+ +
+ + {{-- Footer Hinweis --}} +
+

+ {{ __('Diese Einladung ist gültig bis zum') }} + @if($invitation->expires_at) + {{ $invitation->expires_at->format('d.m.Y H:i') }} {{ __('Uhr') }}. + @else + {{ __('unbegrenzt') }} {{ __('Uhr') }}. + @endif +

+
+
+ diff --git a/resources/views/livewire/partner/setup-wizard.blade.php b/resources/views/livewire/partner/setup-wizard.blade.php new file mode 100644 index 0000000..1b57b3b --- /dev/null +++ b/resources/views/livewire/partner/setup-wizard.blade.php @@ -0,0 +1,549 @@ +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); + } +}; ?> + +
+ {{-- Header --}} +
+ @include('partials.logo-head') +

+ {{ __('Vervollständigen Sie Ihr Profil') }} +

+
+ + {{-- Progress Indicator --}} + + + {{-- Wizard Content --}} + + {{-- Step 1: Stammdaten (für alle Rollen) --}} + @if ($currentStep === 1) +
+
+ +
+ @svg('heroicon-o-'.$roleIcon, 'w-5 h-5') + + @if ($partnerType === 'Retailer') + {{ __('Ihre Stammdaten') }} + @elseif($partnerType === 'Manufacturer') + {{ __('Ihre Stammdaten') }} + @else + {{ __('Ihr Profil') }} + @endif + / {{ $roleName }} +
+
+ + {{ __('Diese Informationen helfen uns, Ihr Profil zu vervollständigen.') }} + +
+ + + + + {{ __('Firmenname') }} * + + @error('companyName') + {{ $message }} + @enderror + + + + {{ __('Logo (optional)') }} + {{ __('Laden Sie Ihr Firmenlogo hoch (max. 2 MB, JPG/PNG)') }} + +
+ + + @error('logo') + {{ $message }} + @enderror + +
+ + {{ __('Wird hochgeladen...') }} +
+ + @if ($logo) +
+ @try +
+
+ Logo Preview +
+

{{ __('Logo erfolgreich hochgeladen') }}

+

{{ $logo->getClientOriginalName() }}

+
+
+
+ @catch(\Exception $e) +
+

{{ __('Fehler beim Laden der Vorschau') }}

+
+ @endtry +
+ @endif +
+
+ + + {{ __('Kurzbeschreibung') }} + + @error('description') + {{ $message }} + @enderror + + + + +
+ + {{ __('Straße') }} * + + @error('street') + {{ $message }} + @enderror + + + + {{ __('PLZ') }} * + + @error('zip') + {{ $message }} + @enderror + +
+ + + {{ __('Stadt') }} * + + @error('city') + {{ $message }} + @enderror + + + + {{ __('Website (optional)') }} + + @error('website') + {{ $message }} + @enderror + + + + + + +
+ + + {{ __('Logout') }} + + + @if ($partnerType === 'Retailer') + {{ __('Weiter zu Liefergebiete') }} + @elseif($partnerType === 'Manufacturer') + {{ __('Weiter zu Marke') }} + @else + {{ __('Setup abschließen') }} + @endif + +
+ + @endif + + {{-- Step 2: Retailer - Liefergebiete --}} + @if ($currentStep === 2 && $partnerType === 'Retailer') +
+
+ + 🚚 {{ __('Ihre Liefergebiete') }} + + + {{ __('Wie weit liefern und montieren Sie von Ihrer Adresse (:zip :city) aus?', ['zip' => $zip, 'city' => $city]) }} + +
+ + + + + {{ __('Lieferradius') }} * + {{ __('Ich liefere im Umkreis von ... km') }} +
+ + km +
+ @error('deliveryRadius') + {{ $message }} + @enderror +
+ + + {{ __('Montageradius') }} * + {{ __('Ich montiere im Umkreis von ... km') }} +
+ + km +
+ @error('assemblyRadius') + {{ $message }} + @enderror +
+ + + + + +
+ + {{ __('Setup abschließen') }} + +
+ + @endif + + {{-- Step 2: Manufacturer - Marke anlegen --}} + @if ($currentStep === 2 && $partnerType === 'Manufacturer') +
+
+ + ™️ {{ __('Ihre Marke') }} + + + {{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }} + +
+ + + + + {{ __('Markenname') }} * + + @error('brandName') + {{ $message }} + @enderror + + + + {{ __('Marken-Logo (optional)') }} + {{ __('Laden Sie Ihr Marken-Logo hoch (max. 2 MB, JPG/PNG)') }} + +
+ + + @error('brandLogo') + {{ $message }} + @enderror + +
+ + {{ __('Wird hochgeladen...') }} +
+ + @if ($brandLogo) +
+ @try +
+
+ Brand Logo Preview +
+

{{ __('Logo erfolgreich hochgeladen') }}

+

{{ $brandLogo->getClientOriginalName() }}

+
+
+
+ @catch(\Exception $e) +
+

{{ __('Fehler beim Laden der Vorschau') }}

+
+ @endtry +
+ @endif +
+
+ + + {{ __('Marken-Beschreibung') }} + + @error('brandDescription') + {{ $message }} + @enderror + + + + + + +
+ + {{ __('Setup abschließen') }} + +
+ + @endif + + {{-- Final Step: Fertig! --}} + @if ($currentStep === $totalSteps) +
+
+
+ +
+
+ +
+ + ✅ {{ __('Einrichtung abgeschlossen!') }} + + + @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 + +
+ + + +
+ @if ($partnerType !== 'Estate-Agent') + + {{ __('Erstes Produkt anlegen') }} + + @endif + + {{ __('Zum Dashboard') }} + +
+
+ @endif +
+
diff --git a/resources/views/livewire/portal/users.blade.php b/resources/views/livewire/portal/users.blade.php deleted file mode 100644 index 36702f1..0000000 --- a/resources/views/livewire/portal/users.blade.php +++ /dev/null @@ -1,189 +0,0 @@ -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), - // ]; - // } -}; ?> - -
- {{-- Filter und Suchleiste (Beispiel, FluxUI Klassen anpassen) --}} -
-
- - -
-
- - -
-
- - -
-
- - {{-- Tabelle (FluxUI Klassen anpassen!) --}} -
- - {{-- FluxUI Klasse für Thead --}} - - - - - - - - - - - {{-- FluxUI Klasse für Tbody --}} - @forelse ($this->users() as $user) - - - - - - - - - - @empty - - - - @endforelse - -
- Name @if($sortField === 'name'){{ $sortDirection === 'asc' ? '▲' : '▼' }}@endif - - E-Mail @if($sortField === 'email'){{ $sortDirection === 'asc' ? '▲' : '▼' }}@endif - - Status @if($sortField === 'status'){{ $sortDirection === 'asc' ? '▲' : '▼' }}@endif - - Rolle @if($sortField === 'role'){{ $sortDirection === 'asc' ? '▲' : '▼' }}@endif - - Gruppe - - Rechte - - Aktionen -
{{ $user->name }}{{ $user->email }} - - {{ ucfirst($user->status) }} - - - {{ $user->role }} {{-- Oder $user->role->name, wenn es eine Relation ist --}} - - {{ $user->group }} {{-- Oder $user->group->name --}} - - {{-- Darstellung der Rechte. Wenn es eine Many-to-Many Relation ist: --}} - {{-- @foreach($user->permissions as $permission) - - {{ $permission->name }} - - @endforeach --}} - {{-- Oder wenn es ein JSON-Feld ist, musst du es parsen und anzeigen --}} - Einfache Rechte-Anzeige (Todo) - - Bearbeiten {{-- FluxUI Button-Klassen --}} - {{-- FluxUI Button-Klassen --}} -
- Keine Benutzer gefunden. -
-
- -
- {{ $this->users()->links() }} {{-- Stellt sicher, dass die Paginierungs-Views für dein UI-Kit konfiguriert sind --}} -
-
\ No newline at end of file diff --git a/resources/views/livewire/portal/users/table.blade.php b/resources/views/livewire/portal/users/table.blade.php deleted file mode 100644 index 940a902..0000000 --- a/resources/views/livewire/portal/users/table.blade.php +++ /dev/null @@ -1,144 +0,0 @@ -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), - // ]; - // } -}; ?> - - - Customer - Date - Status - Amount - - - - @foreach ($this->users as $user) - - - - - {{ $user->name }} - - - {{ $user->email }} - - - RTest - - - {{ $user->role }} - - - - - - New post - - - - Name - Date - Popularity - - - - Draft - Published - Archived - - - Delete - - - - - @endforeach - - - diff --git a/resources/views/livewire/settings/profile.blade.php b/resources/views/livewire/settings/profile.blade.php index cb08833..7dc642f 100644 --- a/resources/views/livewire/settings/profile.blade.php +++ b/resources/views/livewire/settings/profile.blade.php @@ -69,46 +69,46 @@ new class extends Component { } }; ?> -
- @include('partials.settings-heading') +
+ @include('partials.settings-heading') - -
- + + + -
- +
+ - @if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail()) -
- - {{ __('Your email address is unverified.') }} + @if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail()) +
+ + {{ __('Your email address is unverified.') }} - - {{ __('Click here to re-send the verification email.') }} - - - - @if (session('status') === 'verification-link-sent') - - {{ __('A new verification link has been sent to your email address.') }} + + {{ __('Click here to re-send the verification email.') }} + - @endif -
- @endif -
-
-
- {{ __('Save') }} + @if (session('status') === 'verification-link-sent') + + {{ __('A new verification link has been sent to your email address.') }} + + @endif +
+ @endif
- - {{ __('Saved.') }} - -
- +
+
+ {{ __('Save') }} +
- - -
+ + {{ __('Saved.') }} + +
+ + + + + diff --git a/resources/views/livewire/web/components/sections/about-hero.blade.php b/resources/views/livewire/web/components/sections/about-hero.blade.php index 03668f5..bd14241 100644 --- a/resources/views/livewire/web/components/sections/about-hero.blade.php +++ b/resources/views/livewire/web/components/sections/about-hero.blade.php @@ -1,13 +1,13 @@ -
+
-
+

{!! $content['title'] !!}

- {{ $content['quote'] }} + {!! $content['quote'] !!}
@@ -20,17 +20,19 @@
-
- + {{ $content['image_alt'] }} -
-
-
{{ $content['year'] }}
-

{{ $content['year_text'] }}

+ class="w-full h-[600px] object-cover" /> +
+ + {{-- Floating info card --}} +
+
{{ $content['card_title'] }}
+
{!! $content['card_text'] !!}
+
diff --git a/resources/views/livewire/web/components/sections/digital-core.blade.php b/resources/views/livewire/web/components/sections/digital-core.blade.php index 50da5a1..9678ab6 100644 --- a/resources/views/livewire/web/components/sections/digital-core.blade.php +++ b/resources/views/livewire/web/components/sections/digital-core.blade.php @@ -11,8 +11,8 @@
@foreach($content['features'] as $index => $feature) -
-
+
+
@if($feature['icon']) diff --git a/resources/views/livewire/web/components/sections/hero.blade.php b/resources/views/livewire/web/components/sections/hero.blade.php index 01978c3..ef8e3f2 100644 --- a/resources/views/livewire/web/components/sections/hero.blade.php +++ b/resources/views/livewire/web/components/sections/hero.blade.php @@ -4,10 +4,10 @@ {{-- Left Content --}}
-

+

{!! $content['title'] !!}

-

+

{!! $content['subtitle'] !!}

@@ -21,15 +21,16 @@
-
- @foreach ($content['stats'] as $stat) -
- @svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary') - {{ $stat }} -
- @endforeach -
- + @if(isset($content['stats'])) +
+ @foreach ($content['stats'] as $stat) +
+ @svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary') + {{ $stat }} +
+ @endforeach +
+ @endif
@@ -43,11 +44,10 @@ {{-- Floating info card --}}
-
{{ $content['card_title'] }}
-
{!! $content['card_text'] !!} -
-
+ 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"> +
{{ $content['card_title'] }}
+
{!! $content['card_text'] !!}
+
diff --git a/resources/views/livewire/web/components/sections/magazin-detail.blade.php b/resources/views/livewire/web/components/sections/magazin-detail.blade.php index fde730a..45e8e26 100644 --- a/resources/views/livewire/web/components/sections/magazin-detail.blade.php +++ b/resources/views/livewire/web/components/sections/magazin-detail.blade.php @@ -1,7 +1,7 @@
-
-
+ - -
+
-
+

- {{ $article['title'] }} + {!! $article['title'] !!}

{{ $article['subtitle'] }} @@ -38,7 +37,7 @@

-
+
{{ $article['title'] }}
-

+

{{ $article['content']['intro'] }}

@foreach($article['content']['sections'] as $index => $section) -
+

{{ $index + 1 }}. {{ $section['title'] }}

@@ -71,21 +70,25 @@
-
-

{{ $content['share_article'] }}

+
+

{{ $content['share_article'] }}

diff --git a/resources/views/livewire/web/components/sections/magazin-list.blade.php b/resources/views/livewire/web/components/sections/magazin-list.blade.php index 91d4de0..45b8ee2 100644 --- a/resources/views/livewire/web/components/sections/magazin-list.blade.php +++ b/resources/views/livewire/web/components/sections/magazin-list.blade.php @@ -12,7 +12,7 @@
@foreach($this->posts as $post)
-
+
- {{ $post['title'] }} + {!! $post['title'] !!} diff --git a/resources/views/livewire/web/components/sections/our-story.blade.php b/resources/views/livewire/web/components/sections/our-story.blade.php index b6fb72f..fb16cf3 100644 --- a/resources/views/livewire/web/components/sections/our-story.blade.php +++ b/resources/views/livewire/web/components/sections/our-story.blade.php @@ -1,22 +1,42 @@
-

- {!! $content['title'] !!} -

+
+

+ {!! $content['title'] !!} +

+ +
- @foreach($content['timeline'] as $index => $item) + @foreach($content['timeline'] as $index => $card)
-
-
-
-
-
-

{{ $item['title'] }}

+
+ + + @if(isset($card['icon'])) +
+
+ @svg('heroicon-o-'.$card['icon'], 'w-10 h-10 text-secondary-foreground') +
+
+ @endif + +
+
+ @if (isset($card['logo'])) + {{ $card['title'] }} + @endif + @if(isset($card['title'])) +

{{ $card['title'] }}

+ @endif + @if(isset($card['description'])) +

+ {{ $card['description'] }} +

+ @endif +
-

- {{ $item['description'] }} -

diff --git a/resources/views/livewire/web/components/sections/our-values.blade.php b/resources/views/livewire/web/components/sections/our-values.blade.php index b45f94d..5d6ccc8 100644 --- a/resources/views/livewire/web/components/sections/our-values.blade.php +++ b/resources/views/livewire/web/components/sections/our-values.blade.php @@ -21,35 +21,41 @@ function renderHeroIcon($iconName, $style = 'outline') { @endphp
-
-

- {!! $content['title'] !!} -

-

- {{ $content['subtitle'] }} +

+

{!! $content['title'] !!}

+

+ {!! $content['subtitle'] !!}

@foreach($content['values'] as $index => $value)
-
-
-
- {!! renderHeroIcon($value['icon'], $value['icon_style'] ?? 'outline') !!} +
+ @if(isset($value['icon'])) +
+
+ @svg('heroicon-o-'.$value['icon'], 'w-10 h-10 text-secondary-foreground') +
+
+ @endif + +
+
+ @if (isset($value['logo'])) + {{ $value['title'] }} + @endif + @if(isset($value['title'])) +

{{ $value['title'] }}

+ @endif + @if(isset($value['description'])) +

+ {{ $value['description'] }} +

+ @endif +
- -

- {{ $value['title'] }} -

- -
- -

- {{ $value['description'] }} -

-
-
diff --git a/resources/views/livewire/web/components/sections/partner-hero.blade.php b/resources/views/livewire/web/components/sections/partner-hero.blade.php index fe61a1a..87d13aa 100644 --- a/resources/views/livewire/web/components/sections/partner-hero.blade.php +++ b/resources/views/livewire/web/components/sections/partner-hero.blade.php @@ -27,6 +27,16 @@ @endforeach
@endif + @if(isset($content['stats'])) +
+ @foreach ($content['stats'] as $stat) +
+ @svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary') + {{ $stat }} +
+ @endforeach +
+ @endif
@@ -39,15 +49,13 @@
{{-- Floating info card --}} +
-
-

{{ $content['hub']['title'] }}

-

{{ $content['hub']['subtitle'] }}

-
+ 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"> +
{!! $content['hub']['title'] !!}
+
{!! $content['hub']['subtitle'] !!}
-
diff --git a/resources/views/livewire/web/components/sections/vision-section.blade.php b/resources/views/livewire/web/components/sections/vision-section.blade.php index fc5b4b5..bd6a5ad 100644 --- a/resources/views/livewire/web/components/sections/vision-section.blade.php +++ b/resources/views/livewire/web/components/sections/vision-section.blade.php @@ -1,17 +1,7 @@
- {{-- Content --}} -
-
-

{{ $content['title'] }}

-
- @foreach ($content['paragraphs'] as $paragraph) -

{!! $paragraph !!}

- @endforeach -
-
-
+ {{-- Image --}}
@@ -24,6 +14,19 @@ {{ $content['image_caption'] }}
+ + {{-- Content --}} +
+
+

{{ $content['title'] }}

+
+ @foreach ($content['paragraphs'] as $paragraph) +

{!! $paragraph !!}

+ @endforeach +
+
+
+
diff --git a/resources/views/livewire/web/components/ui/contact-form.blade.php b/resources/views/livewire/web/components/ui/contact-form.blade.php index a347f83..3aa0619 100644 --- a/resources/views/livewire/web/components/ui/contact-form.blade.php +++ b/resources/views/livewire/web/components/ui/contact-form.blade.php @@ -4,7 +4,7 @@
-
+

{!! $content['hero']['title'] ?? 'Send us a
message.' !!}

@@ -14,7 +14,7 @@
-
+
@if (session()->has('message'))
{{ session('message') }} @@ -153,8 +153,8 @@
- @foreach($this->contactInfo as $info) -
+ @foreach($this->contactInfo as $index => $info) +
@if($info['icon'] === 'map-pin') @@ -187,7 +187,7 @@
-
+

{!! $content['social_media']['title'] ?? 'Follow for
exclusives' !!}

@@ -197,8 +197,8 @@
- @foreach($this->socialMedia as $social) -
+ @foreach($this->socialMedia as $index => $social) +

{{ $social['name'] }}

{{ $social['handle'] }}

diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index 14766b1..36d4f74 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,5 +1,6 @@ + {{ $title ?? config('app.name') }} @@ -8,9 +9,13 @@ - + + + @vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal') +@livewireStyles @fluxAppearance diff --git a/resources/views/partials/logo-head.blade.php b/resources/views/partials/logo-head.blade.php new file mode 100644 index 0000000..ed7dac2 --- /dev/null +++ b/resources/views/partials/logo-head.blade.php @@ -0,0 +1,8 @@ +
+ B2IN Logo + +
diff --git a/resources/views/partials/theme-init-script.blade.php b/resources/views/partials/theme-init-script.blade.php new file mode 100644 index 0000000..77f880a --- /dev/null +++ b/resources/views/partials/theme-init-script.blade.php @@ -0,0 +1,9 @@ + + + diff --git a/resources/views/partials/theme-toggle-script.blade.php b/resources/views/partials/theme-toggle-script.blade.php new file mode 100644 index 0000000..06c2f5c --- /dev/null +++ b/resources/views/partials/theme-toggle-script.blade.php @@ -0,0 +1,38 @@ + + diff --git a/resources/views/partner/invitation-expired.blade.php b/resources/views/partner/invitation-expired.blade.php new file mode 100644 index 0000000..3fe2733 --- /dev/null +++ b/resources/views/partner/invitation-expired.blade.php @@ -0,0 +1,41 @@ + +
+
+
+ @include('partials.logo-head') +
+ + +
+ + + + {{ __('Einladung abgelaufen') }} + + + + {{ __('Diese Einladung ist am :date abgelaufen.', ['date' => $invitation->expires_at->format('d.m.Y H:i')]) }} + + +
+

+ {{ __('Firma') }}: {{ $invitation->company_name }}
+ {{ __('E-Mail') }}: {{ $invitation->email }} +

+
+ + + +

+ {{ __('Bitte kontaktieren Sie Ihren Ansprechpartner, um eine neue Einladung zu erhalten.') }} +

+ + + {{ __('Support kontaktieren') }} + +
+
+
+
+
+ diff --git a/resources/views/partner/invitation-used.blade.php b/resources/views/partner/invitation-used.blade.php new file mode 100644 index 0000000..c8019d6 --- /dev/null +++ b/resources/views/partner/invitation-used.blade.php @@ -0,0 +1,38 @@ + +
+
+ @include('partials.logo-head') + +
+ + + + {{ __('Einladung bereits verwendet') }} + + + + {{ __('Diese Einladung wurde bereits am :date akzeptiert.', ['date' => $invitation->accepted_at?->format('d.m.Y H:i') ?? '']) }} + + +
+

+ {{ __('Firma') }}: {{ $invitation->company_name }}
+ {{ __('E-Mail') }}: {{ $invitation->email }} +

+
+ + + +

+ {{ __('Sie können sich mit Ihren Zugangsdaten anmelden.') }} +

+ + + {{ __('Zur Anmeldung') }} + +
+
+
+
+
+ diff --git a/resources/views/web/about.blade.php b/resources/views/web/about.blade.php index d488f91..99edd7a 100644 --- a/resources/views/web/about.blade.php +++ b/resources/views/web/about.blade.php @@ -6,7 +6,7 @@
-
+
diff --git a/resources/views/web/contact.blade.php b/resources/views/web/contact.blade.php index 88ac352..bbd315f 100644 --- a/resources/views/web/contact.blade.php +++ b/resources/views/web/contact.blade.php @@ -6,7 +6,7 @@
-
+
diff --git a/resources/views/web/home.blade.php b/resources/views/web/home.blade.php index 246920e..6ab1811 100644 --- a/resources/views/web/home.blade.php +++ b/resources/views/web/home.blade.php @@ -8,8 +8,9 @@
- - + + + diff --git a/resources/views/web/magazin-detail.blade.php b/resources/views/web/magazin-detail.blade.php index 97a7a20..2308f4d 100644 --- a/resources/views/web/magazin-detail.blade.php +++ b/resources/views/web/magazin-detail.blade.php @@ -6,7 +6,7 @@
-
+
diff --git a/resources/views/web/magazin.blade.php b/resources/views/web/magazin.blade.php index 58dec7f..a971701 100644 --- a/resources/views/web/magazin.blade.php +++ b/resources/views/web/magazin.blade.php @@ -6,7 +6,7 @@
-
+
diff --git a/routes/admin.php b/routes/admin.php index 5d9b361..2032253 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -10,10 +10,16 @@ Route::get('/', function () { return redirect()->route('dashboard'); })->name('home'); -Route::view('dashboard', 'admin.dashboard')->middleware(['auth', 'verified'])->name('dashboard'); +// Partner Setup Wizard (muss vor anderen auth-Routes sein, um keine Middleware zu bekommen) +Route::middleware(['auth'])->group(function () { + Volt::route('partner/setup', 'partner.setup-wizard') + ->name('partner.setup.wizard'); +}); + +Route::view('dashboard', 'admin.dashboard')->middleware(['auth', 'verified', 'partner.setup'])->name('dashboard'); // Admin-Einstellungen -Route::middleware(['auth'])->group(function () { +Route::middleware(['auth', 'partner.setup'])->group(function () { Route::redirect('settings', 'settings/profile'); Volt::route('settings/profile', 'settings.profile')->name('settings.profile'); @@ -22,8 +28,13 @@ Route::middleware(['auth'])->group(function () { // Weitere Admin-Routen hier... + // User Management Volt::route('admin/users', 'admin.users')->name('admin.users'); Volt::route('admin/users/table', 'admin.users.table')->name('admin.users.table'); + Volt::route('admin/users/permissions', 'admin.users.permissions')->name('admin.users.permissions'); + + // Partner Management + Volt::route('admin/partners/invite', 'admin.partners.invite')->name('admin.partners.invite'); }); // Admin-Authentication wird bereits in domains.php geladen diff --git a/routes/auth.php b/routes/auth.php index 55c7f4f..02f72e9 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -7,32 +7,32 @@ use Livewire\Volt\Volt; Route::group(['middleware' => config('fortify.middleware', ['web'])], function () { // Login mit Livewire Volt::route('/login', 'auth.login') - ->middleware(['guest:'.config('fortify.guard')]) + ->middleware(['guest:' . config('fortify.guard')]) ->name('login'); // Registrierung mit Livewire Volt::route('/register', 'auth.register') - ->middleware(['guest:'.config('fortify.guard')]) + ->middleware(['guest:' . config('fortify.guard')]) ->name('register'); // Passwort vergessen mit Livewire Volt::route('/forgot-password', 'auth.forgot-password') - ->middleware(['guest:'.config('fortify.guard')]) + ->middleware(['guest:' . config('fortify.guard')]) ->name('password.request'); // Passwort zurücksetzen mit Livewire Volt::route('/reset-password/{token}', 'auth.reset-password') - ->middleware(['guest:'.config('fortify.guard')]) + ->middleware(['guest:' . config('fortify.guard')]) ->name('password.reset'); // E-Mail-Verifizierung mit Livewire Volt::route('/verify-email', 'auth.verify-email') - ->middleware(['auth:'.config('fortify.guard')]) + ->middleware(['auth:' . config('fortify.guard')]) ->name('verification.notice'); // Passwort bestätigen mit Livewire Volt::route('/confirm-password', 'auth.confirm-password') - ->middleware(['auth:'.config('fortify.guard')]) + ->middleware(['auth:' . config('fortify.guard')]) ->name('password.confirm'); // Logout-Route diff --git a/routes/web.php b/routes/web.php index 4ebe592..840848c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,7 @@ name('partner.invitation.accept'); + +Route::get('/partner/invitation/expired/{token}', function (string $token) { + $invitation = \App\Models\PartnerInvitation::with('role')->where('token', $token)->firstOrFail(); + return view('partner.invitation-expired', compact('invitation')); +})->name('partner.invitation.expired'); + +Route::get('/partner/invitation/used/{token}', function (string $token) { + $invitation = \App\Models\PartnerInvitation::with('role')->where('token', $token)->firstOrFail(); + return view('partner.invitation-used', compact('invitation')); +})->name('partner.invitation.used'); + +// Partner Setup Wizard +Route::middleware('auth')->group(function () { + Volt::route('/partner/setup', 'partner.setup-wizard') + ->name('partner.setup.wizard'); +}); diff --git a/tailwind.portal.config.js b/tailwind.portal.config.js index c926989..34c5c45 100644 --- a/tailwind.portal.config.js +++ b/tailwind.portal.config.js @@ -13,45 +13,46 @@ module.exports = { theme: { extend: { fontFamily: { - sans: ["Instrument Sans", ...defaultTheme.fontFamily.sans], + sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans], + }, colors: { accent: { - 50: "rgb(var(--color-accent-50) / )", - 100: "rgb(var(--color-accent-100) / )", - 200: "rgb(var(--color-accent-200) / )", - 300: "rgb(var(--color-accent-300) / )", - 400: "rgb(var(--color-accent-400) / )", - 500: "rgb(var(--color-accent-500) / )", - 600: "rgb(var(--color-accent-600) / )", - 700: "rgb(var(--color-accent-700) / )", - 800: "rgb(var(--color-accent-800) / )", - 900: "rgb(var(--color-accent-900) / )", - 950: "rgb(var(--color-accent-950) / )", - DEFAULT: "rgb(var(--color-accent-600) / )", + 50: "hsl(var(--color-accent-50) / )", + 100: "hsl(var(--color-accent-100) / )", + 200: "hsl(var(--color-accent-200) / )", + 300: "hsl(var(--color-accent-300) / )", + 400: "hsl(var(--color-accent-400) / )", + 500: "hsl(var(--color-accent-500) / )", + 600: "hsl(var(--color-accent-600) / )", + 700: "hsl(var(--color-accent-700) / )", + 800: "hsl(var(--color-accent-800) / )", + 900: "hsl(var(--color-accent-900) / )", + 950: "hsl(var(--color-accent-950) / )", + DEFAULT: "hsl(var(--color-accent-600) / )", }, }, ringColor: { - accent: "rgb(var(--color-accent-500) / )", + accent: "hsl(var(--color-accent-500) / )", }, ringOffsetColor: { - accent: "rgb(var(--color-accent-100) / )", + accent: "hsl(var(--color-accent-100) / )", }, backgroundColor: { accent: { - DEFAULT: "rgb(var(--color-accent-600) / )", + DEFAULT: "hsl(var(--color-accent-600) / )", foreground: "var(--color-white)", }, }, textColor: { accent: { - DEFAULT: "rgb(var(--color-accent-600) / )", + DEFAULT: "hsl(var(--color-accent-600) / )", foreground: "var(--color-white)", }, }, borderColor: { accent: { - DEFAULT: "rgb(var(--color-accent-600) / )", + DEFAULT: "hsl(var(--color-accent-600) / )", }, }, },