23-01-2026
This commit is contained in:
parent
07959c0ba2
commit
854ce02bf6
166 changed files with 32909 additions and 1262 deletions
|
|
@ -10,6 +10,15 @@ class ThemeHelper
|
|||
public static function getLogoPath(string $type = 'positive'): string
|
||||
{
|
||||
$theme = config('app.theme', 'b2in');
|
||||
return self::getLogoPathForBrand($theme, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the logo path for a specific brand
|
||||
*/
|
||||
public static function getLogoPathForBrand(?string $brand = null, string $type = 'positive'): string
|
||||
{
|
||||
$brand = $brand ?? config('app.theme', 'b2in');
|
||||
|
||||
$logoMap = [
|
||||
'b2in' => [
|
||||
|
|
@ -30,7 +39,7 @@ class ThemeHelper
|
|||
]
|
||||
];
|
||||
|
||||
return $logoMap[$theme][$type] ?? $logoMap['b2in'][$type];
|
||||
return $logoMap[$brand][$type] ?? $logoMap['b2in'][$type];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -110,4 +119,45 @@ class ThemeHelper
|
|||
$config = self::getDomainConfig();
|
||||
return $config['url'] ?? config('app.url');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brand colors for a specific brand
|
||||
*/
|
||||
public static function getBrandColors(?string $brand = null): array
|
||||
{
|
||||
$brand = $brand ?? config('app.theme', 'b2in');
|
||||
$config = config("domains.domains.{$brand}", []);
|
||||
|
||||
return [
|
||||
'primary' => $config['color_scheme']['primary'] ?? '#2b3f51',
|
||||
'secondary' => $config['color_scheme']['secondary'] ?? '#20a0da',
|
||||
'accent' => $config['color_scheme']['accent'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brand name for a specific brand
|
||||
*/
|
||||
public static function getBrandName(?string $brand = null): string
|
||||
{
|
||||
$brand = $brand ?? config('app.theme', 'b2in');
|
||||
|
||||
$brandNames = [
|
||||
'b2in' => 'B2IN',
|
||||
'b2a' => 'B2A',
|
||||
'stileigentum' => 'StilEigentum',
|
||||
'style2own' => 'Style2Own',
|
||||
];
|
||||
|
||||
return $brandNames[$brand] ?? strtoupper($brand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all brand configuration for a specific brand
|
||||
*/
|
||||
public static function getBrandConfig(?string $brand = null): array
|
||||
{
|
||||
$brand = $brand ?? config('app.theme', 'b2in');
|
||||
return config("domains.domains.{$brand}", []);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
app/Http/Controllers/Api/DisplayConfigController.php
Normal file
43
app/Http/Controllers/Api/DisplayConfigController.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DisplayVideo;
|
||||
use App\Models\DisplayFooterContent;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DisplayConfigController extends Controller
|
||||
{
|
||||
/**
|
||||
* Gibt die Konfiguration für die Display-Seite zurück
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$videos = DisplayVideo::active()->get()->map(function ($video) {
|
||||
return [
|
||||
'src' => $video->full_path,
|
||||
'position' => $video->position,
|
||||
];
|
||||
});
|
||||
|
||||
$footerContent = DisplayFooterContent::active()->get()->map(function ($footer) {
|
||||
$data = [
|
||||
'headline' => $footer->headline,
|
||||
'subline' => $footer->subline,
|
||||
];
|
||||
|
||||
// URL nur hinzufügen wenn vorhanden
|
||||
if ($footer->url) {
|
||||
$data['url'] = $footer->short_url;
|
||||
}
|
||||
|
||||
return $data;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'videoPlaylist' => $videos,
|
||||
'footerContent' => $footerContent,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,16 @@ class BasicAuthMiddleware
|
|||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip Basic Auth für Flux UI Assets (flux.js, flux.min.js, editor.js, etc.)
|
||||
if (str_starts_with($path, 'flux/')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip Basic Auth für Display-API und Short-Links (öffentlicher Zugriff für Display-Seite)
|
||||
if ($request->is('api/display/*') || $request->is('_cabinet/*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Credentials from .env file
|
||||
$user = config('auth.basic.user');
|
||||
$pass = config('auth.basic.password');
|
||||
|
|
|
|||
59
app/Http/Middleware/SetDomainUrl.php
Normal file
59
app/Http/Middleware/SetDomainUrl.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Middleware um die URL-Konfiguration basierend auf der aktuellen Domain zu setzen.
|
||||
*
|
||||
* Diese Middleware muss sehr früh im Request-Lifecycle ausgeführt werden,
|
||||
* um sicherzustellen, dass url() und asset() die richtige Domain verwenden.
|
||||
*/
|
||||
class SetDomainUrl
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$host = $request->getHost();
|
||||
|
||||
// Suche nach der Domain-Konfiguration
|
||||
$domainConfig = null;
|
||||
$domains = config('domains.domains', []);
|
||||
|
||||
foreach ($domains as $name => $config) {
|
||||
if (is_array($config) && isset($config['domain_name']) && $config['domain_name'] === $host) {
|
||||
$domainConfig = $config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn eine Domain-Konfiguration gefunden wurde, setze die URL
|
||||
if ($domainConfig && isset($domainConfig['url'])) {
|
||||
$domainUrl = $domainConfig['url'];
|
||||
|
||||
// URL-Generator konfigurieren
|
||||
URL::forceRootUrl($domainUrl);
|
||||
URL::forceScheme(parse_url($domainUrl, PHP_URL_SCHEME) ?: 'https');
|
||||
|
||||
// Asset-Root setzen
|
||||
/** @var UrlGenerator $urlGenerator */
|
||||
$urlGenerator = app('url');
|
||||
$urlGenerator->useAssetOrigin($domainUrl);
|
||||
|
||||
// Config aktualisieren
|
||||
config([
|
||||
'app.url' => $domainUrl,
|
||||
'app.asset_url' => $domainUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,13 +19,13 @@ class ThemeMiddleware
|
|||
$path = $request->path();
|
||||
|
||||
// Theme-Switching über Subdomains
|
||||
if (str_contains($host, 'b2in.test')) {
|
||||
if (str_contains($host, 'b2in')) {
|
||||
config(['app.theme' => 'b2in']);
|
||||
} elseif (str_contains($host, 'b2a.test')) {
|
||||
} elseif (str_contains($host, 'b2a') || str_contains($host, 'bridges2america')) {
|
||||
config(['app.theme' => 'b2a']);
|
||||
} elseif (str_contains($host, 'stileigentum.test')) {
|
||||
} elseif (str_contains($host, 'stileigentum')) {
|
||||
config(['app.theme' => 'stileigentum']);
|
||||
} elseif (str_contains($host, 'style2own.test')) {
|
||||
} elseif (str_contains($host, 'style2own')) {
|
||||
config(['app.theme' => 'style2own']);
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class ThemeMiddleware
|
|||
if (str_starts_with($path, 'b2in/')) {
|
||||
config(['app.theme' => 'b2in']);
|
||||
$request->server->set('REQUEST_URI', '/' . substr($path, 5)); // Entferne 'b2in/' vom Pfad
|
||||
} elseif (str_starts_with($path, 'b2a/')) {
|
||||
} elseif (str_starts_with($path, 'b2a/') || str_starts_with($path, 'bridges2america/')) {
|
||||
config(['app.theme' => 'b2a']);
|
||||
$request->server->set('REQUEST_URI', '/' . substr($path, 4)); // Entferne 'b2a/' vom Pfad
|
||||
} elseif (str_starts_with($path, 'stileigentum/')) {
|
||||
|
|
|
|||
292
app/Livewire/Admin/CMS/CabinetDisplay.php
Normal file
292
app/Livewire/Admin/CMS/CabinetDisplay.php
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\CMS;
|
||||
|
||||
use App\Models\DisplayVideo;
|
||||
use App\Models\DisplayFooterContent;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\On;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class CabinetDisplay extends Component
|
||||
{
|
||||
// Video-Verwaltung
|
||||
public $videoId = null;
|
||||
public $videoFilename = '';
|
||||
public $videoTitle = '';
|
||||
public $videoPosition = 25;
|
||||
public $videoIsActive = true;
|
||||
public $showVideoModal = false;
|
||||
public $availableVideos = [];
|
||||
|
||||
// Footer-Content-Verwaltung
|
||||
public $footerId = null;
|
||||
public $footerHeadline = '';
|
||||
public $footerSubline = '';
|
||||
public $footerUrl = '';
|
||||
public $footerIsActive = true;
|
||||
public $showFooterModal = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner
|
||||
*/
|
||||
public function loadAvailableVideos()
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$files = File::files($assetsPath);
|
||||
$this->availableVideos = collect($files)
|
||||
->map(fn($file) => $file->getFilename())
|
||||
->filter(fn($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VIDEO-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openVideoModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$this->videoId = $video->id;
|
||||
$this->videoFilename = $video->filename;
|
||||
$this->videoTitle = $video->title ?? '';
|
||||
$this->videoPosition = $video->position;
|
||||
$this->videoIsActive = $video->is_active;
|
||||
} else {
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
$this->showVideoModal = true;
|
||||
}
|
||||
|
||||
public function saveVideo()
|
||||
{
|
||||
$this->validate([
|
||||
'videoFilename' => 'required|string',
|
||||
'videoPosition' => 'required|integer|min:0|max:100',
|
||||
], [
|
||||
'videoFilename.required' => 'Bitte wählen Sie ein Video aus.',
|
||||
'videoPosition.required' => 'Die Position ist erforderlich.',
|
||||
'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
'is_active' => $this->videoIsActive,
|
||||
];
|
||||
|
||||
if ($this->videoId) {
|
||||
$video = DisplayVideo::findOrFail($this->videoId);
|
||||
$video->update($data);
|
||||
session()->flash('success', 'Video erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayVideo::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
DisplayVideo::create($data);
|
||||
session()->flash('success', 'Video erfolgreich hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeVideoModal();
|
||||
}
|
||||
|
||||
public function deleteVideo($id)
|
||||
{
|
||||
DisplayVideo::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Video erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleVideoStatus($id)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$video->update(['is_active' => !$video->is_active]);
|
||||
}
|
||||
|
||||
public function moveVideo($id, $direction)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$currentOrder = $video->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetVideoForm()
|
||||
{
|
||||
$this->videoId = null;
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
public function closeVideoModal()
|
||||
{
|
||||
$this->showVideoModal = false;
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOOTER-CONTENT-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openFooterModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$this->footerId = $footer->id;
|
||||
$this->footerHeadline = $footer->headline;
|
||||
$this->footerSubline = $footer->subline;
|
||||
$this->footerUrl = $footer->url;
|
||||
$this->footerIsActive = $footer->is_active;
|
||||
} else {
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
$this->showFooterModal = true;
|
||||
}
|
||||
|
||||
public function saveFooter()
|
||||
{
|
||||
$this->validate([
|
||||
'footerHeadline' => 'required|string|max:255',
|
||||
'footerSubline' => 'required|string|max:255',
|
||||
'footerUrl' => 'nullable|url',
|
||||
], [
|
||||
'footerHeadline.required' => 'Die Überschrift ist erforderlich.',
|
||||
'footerSubline.required' => 'Die Unterzeile ist erforderlich.',
|
||||
'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
'is_active' => $this->footerIsActive,
|
||||
];
|
||||
|
||||
if ($this->footerId) {
|
||||
$footer = DisplayFooterContent::findOrFail($this->footerId);
|
||||
$footer->update($data);
|
||||
|
||||
// Short-Code generieren falls URL vorhanden aber noch kein Short-Code
|
||||
if ($footer->url && !$footer->short_code) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
|
||||
$footer = DisplayFooterContent::create($data);
|
||||
|
||||
// Short-Code nur generieren wenn URL vorhanden
|
||||
if ($footer->url) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: ' . $footer->short_url);
|
||||
} else {
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)');
|
||||
}
|
||||
}
|
||||
|
||||
$this->closeFooterModal();
|
||||
}
|
||||
|
||||
public function regenerateShortCode($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Short-Code wurde neu generiert!');
|
||||
}
|
||||
|
||||
public function resetClicks($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->clicks = 0;
|
||||
$footer->save();
|
||||
session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function deleteFooter($id)
|
||||
{
|
||||
DisplayFooterContent::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleFooterStatus($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->update(['is_active' => !$footer->is_active]);
|
||||
}
|
||||
|
||||
public function moveFooter($id, $direction)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$currentOrder = $footer->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetFooterForm()
|
||||
{
|
||||
$this->footerId = null;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
public function closeFooterModal()
|
||||
{
|
||||
$this->showFooterModal = false;
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$videos = DisplayVideo::orderBy('sort_order')->get();
|
||||
$footerContents = DisplayFooterContent::orderBy('sort_order')->get();
|
||||
|
||||
return view('livewire.admin.c-m-s.cabinet-display', [
|
||||
'videos' => $videos,
|
||||
'footerContents' => $footerContents,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,18 @@ class Header extends Component
|
|||
$this->domainName = \App\Helpers\ThemeHelper::getDomainName();
|
||||
$this->domainUrl = \App\Helpers\ThemeHelper::getDomainUrl();
|
||||
$theme = config('app.theme', 'b2in');
|
||||
$this->content = config("content.themes.{$theme}.header", []);
|
||||
$this->content = config("content.themes.{$theme}.header", [
|
||||
'portal_login' => 'Portal Login',
|
||||
'navigation' => []
|
||||
]);
|
||||
|
||||
// Ensure required keys exist
|
||||
if (!isset($this->content['portal_login'])) {
|
||||
$this->content['portal_login'] = 'Portal Login';
|
||||
}
|
||||
if (!isset($this->content['navigation']) || !is_array($this->content['navigation'])) {
|
||||
$this->content['navigation'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleMobileMenu()
|
||||
|
|
|
|||
89
app/Models/DisplayFooterContent.php
Normal file
89
app/Models/DisplayFooterContent.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DisplayFooterContent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'headline',
|
||||
'subline',
|
||||
'url',
|
||||
'short_code',
|
||||
'clicks',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'clicks' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot-Methode für automatische Short-Code-Generierung
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
// Short-Code nur generieren wenn URL vorhanden ist
|
||||
if ($model->url && empty($model->short_code)) {
|
||||
$model->short_code = self::generateUniqueShortCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen eindeutigen Short-Code
|
||||
*/
|
||||
public static function generateUniqueShortCode(): string
|
||||
{
|
||||
do {
|
||||
// Generiere einen 6-stelligen alphanumerischen Code
|
||||
$code = strtolower(substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 6));
|
||||
} while (self::where('short_code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Short-URL zurück (dynamisch je nach Umgebung)
|
||||
*/
|
||||
public function getShortUrlAttribute(): string
|
||||
{
|
||||
$basePath = config('display.base_path', '_display-b2in-eu');
|
||||
$subdomain = config('display.subdomain');
|
||||
$domain = config('display.domain', 'b2in.eu');
|
||||
|
||||
// Wenn Subdomain konfiguriert ist, verwende diese (Live-Server)
|
||||
if ($subdomain) {
|
||||
// Nutze HTTPS für Produktion
|
||||
$protocol = config('app.env') === 'production' ? 'https' : 'http';
|
||||
return "{$protocol}://{$subdomain}.{$domain}/go.php?z={$this->short_code}";
|
||||
}
|
||||
|
||||
// Ansonsten verwende APP_URL + Base-Path (Testserver)
|
||||
$baseUrl = rtrim(config('app.url'), '/');
|
||||
return "{$baseUrl}/{$basePath}/go.php?z={$this->short_code}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Erhöht den Klick-Zähler
|
||||
*/
|
||||
public function incrementClicks(): void
|
||||
{
|
||||
$this->increment('clicks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope für aktive Footer-Inhalte in sortierter Reihenfolge
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
38
app/Models/DisplayVideo.php
Normal file
38
app/Models/DisplayVideo.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DisplayVideo extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'filename',
|
||||
'title',
|
||||
'position',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'position' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope für aktive Videos in sortierter Reihenfolge
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den vollständigen Pfad zum Video zurück
|
||||
*/
|
||||
public function getFullPathAttribute(): string
|
||||
{
|
||||
return "assets/{$this->filename}";
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class Partner extends Model
|
||||
{
|
||||
|
|
@ -13,9 +14,22 @@ class Partner extends Model
|
|||
|
||||
protected $fillable = [
|
||||
'company_name',
|
||||
'display_name',
|
||||
'slug',
|
||||
'type',
|
||||
'brand',
|
||||
'hub_id',
|
||||
'parent_partner_id',
|
||||
'salutation',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'street',
|
||||
'house_number',
|
||||
'zip',
|
||||
'city',
|
||||
'country',
|
||||
'phone',
|
||||
'website',
|
||||
'description',
|
||||
'logo_url',
|
||||
'is_active',
|
||||
|
|
@ -50,6 +64,46 @@ class Partner extends Model
|
|||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Für Kunden: Zugeordneter Makler/Händler (Parent-Partner)
|
||||
*/
|
||||
public function parentPartner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Partner::class, 'parent_partner_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Für Makler/Händler: Zugeordnete Kunden (Child-Partners)
|
||||
*/
|
||||
public function childPartners(): HasMany
|
||||
{
|
||||
return $this->hasMany(Partner::class, 'parent_partner_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias für parentPartner (für bessere Lesbarkeit im Code)
|
||||
*/
|
||||
public function broker(): BelongsTo
|
||||
{
|
||||
return $this->parentPartner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias für childPartners (für bessere Lesbarkeit im Code)
|
||||
*/
|
||||
public function customers(): HasMany
|
||||
{
|
||||
return $this->childPartners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Partner (Manufacturer) kann eine Marke haben
|
||||
*/
|
||||
public function brand(): HasOne
|
||||
{
|
||||
return $this->hasOne(Brand::class);
|
||||
}
|
||||
|
||||
// TODO: Später die Beziehung zu Products hinzufügen
|
||||
// public function products(): HasMany
|
||||
// {
|
||||
|
|
|
|||
87
app/Models/RegistrationCode.php
Normal file
87
app/Models/RegistrationCode.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RegistrationCode extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_AVAILABLE = 'available';
|
||||
public const STATUS_USED = 'used';
|
||||
public const STATUS_EXPIRED = 'expired';
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'role',
|
||||
'name',
|
||||
'status',
|
||||
'broker_partner_id',
|
||||
'partner_id',
|
||||
'assigned_to_code_id',
|
||||
'used_by_user_id',
|
||||
'used_at',
|
||||
'expires_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'expires_at' => 'datetime',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function broker(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Partner::class, 'broker_partner_id');
|
||||
}
|
||||
|
||||
public function partner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Partner::class);
|
||||
}
|
||||
|
||||
public function usedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'used_by_user_id');
|
||||
}
|
||||
|
||||
public function assignedToCode(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(RegistrationCode::class, 'assigned_to_code_id');
|
||||
}
|
||||
|
||||
public function scopeAvailable($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_AVAILABLE);
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_AVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->expires_at && now()->greaterThan($this->expires_at)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function markUsed(?User $user = null): void
|
||||
{
|
||||
$this->status = self::STATUS_USED;
|
||||
$this->used_at = now();
|
||||
if ($user) {
|
||||
$this->used_by_user_id = $user->id;
|
||||
if ($user->partner_id && !$this->partner_id) {
|
||||
$this->partner_id = $user->partner_id;
|
||||
}
|
||||
}
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Notifications\CustomResetPasswordNotification;
|
||||
use App\Notifications\CustomVerifyEmailNotification;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -11,11 +14,12 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
|||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable;
|
||||
use HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -25,6 +29,7 @@ class User extends Authenticatable
|
|||
protected $fillable = [
|
||||
'partner_id',
|
||||
'name',
|
||||
'display_name',
|
||||
'email',
|
||||
'password',
|
||||
'email_verified_at',
|
||||
|
|
@ -49,6 +54,7 @@ class User extends Authenticatable
|
|||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
|
@ -57,6 +63,15 @@ class User extends Authenticatable
|
|||
{
|
||||
return $this->belongsTo(Partner::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registration code used by this user
|
||||
*/
|
||||
public function registrationCode(): HasOne
|
||||
{
|
||||
return $this->hasOne(RegistrationCode::class, 'used_by_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's initials
|
||||
*/
|
||||
|
|
@ -67,4 +82,56 @@ class User extends Authenticatable
|
|||
->map(fn(string $name) => Str::of($name)->substr(0, 1))
|
||||
->implode('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize user data (for users with dependencies)
|
||||
*/
|
||||
public function anonymize(): void
|
||||
{
|
||||
$this->update([
|
||||
'name' => 'Gelöschter Benutzer #' . $this->id,
|
||||
'display_name' => null,
|
||||
'email' => 'deleted_' . $this->id . '@anonymized.local',
|
||||
'password' => bcrypt(Str::random(64)),
|
||||
]);
|
||||
|
||||
// Entferne alle Rollen
|
||||
$this->syncRoles([]);
|
||||
|
||||
// Soft Delete
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has dependencies that require anonymization instead of deletion
|
||||
*/
|
||||
public function hasDependencies(): bool
|
||||
{
|
||||
// TODO: Später erweitern mit weiteren Verknüpfungen
|
||||
// Beispiele: Orders, Projects, Documents, etc.
|
||||
|
||||
// Aktuell: Prüfe ob Partner existiert
|
||||
return $this->partner_id !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token): void
|
||||
{
|
||||
$this->notify(new CustomResetPasswordNotification($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email verification notification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sendEmailVerificationNotification(): void
|
||||
{
|
||||
$this->notify(new CustomVerifyEmailNotification);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
app/Notifications/CustomResetPasswordNotification.php
Normal file
30
app/Notifications/CustomResetPasswordNotification.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class CustomResetPasswordNotification extends ResetPassword
|
||||
{
|
||||
/**
|
||||
* Get the reset password notification mail message for the given URL.
|
||||
*
|
||||
* @param string $url
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
protected function buildMailMessage($url)
|
||||
{
|
||||
$expiryMinutes = config('auth.passwords.' . config('auth.defaults.passwords') . '.expire');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Passwort zurücksetzen - ' . config('app.name'))
|
||||
->greeting('Hallo!')
|
||||
->line('Sie erhalten diese E-Mail, weil wir eine Anfrage zum Zurücksetzen des Passworts für Ihr Konto erhalten haben.')
|
||||
->line('Klicken Sie auf den folgenden Button, um ein neues Passwort zu vergeben:')
|
||||
->action('Passwort zurücksetzen', $url)
|
||||
->line('Dieser Link ist **' . $expiryMinutes . ' Minuten** gültig.')
|
||||
->line('Falls Sie keine Passwort-Zurücksetzung angefordert haben, ist keine weitere Aktion erforderlich. Ihr Passwort bleibt unverändert.')
|
||||
->salutation('Mit freundlichen Grüßen, ' . PHP_EOL . 'Ihr **' . config('app.name') . '** Team');
|
||||
}
|
||||
}
|
||||
29
app/Notifications/CustomVerifyEmailNotification.php
Normal file
29
app/Notifications/CustomVerifyEmailNotification.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class CustomVerifyEmailNotification extends VerifyEmail
|
||||
{
|
||||
/**
|
||||
* Get the verify email notification mail message for the given URL.
|
||||
*
|
||||
* @param string $url
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
protected function buildMailMessage($url)
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('E-Mail-Adresse bestätigen - ' . config('app.name'))
|
||||
->greeting('Willkommen bei ' . config('app.name') . '!')
|
||||
->line('Vielen Dank für Ihre Registrierung!')
|
||||
->line('Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Button klicken:')
|
||||
->action('E-Mail-Adresse bestätigen', $url)
|
||||
->line('Dieser Bestätigungslink läuft in **60 Minuten** ab.')
|
||||
->line('Falls Sie kein Konto bei uns erstellt haben, ignorieren Sie bitte diese E-Mail.')
|
||||
->salutation('Mit freundlichen Grüßen, ' . PHP_EOL . 'Ihr **' . config('app.name') . '** Team');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Routing\UrlGenerator;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
|
@ -55,23 +57,55 @@ class ThemeServiceProvider extends ServiceProvider
|
|||
$domainConfig = array_merge($domainConfig, $confiDomains[$themeOverride]);
|
||||
}
|
||||
|
||||
// Dynamische ASSET_URL basierend auf der aktuellen Domain setzen
|
||||
// Verhindert CORS-Probleme, da Assets immer von derselben Domain geladen werden
|
||||
$assetUrl = $domainConfig['url'];
|
||||
|
||||
// Grundlegende Konfiguration im Anwendungskontext verfügbar machen
|
||||
config([
|
||||
'app.theme' => $domainConfig['theme'],
|
||||
'app.view_prefix' => $domainConfig['view_prefix'],
|
||||
'app.domain_name' => $domainConfig['domain_name'],
|
||||
'app.url' => $domainConfig['url'],
|
||||
'app.asset_url' => $assetUrl, // Dynamische Asset-URL für die aktuelle Domain
|
||||
]);
|
||||
|
||||
// URL-Generator für die aktuelle Domain konfigurieren
|
||||
// Dies ist wichtig, damit asset() und url() die richtige Domain verwenden
|
||||
URL::forceRootUrl($domainConfig['url']);
|
||||
URL::forceScheme(parse_url($domainConfig['url'], PHP_URL_SCHEME) ?: 'https');
|
||||
|
||||
// WICHTIG: Asset-Root direkt im UrlGenerator setzen
|
||||
// Der asset() Helper verwendet einen separaten Asset-Root
|
||||
/** @var UrlGenerator $urlGenerator */
|
||||
$urlGenerator = app('url');
|
||||
$urlGenerator->useAssetOrigin($assetUrl);
|
||||
|
||||
// Spezifischere Daten für die Views verfügbar machen
|
||||
View::share('theme', $domainConfig['theme']);
|
||||
View::share('viewPrefix', $domainConfig['view_prefix']);
|
||||
View::share('domainName', $domainConfig['domain_name']);
|
||||
View::share('domainConfig', $domainConfig);
|
||||
View::share('domainUrl', $domainConfig['url']);
|
||||
View::share('assetUrl', $assetUrl);
|
||||
|
||||
// Vite-Assets-Konfiguration für die aktuelle Domain
|
||||
if (! app()->runningInConsole() && isset($domainConfig['assets_dir'])) {
|
||||
Vite::useBuildDirectory($domainConfig['assets_dir']);
|
||||
if (! app()->runningInConsole()) {
|
||||
if (isset($domainConfig['assets_dir'])) {
|
||||
Vite::useBuildDirectory($domainConfig['assets_dir']);
|
||||
}
|
||||
|
||||
if (app()->environment('local')) {
|
||||
// Entwicklung: Vite Dev Server mit HMR
|
||||
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.b2in.test');
|
||||
Vite::useHotFile(public_path('hot'));
|
||||
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
|
||||
View::share('viteDevServerUrl', $viteDevServerUrl);
|
||||
} else {
|
||||
// Produktion: Assets von der aktuellen Domain laden (kein CORS nötig)
|
||||
Vite::useScriptTagAttributes(['crossorigin' => false]);
|
||||
Vite::useStyleTagAttributes(['crossorigin' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue