Display Module 13-05-2026
This commit is contained in:
parent
6a65354f4c
commit
9262132325
41 changed files with 496 additions and 334 deletions
|
|
@ -62,7 +62,7 @@ services:
|
||||||
MYSQL_EXTRA_OPTIONS: --default-authentication-plugin=mysql_native_password
|
MYSQL_EXTRA_OPTIONS: --default-authentication-plugin=mysql_native_password
|
||||||
volumes:
|
volumes:
|
||||||
- '../:/var/www/html'
|
- '../:/var/www/html'
|
||||||
- './php-upload-limits.ini:/etc/php/8.4/cli/conf.d/99-upload-limits.ini:ro'
|
- './php-upload-limits.ini:/etc/php/8.4/cli/conf.d/100-b2in-upload-limits.ini:ro'
|
||||||
networks:
|
networks:
|
||||||
- sail
|
- sail
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
|
[PHP]
|
||||||
|
; Muss über Sail-Standard (99-sail.ini: 100M) liegen; siehe Display-Mediathek / Livewire-Uploads (~200 MB).
|
||||||
upload_max_filesize = 210M
|
upload_max_filesize = 210M
|
||||||
post_max_size = 210M
|
post_max_size = 210M
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Display;
|
use App\Models\Display;
|
||||||
use App\Models\DisplayFooterContent;
|
use App\Models\DisplayFooterContent;
|
||||||
|
use App\Models\DisplayPlaylist;
|
||||||
|
use App\Models\DisplayPlaylistItem;
|
||||||
use App\Models\DisplayVersion;
|
use App\Models\DisplayVersion;
|
||||||
use App\Models\DisplayVersionItem;
|
use App\Models\DisplayVersionItem;
|
||||||
use App\Models\DisplayVideo;
|
use App\Models\DisplayVideo;
|
||||||
|
|
@ -75,7 +77,16 @@ class MigrateLegacyDisplays extends Command
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$display->versions()->attach($version->id, ['sort_order' => 0]);
|
$playlist = $display->playlists()->create([
|
||||||
|
'status' => DisplayPlaylist::STATUS_PUBLISHED,
|
||||||
|
'published_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DisplayPlaylistItem::create([
|
||||||
|
'display_playlist_id' => $playlist->id,
|
||||||
|
'display_version_id' => $version->id,
|
||||||
|
'sort_order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->info("Migrated {$videos->count()} videos and {$footers->count()} footer items.");
|
$this->info("Migrated {$videos->count()} videos and {$footers->count()} footer items.");
|
||||||
$this->info("Created version: {$version->name} (ID: {$version->id})");
|
$this->info("Created version: {$version->name} (ID: {$version->id})");
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,35 @@ use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
class DisplayVersionApiController extends Controller
|
class DisplayVersionApiController extends Controller
|
||||||
{
|
{
|
||||||
|
public function overview(): JsonResponse
|
||||||
|
{
|
||||||
|
$displays = Display::query()
|
||||||
|
->with(['livePlaylist.modules'])
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereHas('livePlaylist.modules')
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->map(function (Display $display): array {
|
||||||
|
$playlist = $display->livePlaylist;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $display->id,
|
||||||
|
'name' => $display->name,
|
||||||
|
'location' => $display->location,
|
||||||
|
'is_active' => $display->is_active,
|
||||||
|
'is_live' => true,
|
||||||
|
'module_count' => $playlist?->modules->count() ?? 0,
|
||||||
|
'updated_at' => $playlist?->updated_at?->toIso8601String(),
|
||||||
|
'url' => rtrim(config('display.player_url'), '/').'/?id='.$display->id,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'displays' => $displays,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function config(Display $display, DisplayPlaylistConfigBuilder $configBuilder): JsonResponse
|
public function config(Display $display, DisplayPlaylistConfigBuilder $configBuilder): JsonResponse
|
||||||
{
|
{
|
||||||
if (! $display->is_active) {
|
if (! $display->is_active) {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class DisplayList extends Component
|
||||||
], true) ? $playlistStatus : DisplayPlaylist::STATUS_PUBLISHED;
|
], true) ? $playlistStatus : DisplayPlaylist::STATUS_PUBLISHED;
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
$display = Display::with(['livePlaylist.modules', 'draftPlaylist.modules', 'versions'])->findOrFail($id);
|
$display = Display::with(['livePlaylist.modules', 'draftPlaylist.modules'])->findOrFail($id);
|
||||||
$this->displayId = $display->id;
|
$this->displayId = $display->id;
|
||||||
$this->displayName = $display->name;
|
$this->displayName = $display->name;
|
||||||
$this->displayLocation = $display->location ?? '';
|
$this->displayLocation = $display->location ?? '';
|
||||||
|
|
@ -137,7 +137,6 @@ class DisplayList extends Component
|
||||||
$this->previewFrameRefreshCounter++;
|
$this->previewFrameRefreshCounter++;
|
||||||
} else {
|
} else {
|
||||||
$this->syncPublishedPlaylist($display);
|
$this->syncPublishedPlaylist($display);
|
||||||
$this->syncLegacyPivot($display, $this->selectedVersionIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
|
|
@ -202,24 +201,9 @@ class DisplayList extends Component
|
||||||
return $display->draftPlaylist->fresh('modules');
|
return $display->draftPlaylist->fresh('modules');
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->syncLegacyPivot($display, $this->moduleIdsForPlaylist($publishedPlaylist));
|
|
||||||
|
|
||||||
session()->flash('success', 'Entwurf wurde veröffentlicht.');
|
session()->flash('success', 'Entwurf wurde veröffentlicht.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<int> $versionIds
|
|
||||||
*/
|
|
||||||
private function syncLegacyPivot(Display $display, array $versionIds): void
|
|
||||||
{
|
|
||||||
$syncData = [];
|
|
||||||
foreach ($versionIds as $sortOrder => $versionId) {
|
|
||||||
$syncData[$versionId] = ['sort_order' => $sortOrder];
|
|
||||||
}
|
|
||||||
|
|
||||||
$display->versions()->sync($syncData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function syncPublishedPlaylist(Display $display): void
|
private function syncPublishedPlaylist(Display $display): void
|
||||||
{
|
{
|
||||||
$playlist = $display->playlists()->firstOrCreate(
|
$playlist = $display->playlists()->firstOrCreate(
|
||||||
|
|
@ -297,8 +281,7 @@ class DisplayList extends Component
|
||||||
return $this->moduleIdsForPlaylist($display->draftPlaylist);
|
return $this->moduleIdsForPlaylist($display->draftPlaylist);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->moduleIdsForPlaylist($display->livePlaylist)
|
return $this->moduleIdsForPlaylist($display->livePlaylist);
|
||||||
?: $display->versions->pluck('id')->all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteDisplay(int $id): void
|
public function deleteDisplay(int $id): void
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Livewire\Admin\Cms;
|
||||||
use App\Enums\DisplayVersionType;
|
use App\Enums\DisplayVersionType;
|
||||||
use App\Models\DisplayVersion;
|
use App\Models\DisplayVersion;
|
||||||
use App\Models\DisplayVersionItem;
|
use App\Models\DisplayVersionItem;
|
||||||
|
use App\Support\DisplayModuleSettings;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -480,46 +481,7 @@ class DisplayVersionEditor extends Component
|
||||||
*/
|
*/
|
||||||
private function settingsWithDefaults(): array
|
private function settingsWithDefaults(): array
|
||||||
{
|
{
|
||||||
return array_replace_recursive($this->defaultSettings(), $this->version->settings ?? []);
|
return DisplayModuleSettings::merge($this->version->type, $this->version->settings);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function defaultSettings(): array
|
|
||||||
{
|
|
||||||
return match ($this->version->type) {
|
|
||||||
DisplayVersionType::VideoDisplay => [
|
|
||||||
'qr_label' => 'Website',
|
|
||||||
],
|
|
||||||
DisplayVersionType::B2in => [
|
|
||||||
'theme' => 'dark',
|
|
||||||
'header_logo_url' => '../assets/b2in-logo-positive.svg',
|
|
||||||
'header_claim' => 'Connecting Design & Property',
|
|
||||||
'footer_url' => 'B2in.eu',
|
|
||||||
'footer_name' => '',
|
|
||||||
'footer_prefix' => 'by',
|
|
||||||
'qr_url' => '',
|
|
||||||
'transition' => [
|
|
||||||
'type' => 'crossfade',
|
|
||||||
'duration_ms' => 800,
|
|
||||||
],
|
|
||||||
'default_image_duration' => 10,
|
|
||||||
],
|
|
||||||
DisplayVersionType::Offers => [
|
|
||||||
'loop' => true,
|
|
||||||
'logo_url' => '../logo-cabinet-300.png',
|
|
||||||
'brand_text' => 'Bielefeld',
|
|
||||||
'footer_claim' => '',
|
|
||||||
'footer_url' => '',
|
|
||||||
'qr_default_title' => 'Kontakt',
|
|
||||||
'qr_subtitle' => 'QR scannen',
|
|
||||||
'transition' => [
|
|
||||||
'type' => 'fade',
|
|
||||||
'duration' => 600,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ namespace App\Livewire\Admin\Cms;
|
||||||
|
|
||||||
use App\Enums\DisplayVersionType;
|
use App\Enums\DisplayVersionType;
|
||||||
use App\Models\DisplayVersion;
|
use App\Models\DisplayVersion;
|
||||||
|
use App\Support\DisplayModuleSettings;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class DisplayVersionList extends Component
|
class DisplayVersionList extends Component
|
||||||
|
|
@ -70,40 +72,17 @@ class DisplayVersionList extends Component
|
||||||
*/
|
*/
|
||||||
private function defaultSettingsForType(string $type): array
|
private function defaultSettingsForType(string $type): array
|
||||||
{
|
{
|
||||||
return match ($type) {
|
return DisplayModuleSettings::defaults($type);
|
||||||
'b2in' => [
|
|
||||||
'theme' => 'dark',
|
|
||||||
'header_logo_url' => '../assets/b2in-logo-positive.svg',
|
|
||||||
'header_claim' => 'Connecting Design & Property',
|
|
||||||
'footer_name' => '',
|
|
||||||
'footer_url' => 'B2in.eu',
|
|
||||||
'footer_prefix' => 'by',
|
|
||||||
'qr_url' => '',
|
|
||||||
'transition' => ['type' => 'crossfade', 'duration_ms' => 800],
|
|
||||||
'default_image_duration' => 10,
|
|
||||||
'rotation_weights' => ['immobilien' => 70, 'moebel' => 30],
|
|
||||||
'display_active' => true,
|
|
||||||
],
|
|
||||||
'offers' => [
|
|
||||||
'loop' => true,
|
|
||||||
'logo_url' => '../logo-cabinet-300.png',
|
|
||||||
'brand_text' => 'Bielefeld',
|
|
||||||
'footer_claim' => '',
|
|
||||||
'footer_url' => '',
|
|
||||||
'qr_default_title' => 'Kontakt',
|
|
||||||
'qr_subtitle' => 'QR scannen',
|
|
||||||
'transition' => ['type' => 'fade', 'duration' => 600],
|
|
||||||
],
|
|
||||||
'video-display' => [
|
|
||||||
'qr_label' => 'Website',
|
|
||||||
],
|
|
||||||
default => [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$versions = DisplayVersion::withCount(['items', 'displays'])
|
$versions = DisplayVersion::withCount([
|
||||||
|
'items',
|
||||||
|
'playlistItems as displays_count' => fn ($query) => $query
|
||||||
|
->join('display_playlists', 'display_playlist_items.display_playlist_id', '=', 'display_playlists.id')
|
||||||
|
->select(DB::raw('count(distinct display_playlists.display_id)')),
|
||||||
|
])
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class MediaLibraryUploader extends Component
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'uploads' => 'nullable|array|max:20',
|
'uploads' => 'nullable|array|max:20',
|
||||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:204800',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$service = app(MediaConversionService::class);
|
$service = app(MediaConversionService::class);
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class MediaPicker extends Component
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'quickUploads' => 'nullable|array|max:5',
|
'quickUploads' => 'nullable|array|max:5',
|
||||||
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:204800',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$service = app(MediaConversionService::class);
|
$service = app(MediaConversionService::class);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -30,17 +29,6 @@ class Display extends Model
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Wird in Phase 7 entfernt. Nutze stattdessen liveModules()
|
|
||||||
* oder die Playlist-Relationen (livePlaylist, draftPlaylist).
|
|
||||||
*/
|
|
||||||
public function versions(): BelongsToMany
|
|
||||||
{
|
|
||||||
return $this->belongsToMany(DisplayVersion::class, 'display_display_version')
|
|
||||||
->withPivot('sort_order')
|
|
||||||
->orderByPivot('sort_order');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<DisplayPlaylist, $this>
|
* @return HasMany<DisplayPlaylist, $this>
|
||||||
*/
|
*/
|
||||||
|
|
@ -67,29 +55,6 @@ class Display extends Model
|
||||||
->where('status', DisplayPlaylist::STATUS_DRAFT);
|
->where('status', DisplayPlaylist::STATUS_DRAFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die Module der aktuell veröffentlichten Bespielung in Reihenfolge.
|
|
||||||
*/
|
|
||||||
public function liveModules(): BelongsToMany
|
|
||||||
{
|
|
||||||
return $this->belongsToMany(
|
|
||||||
DisplayVersion::class,
|
|
||||||
'display_playlist_items',
|
|
||||||
'display_playlist_id',
|
|
||||||
'display_version_id'
|
|
||||||
)
|
|
||||||
->wherePivotIn(
|
|
||||||
'display_playlist_id',
|
|
||||||
DisplayPlaylist::query()
|
|
||||||
->where('display_id', $this->id ?? 0)
|
|
||||||
->where('status', DisplayPlaylist::STATUS_PUBLISHED)
|
|
||||||
->select('id')
|
|
||||||
)
|
|
||||||
->withPivot(['sort_order', 'id'])
|
|
||||||
->withTimestamps()
|
|
||||||
->orderByPivot('sort_order');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function ensurePreviewToken(): string
|
public function ensurePreviewToken(): string
|
||||||
{
|
{
|
||||||
if (! $this->preview_token) {
|
if (! $this->preview_token) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use App\Enums\DisplayVersionType;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class DisplayVersion extends Model
|
class DisplayVersion extends Model
|
||||||
|
|
@ -35,10 +34,12 @@ class DisplayVersion extends Model
|
||||||
return $this->hasMany(DisplayVersionItem::class)->orderBy('sort_order');
|
return $this->hasMany(DisplayVersionItem::class)->orderBy('sort_order');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function displays(): BelongsToMany
|
/**
|
||||||
|
* @return HasMany<DisplayPlaylistItem, $this>
|
||||||
|
*/
|
||||||
|
public function playlistItems(): HasMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Display::class, 'display_display_version')
|
return $this->hasMany(DisplayPlaylistItem::class);
|
||||||
->withPivot('sort_order');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Services;
|
||||||
|
|
||||||
use App\Models\DisplayPlaylist;
|
use App\Models\DisplayPlaylist;
|
||||||
use App\Models\DisplayVersion;
|
use App\Models\DisplayVersion;
|
||||||
|
use App\Support\DisplayModuleSettings;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
class DisplayPlaylistConfigBuilder
|
class DisplayPlaylistConfigBuilder
|
||||||
|
|
@ -95,9 +96,7 @@ class DisplayPlaylistConfigBuilder
|
||||||
return [
|
return [
|
||||||
'type' => 'video-display',
|
'type' => 'video-display',
|
||||||
'version_name' => $module->name,
|
'version_name' => $module->name,
|
||||||
'settings' => array_replace([
|
'settings' => DisplayModuleSettings::merge($module->type, $module->settings),
|
||||||
'qr_label' => 'Website',
|
|
||||||
], $module->settings ?? []),
|
|
||||||
'videoPlaylist' => $videos,
|
'videoPlaylist' => $videos,
|
||||||
'footerContent' => $footerContent,
|
'footerContent' => $footerContent,
|
||||||
];
|
];
|
||||||
|
|
@ -133,20 +132,7 @@ class DisplayPlaylistConfigBuilder
|
||||||
return [
|
return [
|
||||||
'type' => 'b2in',
|
'type' => 'b2in',
|
||||||
'version_name' => $module->name,
|
'version_name' => $module->name,
|
||||||
'settings' => array_replace_recursive([
|
'settings' => DisplayModuleSettings::merge($module->type, $module->settings),
|
||||||
'theme' => 'dark',
|
|
||||||
'header_logo_url' => '../assets/b2in-logo-positive.svg',
|
|
||||||
'header_claim' => 'Connecting Design & Property',
|
|
||||||
'footer_url' => 'B2in.eu',
|
|
||||||
'footer_name' => '',
|
|
||||||
'footer_prefix' => 'by',
|
|
||||||
'qr_url' => '',
|
|
||||||
'transition' => [
|
|
||||||
'type' => 'crossfade',
|
|
||||||
'duration_ms' => 800,
|
|
||||||
],
|
|
||||||
'default_image_duration' => 10,
|
|
||||||
], $module->settings ?? []),
|
|
||||||
'items' => $mediaItems,
|
'items' => $mediaItems,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -180,19 +166,7 @@ class DisplayPlaylistConfigBuilder
|
||||||
return [
|
return [
|
||||||
'type' => 'offers',
|
'type' => 'offers',
|
||||||
'version_name' => $module->name,
|
'version_name' => $module->name,
|
||||||
'settings' => array_replace_recursive([
|
'settings' => DisplayModuleSettings::merge($module->type, $module->settings),
|
||||||
'loop' => true,
|
|
||||||
'logo_url' => '../logo-cabinet-300.png',
|
|
||||||
'brand_text' => 'Bielefeld',
|
|
||||||
'footer_claim' => '',
|
|
||||||
'footer_url' => '',
|
|
||||||
'qr_default_title' => 'Kontakt',
|
|
||||||
'qr_subtitle' => 'QR scannen',
|
|
||||||
'transition' => [
|
|
||||||
'type' => 'fade',
|
|
||||||
'duration' => 600,
|
|
||||||
],
|
|
||||||
], $module->settings ?? []),
|
|
||||||
'slides' => $slides,
|
'slides' => $slides,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
app/Support/DisplayModuleSettings.php
Normal file
61
app/Support/DisplayModuleSettings.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Enums\DisplayVersionType;
|
||||||
|
|
||||||
|
class DisplayModuleSettings
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function defaults(DisplayVersionType|string $type): array
|
||||||
|
{
|
||||||
|
$typeValue = $type instanceof DisplayVersionType ? $type->value : $type;
|
||||||
|
|
||||||
|
return match ($typeValue) {
|
||||||
|
DisplayVersionType::VideoDisplay->value => [
|
||||||
|
'qr_label' => 'Website',
|
||||||
|
],
|
||||||
|
DisplayVersionType::B2in->value => [
|
||||||
|
'theme' => 'dark',
|
||||||
|
'header_logo_url' => '../assets/b2in-logo-positive.svg',
|
||||||
|
'header_claim' => 'Connecting Design & Property',
|
||||||
|
'footer_url' => 'B2in.eu',
|
||||||
|
'footer_name' => '',
|
||||||
|
'footer_prefix' => 'by',
|
||||||
|
'qr_url' => '',
|
||||||
|
'transition' => [
|
||||||
|
'type' => 'crossfade',
|
||||||
|
'duration_ms' => 800,
|
||||||
|
],
|
||||||
|
'default_image_duration' => 10,
|
||||||
|
'rotation_weights' => ['immobilien' => 70, 'moebel' => 30],
|
||||||
|
'display_active' => true,
|
||||||
|
],
|
||||||
|
DisplayVersionType::Offers->value => [
|
||||||
|
'loop' => true,
|
||||||
|
'logo_url' => '../logo-cabinet-300.png',
|
||||||
|
'brand_text' => 'Bielefeld',
|
||||||
|
'footer_claim' => '',
|
||||||
|
'footer_url' => '',
|
||||||
|
'qr_default_title' => 'Kontakt',
|
||||||
|
'qr_subtitle' => 'QR scannen',
|
||||||
|
'transition' => [
|
||||||
|
'type' => 'fade',
|
||||||
|
'duration' => 600,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed>|null $settings
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function merge(DisplayVersionType|string $type, ?array $settings): array
|
||||||
|
{
|
||||||
|
return array_replace_recursive(self::defaults($type), $settings ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,4 +22,7 @@ return [
|
||||||
// Haupt-Domain
|
// Haupt-Domain
|
||||||
'domain' => env('DISPLAY_DOMAIN', 'b2in.eu'),
|
'domain' => env('DISPLAY_DOMAIN', 'b2in.eu'),
|
||||||
|
|
||||||
|
// Öffentliche Player-URL der Display-Domain
|
||||||
|
'player_url' => env('DISPLAY_PLAYER_URL', 'https://cabinet.b2in.eu/display'),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('display_display_version');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::create('display_display_version', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('display_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('display_version_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['display_id', 'display_version_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -16,8 +16,8 @@ Im Admin-Portal unter `portal.b2in.test/admin/cms/` existiert der Bereich **Stor
|
||||||
|---|---|
|
|---|---|
|
||||||
| `cms/display-dashboard` | Übersicht / Einstieg |
|
| `cms/display-dashboard` | Übersicht / Einstieg |
|
||||||
| `cms/display-media` | Mediathek (eigene Display-Mediathek, getrennt von Flux CMS) |
|
| `cms/display-media` | Mediathek (eigene Display-Mediathek, getrennt von Flux CMS) |
|
||||||
| `cms/display-versions` | Inhalts-„Versionen" |
|
| `cms/display-modules` | Inhalts-Module |
|
||||||
| `cms/display-versions/{id}/edit` | Editor für eine Version |
|
| `cms/display-modules/{id}/edit` | Editor für ein Modul |
|
||||||
| `cms/displays` | Physische Displays + Playlist-Zuweisung |
|
| `cms/displays` | Physische Displays + Playlist-Zuweisung |
|
||||||
| `cms/cabinet-tablet` | Info-Tablet (Öffnungszeiten/Status) |
|
| `cms/cabinet-tablet` | Info-Tablet (Öffnungszeiten/Status) |
|
||||||
|
|
||||||
|
|
@ -25,13 +25,14 @@ Im Admin-Portal unter `portal.b2in.test/admin/cms/` existiert der Bereich **Stor
|
||||||
|
|
||||||
```
|
```
|
||||||
displays (5 Datensätze live)
|
displays (5 Datensätze live)
|
||||||
└── m:n via display_display_version (sort_order = Playlist-Reihenfolge)
|
└── 1:n display_playlists (Live/Entwurf)
|
||||||
└── display_versions (5 Datensätze live)
|
└── 1:n display_playlist_items (sort_order = Playlist-Reihenfolge)
|
||||||
├── type: video-display | b2in | offers
|
└── display_versions (technisch), fachlich Module
|
||||||
├── settings: JSON
|
├── type: video-display | b2in | offers
|
||||||
└── 1:n display_version_items (17 Datensätze live)
|
├── settings: JSON
|
||||||
├── item_type: video | footer | media | slide
|
└── 1:n display_version_items
|
||||||
└── content: JSON
|
├── item_type: video | footer | media | slide
|
||||||
|
└── content: JSON
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.3 Echte Live-Daten (Stand heute)
|
### 1.3 Echte Live-Daten (Stand heute)
|
||||||
|
|
@ -83,7 +84,7 @@ displays (5 Datensätze live)
|
||||||
| Mediathek | **Display-Mediathek** *(unverändert)* | Bilder/Videos für Displays. |
|
| Mediathek | **Display-Mediathek** *(unverändert)* | Bilder/Videos für Displays. |
|
||||||
| Info-Tablet | **Info-Tablet** *(unverändert)* | Eingangs-Tablet mit Öffnungszeiten. |
|
| Info-Tablet | **Info-Tablet** *(unverändert)* | Eingangs-Tablet mit Öffnungszeiten. |
|
||||||
|
|
||||||
Routen werden entsprechend umbenannt: `display-versions` → `display-modules`.
|
Routen wurden entsprechend umbenannt: `display-versions` → `display-modules`. Die Übergangs-Redirects wurden in Phase 7 entfernt.
|
||||||
|
|
||||||
### 2.2 Neues mentales Modell
|
### 2.2 Neues mentales Modell
|
||||||
|
|
||||||
|
|
@ -181,7 +182,7 @@ für jedes Display D:
|
||||||
erstelle display_playlists (display_id=D.id, status='published', published_at=now())
|
erstelle display_playlists (display_id=D.id, status='published', published_at=now())
|
||||||
für jeden Eintrag aus display_display_version (display_id=D.id), sortiert nach sort_order:
|
für jeden Eintrag aus display_display_version (display_id=D.id), sortiert nach sort_order:
|
||||||
erstelle display_playlist_items (...)
|
erstelle display_playlist_items (...)
|
||||||
display_display_version-Tabelle bleibt vorerst → wird in Phase 7 dropped.
|
display_display_version-Tabelle wurde in Phase 7 dropped.
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ergebnis nach Migration:** Alle 5 Displays haben eine Live-Bespielung, kein Entwurf. Konsumenten-API liefert exakt das gleiche wie heute.
|
**Ergebnis nach Migration:** Alle 5 Displays haben eine Live-Bespielung, kein Entwurf. Konsumenten-API liefert exakt das gleiche wie heute.
|
||||||
|
|
@ -355,15 +356,15 @@ Jede Phase liefert ein in sich getestetes, deploybares Inkrement.
|
||||||
- [ ] Player-Templates: Single-Module-Modus
|
- [ ] Player-Templates: Single-Module-Modus
|
||||||
|
|
||||||
### Phase 6 – Umbenennung & Onboarding (Tag 3)
|
### Phase 6 – Umbenennung & Onboarding (Tag 3)
|
||||||
- [ ] Routen: `display-versions` → `display-modules` (mit 301-Redirect)
|
- [x] Routen: `display-versions` → `display-modules`
|
||||||
- [ ] Komponenten / Views umbenennen
|
- [x] Komponenten / Views umbenennen
|
||||||
- [ ] Dashboard-Texte / Hilfe-Bausteine aktualisieren
|
- [x] Dashboard-Texte / Hilfe-Bausteine aktualisieren
|
||||||
- [ ] Tooltips an Schlüsselstellen
|
- [x] Tooltips an Schlüsselstellen
|
||||||
|
|
||||||
### Phase 7 – Aufräumen (Tag 4)
|
### Phase 7 – Aufräumen (Tag 4)
|
||||||
- [ ] `display_display_version`-Tabelle dropped
|
- [x] `display_display_version`-Tabelle dropped
|
||||||
- [ ] Alte Routen entfernt
|
- [x] Alte Routen entfernt
|
||||||
- [ ] `DISPLAY_CMS_README.md` aktualisiert (in `dev/` ablegen)
|
- [x] Entwicklerdoku in `dev/displays-11-05-2026` aktualisiert
|
||||||
- [ ] Vollständiger Test-Run
|
- [ ] Vollständiger Test-Run
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ Umsetzung:
|
||||||
|
|
||||||
## Phase 6 – Umbenennung Versionen → Module + Onboarding
|
## Phase 6 – Umbenennung Versionen → Module + Onboarding
|
||||||
|
|
||||||
**Ziel:** Die Admin-UI verwendet den fachlich korrekten Begriff „Module“. Alte URLs bleiben kompatibel und leiten weiter.
|
**Ziel:** Die Admin-UI verwendet den fachlich korrekten Begriff „Module“. Alte URLs wurden während der Übergangsphase per 301 weitergeleitet und in Phase 7 entfernt.
|
||||||
|
|
||||||
### Stand 12.05.2026 – ✅ abgeschlossen
|
### Stand 12.05.2026 – ✅ abgeschlossen
|
||||||
|
|
||||||
|
|
@ -261,9 +261,9 @@ Dateien:
|
||||||
Umsetzung:
|
Umsetzung:
|
||||||
- Neue Routen: `admin/cms/display-modules` und `admin/cms/display-modules/{displayVersion}/edit`
|
- Neue Routen: `admin/cms/display-modules` und `admin/cms/display-modules/{displayVersion}/edit`
|
||||||
- Neue Routennamen: `admin.cms.display-modules` und `admin.cms.display-module-edit`
|
- Neue Routennamen: `admin.cms.display-modules` und `admin.cms.display-module-edit`
|
||||||
- Alte `display-versions`-Routen bleiben erhalten und leiten per 301 auf die Modul-Routen weiter
|
- Alte `display-versions`-Routen waren während der Übergangsphase als 301-Redirects aktiv und wurden in Phase 7 entfernt
|
||||||
- Sidebar, Dashboard, Listen- und Editor-Texte verwenden „Module“
|
- Sidebar, Dashboard, Listen- und Editor-Texte verwenden „Module“
|
||||||
- Technische Modell-/Klassennamen bleiben bis Phase 7 kompatibel bei `DisplayVersion`
|
- Technische Modell-/Klassennamen bleiben bei `DisplayVersion`, da sie fachlich weiterhin die wiederverwendbaren Module abbilden
|
||||||
|
|
||||||
#### Tests
|
#### Tests
|
||||||
|
|
||||||
|
|
@ -276,3 +276,35 @@ tests/Feature/DisplayPlaylistMigrationTest.php – ok
|
||||||
|
|
||||||
Insgesamt 64 grüne Tests für Phasen 5/6 und die angrenzenden Display-Flows. Pint clean.
|
Insgesamt 64 grüne Tests für Phasen 5/6 und die angrenzenden Display-Flows. Pint clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7 – Technisches Aufräumen & Optimierung
|
||||||
|
|
||||||
|
**Ziel:** Nach Stabilisierung des neuen Playlist-Flows wird die alte Pivot-Kompatibilität entfernt und der Modul-Editor weiter vereinheitlicht.
|
||||||
|
|
||||||
|
### Stand 13.05.2026 – ✅ umgesetzt
|
||||||
|
|
||||||
|
Dateien:
|
||||||
|
- `app/Models/Display.php`
|
||||||
|
- `app/Models/DisplayVersion.php`
|
||||||
|
- `app/Livewire/Admin/Cms/DisplayList.php`
|
||||||
|
- `app/Console/Commands/MigrateLegacyDisplays.php`
|
||||||
|
- `app/Support/DisplayModuleSettings.php`
|
||||||
|
- `app/Services/DisplayPlaylistConfigBuilder.php`
|
||||||
|
- `app/Livewire/Admin/Cms/DisplayVersionEditor.php`
|
||||||
|
- `app/Livewire/Admin/Cms/DisplayVersionList.php`
|
||||||
|
- `routes/admin.php`
|
||||||
|
- `database/migrations/2026_05_13_103600_drop_display_display_version_table.php`
|
||||||
|
- `resources/views/livewire/admin/cms/display-list.blade.php`
|
||||||
|
- `resources/views/livewire/admin/cms/display-version-editor.blade.php`
|
||||||
|
- `resources/views/livewire/admin/cms/partials/version-editor-video.blade.php`
|
||||||
|
|
||||||
|
Umsetzung:
|
||||||
|
- Alte Pivot-Tabelle `display_display_version` wird per Migration entfernt
|
||||||
|
- Legacy-Relationen `Display::versions()` und `DisplayVersion::displays()` wurden entfernt
|
||||||
|
- Display-Bearbeitung, Draft-Veröffentlichung und Legacy-Migrations-Command schreiben ausschließlich in `display_playlists` und `display_playlist_items`
|
||||||
|
- Alte `display-versions`-Redirect-Routen wurden entfernt; die Admin-UI nutzt nur noch `display-modules`
|
||||||
|
- Modul-Settings-Defaults liegen zentral in `App\Support\DisplayModuleSettings` und werden von Editor, Listen-Erstellung und API-Config-Builder gemeinsam genutzt
|
||||||
|
- Admin-Iframes laden per `loading="lazy"` verzögert, um die parallelen Player-Vorschauen leichter zu halten
|
||||||
|
- Video-Display-Items zeigen im Editor sichtbar an, ob die Quelle aus der Mediathek oder aus einem Legacy-Dateinamen kommt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ services:
|
||||||
REDIS_HOST: global-redis
|
REDIS_HOST: global-redis
|
||||||
volumes:
|
volumes:
|
||||||
- '.:/var/www/html'
|
- '.:/var/www/html'
|
||||||
- './.devcontainer/php-upload-limits.ini:/etc/php/8.4/cli/conf.d/99-upload-limits.ini:ro'
|
- './.devcontainer/php-upload-limits.ini:/etc/php/8.4/cli/conf.d/100-b2in-upload-limits.ini:ro'
|
||||||
networks:
|
networks:
|
||||||
- sail
|
- sail
|
||||||
- proxy
|
- proxy
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.pdf,.doc,.docx,.jpg,.jpeg,.png">
|
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.pdf,.doc,.docx,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone
|
<flux:file-upload.dropzone
|
||||||
heading="Dateien hochladen"
|
heading="Dateien hochladen"
|
||||||
text="Bilder (JPG, PNG, WebP, SVG) und Dokumente (PDF, DOC) — max. 10 MB pro Datei"
|
text="Bilder (JPG, PNG, WebP, SVG) und Dokumente (PDF, DOC) — max. 200 MB pro Datei"
|
||||||
with-progress />
|
with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class MediaLibraryUploader extends Component
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'uploads' => 'nullable|array|max:20',
|
'uploads' => 'nullable|array|max:20',
|
||||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:204800',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$service = app(MediaConversionService::class);
|
$service = app(MediaConversionService::class);
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class MediaPicker extends Component
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'quickUploads' => 'nullable|array|max:5',
|
'quickUploads' => 'nullable|array|max:5',
|
||||||
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:204800',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$service = app(MediaConversionService::class);
|
$service = app(MediaConversionService::class);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class MediaUploader extends Component
|
||||||
|
|
||||||
public string $directory = 'cms/uploads';
|
public string $directory = 'cms/uploads';
|
||||||
|
|
||||||
#[Validate('file|max:10240')]
|
#[Validate('file|max:204800')]
|
||||||
public $file;
|
public $file;
|
||||||
|
|
||||||
public function updatedFile(): void
|
public function updatedFile(): void
|
||||||
|
|
|
||||||
|
|
@ -85,11 +85,11 @@ Beim Hochladen neuer Videos beachten:
|
||||||
- [ ] Format: **MP4** (H.264 + AAC)
|
- [ ] Format: **MP4** (H.264 + AAC)
|
||||||
- [ ] Auflösung: **Max 1920x1080**
|
- [ ] Auflösung: **Max 1920x1080**
|
||||||
- [ ] Bitrate: **5-10 Mbps**
|
- [ ] Bitrate: **5-10 Mbps**
|
||||||
- [ ] Dateigröße: **Max 100 MB**
|
- [ ] Dateigröße: **Max 200 MB**
|
||||||
- [ ] Länge: **15-60 Sekunden** (optimal)
|
- [ ] Länge: **15-60 Sekunden** (optimal)
|
||||||
|
|
||||||
### ⚠️ Vermeiden:
|
### ⚠️ Vermeiden:
|
||||||
- ❌ Zu große Dateien (>100MB)
|
- ❌ Zu große Dateien (>200MB)
|
||||||
- ❌ Zu hohe Bitrate (>10 Mbps)
|
- ❌ Zu hohe Bitrate (>10 Mbps)
|
||||||
- ❌ Zu lange Videos (>3 Min)
|
- ❌ Zu lange Videos (>3 Min)
|
||||||
- ❌ Exotische Formate (MOV, AVI, WMV)
|
- ❌ Exotische Formate (MOV, AVI, WMV)
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ setTimeout(() => {
|
||||||
|
|
||||||
### 3. **Dateigrößen**
|
### 3. **Dateigrößen**
|
||||||
- **Optimal:** 10-50 MB pro Video
|
- **Optimal:** 10-50 MB pro Video
|
||||||
- **Maximum:** 100 MB pro Video
|
- **Maximum:** 200 MB pro Video
|
||||||
- **Warum:** Schnelleres Laden, weniger Buffering
|
- **Warum:** Schnelleres Laden, weniger Buffering
|
||||||
|
|
||||||
### 4. **Playlist-Größe**
|
### 4. **Playlist-Größe**
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,56 @@
|
||||||
.status-message { font-weight: 300; opacity: 0.7; }
|
.status-message { font-weight: 300; opacity: 0.7; }
|
||||||
.status-error { color: #ef4444; font-weight: 500; }
|
.status-error { color: #ef4444; font-weight: 500; }
|
||||||
.status-sub { font-size: 1.4vh; opacity: 0.4; margin-top: 1vh; }
|
.status-sub { font-size: 1.4vh; opacity: 0.4; margin-top: 1vh; }
|
||||||
|
|
||||||
|
.display-overview {
|
||||||
|
position: fixed; inset: 0; z-index: 10000;
|
||||||
|
overflow-y: auto; background: radial-gradient(circle at top, #12364d 0, #05070a 42%, #000 100%);
|
||||||
|
color: #fff; cursor: auto; padding: clamp(24px, 5vw, 72px);
|
||||||
|
}
|
||||||
|
.display-overview.hidden { display: none; }
|
||||||
|
.display-overview__inner { width: min(1120px, 100%); margin: 0 auto; }
|
||||||
|
.display-overview__eyebrow {
|
||||||
|
color: #38bdf8; font-size: 13px; font-weight: 700;
|
||||||
|
letter-spacing: 0.16em; text-transform: uppercase; margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.display-overview h1 {
|
||||||
|
font-size: clamp(34px, 6vw, 76px); line-height: 0.95;
|
||||||
|
letter-spacing: -0.05em; margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.display-overview__intro {
|
||||||
|
max-width: 720px; color: rgba(255,255,255,0.68);
|
||||||
|
font-size: clamp(16px, 2vw, 22px); line-height: 1.5; margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.display-overview__grid {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.display-card {
|
||||||
|
display: flex; flex-direction: column; gap: 16px;
|
||||||
|
min-height: 220px; padding: 24px; border-radius: 28px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.14);
|
||||||
|
background: rgba(255,255,255,0.08); color: #fff; text-decoration: none;
|
||||||
|
box-shadow: 0 24px 70px rgba(0,0,0,0.24);
|
||||||
|
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||||||
|
}
|
||||||
|
.display-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(56,189,248,0.55);
|
||||||
|
background: rgba(255,255,255,0.12);
|
||||||
|
}
|
||||||
|
.display-card__badges { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.display-badge {
|
||||||
|
border-radius: 999px; padding: 6px 10px; font-size: 12px; font-weight: 700;
|
||||||
|
background: rgba(34,197,94,0.18); color: #86efac; border: 1px solid rgba(134,239,172,0.28);
|
||||||
|
}
|
||||||
|
.display-badge--live { background: rgba(56,189,248,0.18); color: #7dd3fc; border-color: rgba(125,211,252,0.28); }
|
||||||
|
.display-card__title { font-size: 28px; font-weight: 700; letter-spacing: -0.03em; }
|
||||||
|
.display-card__meta { display: grid; gap: 6px; color: rgba(255,255,255,0.62); font-size: 15px; }
|
||||||
|
.display-card__action { margin-top: auto; color: #7dd3fc; font-weight: 700; }
|
||||||
|
.display-overview__empty {
|
||||||
|
border: 1px dashed rgba(255,255,255,0.24); border-radius: 28px;
|
||||||
|
padding: 32px; color: rgba(255,255,255,0.62);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -434,6 +484,18 @@
|
||||||
<div class="status-sub">Neustart in Kürze...</div>
|
<div class="status-sub">Neustart in Kürze...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="display-overview hidden" id="display-overview">
|
||||||
|
<div class="display-overview__inner">
|
||||||
|
<div class="display-overview__eyebrow">Cabinet Display Player</div>
|
||||||
|
<h1>Aktive Live-Displays</h1>
|
||||||
|
<p class="display-overview__intro">
|
||||||
|
Wählen Sie ein Display aus, um die veröffentlichte Live-Bespielung zu öffnen.
|
||||||
|
Angezeigt werden nur aktive Displays mit veröffentlichter Live-Konfiguration.
|
||||||
|
</p>
|
||||||
|
<div class="display-overview__grid" id="display-overview-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|
@ -462,15 +524,12 @@ class DisplayPlayer {
|
||||||
this.moduleId = this.detectModuleId();
|
this.moduleId = this.detectModuleId();
|
||||||
this.itemId = this.detectItemId();
|
this.itemId = this.detectItemId();
|
||||||
this.displayId = this.detectDisplayId();
|
this.displayId = this.detectDisplayId();
|
||||||
if (!this.displayId && !this.previewToken && !this.moduleId) {
|
|
||||||
this.showError('Keine Display-ID oder Vorschau angegeben. URL: /display/index.html?id=1');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API
|
// API
|
||||||
this.BASE_URL = this.detectBaseUrl();
|
this.BASE_URL = this.detectBaseUrl();
|
||||||
this.API_CONFIG = this.detectConfigUrl();
|
this.API_CONFIG = this.detectConfigUrl();
|
||||||
this.API_CHECK = this.detectCheckUrl();
|
this.API_CHECK = this.detectCheckUrl();
|
||||||
|
this.API_OVERVIEW = `${this.BASE_URL}/api/display/overview`;
|
||||||
|
|
||||||
// Timing
|
// Timing
|
||||||
this.POLL_INTERVAL = 60000;
|
this.POLL_INTERVAL = 60000;
|
||||||
|
|
@ -497,6 +556,8 @@ class DisplayPlayer {
|
||||||
this.loadingInfo = document.getElementById('loading-info');
|
this.loadingInfo = document.getElementById('loading-info');
|
||||||
this.errorOverlay = document.getElementById('error-overlay');
|
this.errorOverlay = document.getElementById('error-overlay');
|
||||||
this.errorMessage = document.getElementById('error-message');
|
this.errorMessage = document.getElementById('error-message');
|
||||||
|
this.overviewOverlay = document.getElementById('display-overview');
|
||||||
|
this.overviewList = document.getElementById('display-overview-list');
|
||||||
|
|
||||||
this.loadingInfo.textContent = this.detectLoadingLabel();
|
this.loadingInfo.textContent = this.detectLoadingLabel();
|
||||||
|
|
||||||
|
|
@ -583,13 +644,16 @@ class DisplayPlayer {
|
||||||
}
|
}
|
||||||
return `Modul #${this.moduleId}`;
|
return `Modul #${this.moduleId}`;
|
||||||
}
|
}
|
||||||
|
if (!this.displayId) {
|
||||||
|
return 'Display-Übersicht';
|
||||||
|
}
|
||||||
return `Display #${this.displayId}`;
|
return `Display #${this.displayId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
detectBaseUrl() {
|
detectBaseUrl() {
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
if (hostname === 'cabinet.b2in.eu' || hostname.includes('b2in.eu')) {
|
if (hostname === 'cabinet.b2in.eu') {
|
||||||
return 'https://b2in.eu';
|
return 'https://portal.b2in.eu';
|
||||||
}
|
}
|
||||||
return window.location.origin;
|
return window.location.origin;
|
||||||
}
|
}
|
||||||
|
|
@ -602,6 +666,11 @@ class DisplayPlayer {
|
||||||
console.log(`[Display] Initializing ${this.detectLoadingLabel()}`);
|
console.log(`[Display] Initializing ${this.detectLoadingLabel()}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!this.displayId && !this.previewToken && !this.moduleId) {
|
||||||
|
await this.fetchOverview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.fetchConfig();
|
await this.fetchConfig();
|
||||||
|
|
||||||
if (this.playlist.length === 0) {
|
if (this.playlist.length === 0) {
|
||||||
|
|
@ -642,6 +711,16 @@ class DisplayPlayer {
|
||||||
console.log(`[Display] Loaded ${this.playlist.length} version(s)`);
|
console.log(`[Display] Loaded ${this.playlist.length} version(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchOverview() {
|
||||||
|
const response = await fetch(this.API_OVERVIEW);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.renderOverview(data.displays || []);
|
||||||
|
}
|
||||||
|
|
||||||
startPolling() {
|
startPolling() {
|
||||||
if (!this.API_CHECK) {
|
if (!this.API_CHECK) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -779,11 +858,50 @@ class DisplayPlayer {
|
||||||
this.loadingOverlay.classList.add('hidden');
|
this.loadingOverlay.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderOverview(displays) {
|
||||||
|
this.hideLoading();
|
||||||
|
this.errorOverlay.classList.add('hidden');
|
||||||
|
this.overviewOverlay.classList.remove('hidden');
|
||||||
|
|
||||||
|
if (displays.length === 0) {
|
||||||
|
this.overviewList.innerHTML = `
|
||||||
|
<div class="display-overview__empty">
|
||||||
|
Es sind aktuell keine aktiven Live-Displays veröffentlicht.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.overviewList.innerHTML = displays.map(display => `
|
||||||
|
<a class="display-card" href="${this.escapeHtml(display.url)}">
|
||||||
|
<div class="display-card__badges">
|
||||||
|
<span class="display-badge">Aktiv</span>
|
||||||
|
<span class="display-badge display-badge--live">Live</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="display-card__title">${this.escapeHtml(display.name)}</div>
|
||||||
|
<div class="display-card__meta">
|
||||||
|
<span>Display-ID: ${this.escapeHtml(display.id)}</span>
|
||||||
|
${display.location ? `<span>Standort: ${this.escapeHtml(display.location)}</span>` : ''}
|
||||||
|
<span>${this.escapeHtml(display.module_count)} Modul(e) veröffentlicht</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="display-card__action">Display öffnen</div>
|
||||||
|
</a>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
showError(msg) {
|
showError(msg) {
|
||||||
this.loadingOverlay.classList.add('hidden');
|
this.loadingOverlay.classList.add('hidden');
|
||||||
this.errorOverlay.classList.remove('hidden');
|
this.errorOverlay.classList.remove('hidden');
|
||||||
this.errorMessage.textContent = msg;
|
this.errorMessage.textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
escapeHtml(value) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = value ?? '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@
|
||||||
|
|
||||||
<flux:navlist.group :heading="__('Cabinet')" class="grid mb-4">
|
<flux:navlist.group :heading="__('Cabinet')" class="grid mb-4">
|
||||||
<flux:navlist.group expandable
|
<flux:navlist.group expandable
|
||||||
:expanded="request()->routeIs(['admin.cms.display-dashboard', 'admin.cms.display-media', 'admin.cms.display-modules', 'admin.cms.display-module-edit', 'admin.cms.display-versions', 'admin.cms.display-version-edit', 'admin.cms.displays', 'admin.cms.cabinet', 'admin.cms.cabinet-tablet'])"
|
:expanded="request()->routeIs(['admin.cms.display-dashboard', 'admin.cms.display-media', 'admin.cms.display-modules', 'admin.cms.display-module-edit', 'admin.cms.displays', 'admin.cms.cabinet', 'admin.cms.cabinet-tablet'])"
|
||||||
heading="Store Displays" class="grid">
|
heading="Store Displays" class="grid">
|
||||||
<flux:navlist.item icon="squares-2x2" :href="route('admin.cms.display-dashboard')"
|
<flux:navlist.item icon="squares-2x2" :href="route('admin.cms.display-dashboard')"
|
||||||
:current="request()->routeIs('admin.cms.display-dashboard')" wire:navigate>{{ __('Übersicht') }}
|
:current="request()->routeIs('admin.cms.display-dashboard')" wire:navigate>{{ __('Übersicht') }}
|
||||||
|
|
@ -192,7 +192,7 @@
|
||||||
:current="request()->routeIs('admin.cms.display-media')" wire:navigate>{{ __('Mediathek') }}
|
:current="request()->routeIs('admin.cms.display-media')" wire:navigate>{{ __('Mediathek') }}
|
||||||
</flux:navlist.item>
|
</flux:navlist.item>
|
||||||
<flux:navlist.item icon="rectangle-group" :href="route('admin.cms.display-modules')"
|
<flux:navlist.item icon="rectangle-group" :href="route('admin.cms.display-modules')"
|
||||||
:current="request()->routeIs(['admin.cms.display-modules', 'admin.cms.display-module-edit', 'admin.cms.display-versions', 'admin.cms.display-version-edit'])" wire:navigate>{{ __('Module') }}
|
:current="request()->routeIs(['admin.cms.display-modules', 'admin.cms.display-module-edit'])" wire:navigate>{{ __('Module') }}
|
||||||
</flux:navlist.item>
|
</flux:navlist.item>
|
||||||
<flux:navlist.item icon="tv" :href="route('admin.cms.displays')"
|
<flux:navlist.item icon="tv" :href="route('admin.cms.displays')"
|
||||||
:current="request()->routeIs('admin.cms.displays')" wire:navigate>{{ __('Displays') }}
|
:current="request()->routeIs('admin.cms.displays')" wire:navigate>{{ __('Displays') }}
|
||||||
|
|
|
||||||
|
|
@ -171,9 +171,9 @@ $tabletStatus = computed(function () {
|
||||||
Es besteht aus drei Bereichen, die Sie über die Kacheln oben erreichen:
|
Es besteht aus drei Bereichen, die Sie über die Kacheln oben erreichen:
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Mediathek</strong> - Zentrale Verwaltung aller Bilder und Videos fuer die Displays. Dateien bis 200 MB direkt hochladen oder groessere Videos als externe URL (Google Drive, OneDrive) einbinden.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Mediathek</strong> - Zentrale Verwaltung aller Bilder, SVG-Logos und Videos fuer die Displays. Dateien bis 200 MB direkt hochladen oder groessere Videos als externe URL (Google Drive, OneDrive) einbinden.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Module</strong> – Wiederverwendbare Content-Pakete, die auf den Displays abgespielt werden. Jede Modul hat einen bestimmten Typ und enthält passende Inhalte (Videos, Bilder oder Angebots-Slides).</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Module</strong> – Wiederverwendbare Content-Pakete, die auf den Displays abgespielt werden. Jedes Modul hat einen bestimmten Typ, passende Inhalte und eigene Meta-Einstellungen fuer Logo, Claim, Footer, QR-Code oder Theme.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Displays</strong> – Die physischen Bildschirme im Showroom. Jedem Display werden eine oder mehrere Module als Playlist zugewiesen.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Displays</strong> – Die physischen Bildschirme im Showroom. Pro Display gibt es einen Live-Stand und optional einen Entwurf, der separat vorbereitet, getestet und bewusst veröffentlicht wird.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Info-Tablet</strong> – Das Tablet an der Eingangstür des Showrooms. Hier verwalten Sie Öffnungszeiten, den aktuellen Store-Status und Hinweise für Besucher.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Info-Tablet</strong> – Das Tablet an der Eingangstür des Showrooms. Hier verwalten Sie Öffnungszeiten, den aktuellen Store-Status und Hinweise für Besucher.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -190,10 +190,10 @@ $tabletStatus = computed(function () {
|
||||||
Sie ist unabhängig von der Website-Mediathek (Flux CMS) und speziell auf die Anforderungen der Displays zugeschnitten.
|
Sie ist unabhängig von der Website-Mediathek (Flux CMS) und speziell auf die Anforderungen der Displays zugeschnitten.
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Direkt-Upload:</strong> Bilder und Videos bis 200 MB direkt per Drag-and-drop oder Dateiauswahl hochladen. Die Dateien werden auf dem Server gespeichert und stehen sofort zur Verfügung.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Direkt-Upload:</strong> Bilder, SVG-Dateien und Videos bis 200 MB direkt per Drag-and-drop oder Dateiauswahl hochladen. Die Dateien werden auf dem Server gespeichert und stehen sofort zur Verfügung.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Externe URLs:</strong> Für Videos über 200 MB (z. B. 4K-Showroom-Rundgänge) können Sie einen Freigabe-Link von Google Drive, OneDrive oder anderen Cloud-Diensten hinterlegen. Diese URL wird wie ein normales Medium in der Mediathek verwaltet und kann genauso in Module eingebunden werden.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Externe URLs:</strong> Für Videos über 200 MB (z. B. 4K-Showroom-Rundgänge) können Sie einen Freigabe-Link von Google Drive, OneDrive oder anderen Cloud-Diensten hinterlegen. Diese URL wird wie ein normales Medium in der Mediathek verwaltet und kann genauso in Module eingebunden werden.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Sammlungen:</strong> Ordnen Sie Medien in Sammlungen wie <em>immobilien</em>, <em>moebel</em> oder <em>brand</em>, um bei vielen Dateien den Überblick zu behalten.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Sammlungen:</strong> Ordnen Sie Medien in Sammlungen wie <em>immobilien</em>, <em>moebel</em> oder <em>brand</em>, um bei vielen Dateien den Überblick zu behalten.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medienauswahl im Editor:</strong> Beim Bearbeiten eines Moduls erscheint ein „Aus Mediathek"-Button. Darüber öffnen Sie die Medienauswahl und können bestehende Medien wählen oder direkt neue hochladen.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medienauswahl im Editor:</strong> Beim Bearbeiten eines Moduls erscheint ein „Aus Mediathek"-Button. Darüber öffnen Sie die Medienauswahl und können bestehende Medien wählen oder direkt neue Dateien inklusive SVG-Logos hochladen.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -211,19 +211,19 @@ $tabletStatus = computed(function () {
|
||||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||||
<li>
|
<li>
|
||||||
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Video-Display</strong> –
|
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Video-Display</strong> –
|
||||||
Für Video-Playlists mit optionalem Footer. Inhalte: <em>Videos</em> (Dateiname, Titel, Position/Ausschnitt) und <em>Footer-Zeilen</em> (Überschrift, Unterzeile, optionaler QR-Code-Link).
|
Für Video-Playlists mit optionalem Footer. Inhalte: <em>Videos</em> aus der Mediathek oder Legacy-Dateinamen, Position/Ausschnitt und <em>Footer-Zeilen</em> (Überschrift, Unterzeile, optionaler QR-Code-Link). Mediathek-URLs wie <code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-1 py-0.5 rounded">/storage/...</code> werden direkt abgespielt.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong class="font-medium text-zinc-800 dark:text-zinc-200">B2in Display</strong> –
|
<strong class="font-medium text-zinc-800 dark:text-zinc-200">B2in Display</strong> –
|
||||||
Für Medien-Rotation (Bilder und Videos) im Marken-Design. Inhalte: <em>Media-Items</em> mit Kategorie (Immobilien / Möbel), Überschrift, Unterzeile und Anzeigedauer. Unterstützt Light-/Dark-Theme.
|
Für Medien-Rotation (Bilder und Videos) im Marken-Design. Inhalte: <em>Media-Items</em> mit Kategorie (Immobilien / Möbel), Überschrift, Unterzeile und Anzeigedauer. Unterstützt Light-/Dark-Theme sowie zentrale Meta-Einstellungen für Header-Logo, Claim, Footer-Domain und QR-Code.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Angebote</strong> –
|
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Angebote</strong> –
|
||||||
Für Produkt-Slides im Angebotsformat. Verschiedene Slide-Layouts: Intro, Produkt-Hero, Produkt-Details und Impuls-Slides mit Preisen, Badges und QR-Codes.
|
Für Produkt-Slides im Angebotsformat. Verschiedene Slide-Layouts: Intro, Produkt-Hero, Produkt-Details und Impuls-Slides mit Preisen, Badges und QR-Codes. Logo, Brand-Text, Footer-Claim und Web-/QR-URL werden einmal am Modul gepflegt und automatisch von allen Slides übernommen.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
Innerhalb eines Moduls können Sie beliebig viele Inhalte anlegen, die Reihenfolge per Hoch/Runter-Sortierung festlegen und einzelne Einträge aktivieren oder deaktivieren.
|
Innerhalb eines Moduls können Sie beliebig viele Inhalte anlegen, die Reihenfolge per Hoch/Runter-Sortierung festlegen und einzelne Einträge aktivieren oder deaktivieren. Der Modul-Editor zeigt Inline-Vorschaubilder, eine 9:16-Player-Vorschau und eine Vollbild-Vorschau. Im Slide-Bearbeiten-Dialog wird nur der aktuell bearbeitete Slide als Einzel-Vorschau gerendert.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -238,9 +238,11 @@ $tabletStatus = computed(function () {
|
||||||
Ein <strong class="font-medium text-zinc-800 dark:text-zinc-200">Display</strong> repräsentiert einen physischen Bildschirm im Showroom.
|
Ein <strong class="font-medium text-zinc-800 dark:text-zinc-200">Display</strong> repräsentiert einen physischen Bildschirm im Showroom.
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-2 ml-5 list-disc space-y-1">
|
<ul class="mt-2 ml-5 list-disc space-y-1">
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul-Zuweisung:</strong> Jedem Display können Sie eine oder mehrere Module zuordnen. Die Module werden in der festgelegten Reihenfolge als Playlist abgespielt.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Live und Entwurf:</strong> Jedes Display zeigt den veröffentlichten Live-Stand und optional einen Entwurf. Entwürfe können aus Live angelegt, separat bearbeitet, verworfen oder veröffentlicht werden.</li>
|
||||||
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul-Zuweisung:</strong> Jedem Live- oder Entwurfsstand können Sie eine oder mehrere Module zuordnen. Die Module werden in der festgelegten Reihenfolge als Playlist abgespielt.</li>
|
||||||
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Vorschau:</strong> Live- und Entwurfs-URLs sind direkt kopierbar. Entwürfe und Module können zusätzlich im 9:16-Iframe oder im Vollbild geprüft werden.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Aktiv/Inaktiv:</strong> Über den Aktiv-Status können Sie einzelne Displays vorübergehend deaktivieren, ohne die Konfiguration zu verlieren.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Aktiv/Inaktiv:</strong> Über den Aktiv-Status können Sie einzelne Displays vorübergehend deaktivieren, ohne die Konfiguration zu verlieren.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">API-Anbindung:</strong> Jedes Display ruft seine Inhalte über eine JSON-API ab (<code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-1 py-0.5 rounded">/api/display/{id}/config</code>). Änderungen werden beim nächsten Abruf automatisch übernommen.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">API-Anbindung:</strong> Jedes Display ruft seine Live-Inhalte über eine JSON-API ab (<code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-1 py-0.5 rounded">/api/display/{id}/config</code>). Entwürfe laufen über Preview-Tokens, Module über eigene Preview-Endpunkte.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -282,10 +284,12 @@ $tabletStatus = computed(function () {
|
||||||
Typischer Workflow
|
Typischer Workflow
|
||||||
</flux:heading>
|
</flux:heading>
|
||||||
<ol class="mt-2 ml-5 list-decimal space-y-1">
|
<ol class="mt-2 ml-5 list-decimal space-y-1">
|
||||||
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medien hochladen</strong> – Bilder, SVG-Logos oder Videos in der Display-Mediathek ablegen.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul erstellen</strong> – Unter „Module" ein neues Modul mit passendem Typ anlegen (z. B. „Frühling 2026 Video").</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Modul erstellen</strong> – Unter „Module" ein neues Modul mit passendem Typ anlegen (z. B. „Frühling 2026 Video").</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Inhalte hinzufügen</strong> – Im Modul Videos, Medien oder Slides anlegen, Reihenfolge festlegen und aktivieren.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Meta-Einstellungen pflegen</strong> – Logo, Claim, Footer, QR-Code, Theme oder Anzeigezeiten einmal auf Modulebene setzen.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Display zuweisen</strong> – Unter „Displays" das Modul einem physischen Bildschirm zuordnen.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Inhalte hinzufügen</strong> – Im Modul Videos, Medien oder Slides anlegen, Reihenfolge festlegen, aktivieren und per Vorschau prüfen.</li>
|
||||||
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Fertig</strong> – Das Display lädt die neuen Inhalte automatisch über die API.</li>
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Display-Entwurf erstellen</strong> – Unter „Displays" aus dem Live-Stand einen Entwurf erzeugen und dort Module hinzufügen, sortieren oder entfernen.</li>
|
||||||
|
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Prüfen und veröffentlichen</strong> – Entwurf in der 9:16-Vorschau oder im Vollbild testen und anschließend bewusst veröffentlichen.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,29 @@
|
||||||
</x-success-alert>
|
</x-success-alert>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@php
|
||||||
|
$displayPlayerUrl = rtrim(config('display.player_url') ?: 'https://cabinet.b2in.eu/display', '/');
|
||||||
|
$displayOverviewUrl = $displayPlayerUrl.'/';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<flux:card class="mb-6 border-blue-200 bg-blue-50/70 dark:border-blue-500/30 dark:bg-blue-950/20">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">{{ __('Öffentliche Display-Übersicht') }}</flux:heading>
|
||||||
|
<flux:text class="mt-1">
|
||||||
|
{{ __('Hier sehen Sie alle aktiven Live-Displays und können die Wiedergabe direkt öffnen.') }}
|
||||||
|
</flux:text>
|
||||||
|
<div class="mt-2 text-xs font-mono text-blue-700 dark:text-blue-300">
|
||||||
|
{{ $displayOverviewUrl }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:button href="{{ $displayOverviewUrl }}" target="_blank" variant="primary" icon="arrow-top-right-on-square">
|
||||||
|
{{ __('Display-Übersicht öffnen') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</flux:card>
|
||||||
|
|
||||||
<flux:card>
|
<flux:card>
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -30,7 +53,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@foreach($displays as $display)
|
@foreach($displays as $display)
|
||||||
@php
|
@php
|
||||||
$liveDisplayUrl = url('/_cabinet/display/index.html').'?id='.$display->id;
|
$liveDisplayUrl = $displayPlayerUrl.'/?id='.$display->id;
|
||||||
$liveApiUrl = url('/api/display/'.$display->id.'/config');
|
$liveApiUrl = url('/api/display/'.$display->id.'/config');
|
||||||
@endphp
|
@endphp
|
||||||
<div wire:key="display-{{ $display->id }}"
|
<div wire:key="display-{{ $display->id }}"
|
||||||
|
|
@ -364,6 +387,7 @@
|
||||||
wire:key="draft-preview-{{ $previewFrameRefreshCounter }}"
|
wire:key="draft-preview-{{ $previewFrameRefreshCounter }}"
|
||||||
src="{{ $draftPreviewUrl }}"
|
src="{{ $draftPreviewUrl }}"
|
||||||
class="h-full w-full border-0"
|
class="h-full w-full border-0"
|
||||||
|
loading="lazy"
|
||||||
title="{{ __('Entwurfs-Vorschau') }}"
|
title="{{ __('Entwurfs-Vorschau') }}"
|
||||||
></iframe>
|
></iframe>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
wire:key="module-preview-{{ $previewFrameRefreshCounter }}"
|
wire:key="module-preview-{{ $previewFrameRefreshCounter }}"
|
||||||
src="{{ $this->modulePreviewUrl() }}"
|
src="{{ $this->modulePreviewUrl() }}"
|
||||||
class="h-full w-full border-0"
|
class="h-full w-full border-0"
|
||||||
|
loading="lazy"
|
||||||
title="{{ __('Modul-Vorschau') }}"
|
title="{{ __('Modul-Vorschau') }}"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -250,6 +251,7 @@
|
||||||
wire:key="item-modal-module-preview-{{ $previewFrameRefreshCounter }}"
|
wire:key="item-modal-module-preview-{{ $previewFrameRefreshCounter }}"
|
||||||
src="{{ $this->itemPreviewUrl() }}"
|
src="{{ $this->itemPreviewUrl() }}"
|
||||||
class="h-full w-full border-0"
|
class="h-full w-full border-0"
|
||||||
|
loading="lazy"
|
||||||
title="{{ __('Einzel-Vorschau im Bearbeiten-Dialog') }}"
|
title="{{ __('Einzel-Vorschau im Bearbeiten-Dialog') }}"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.pdf,.doc,.docx,.jpg,.jpeg,.png">
|
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.pdf,.doc,.docx,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone
|
<flux:file-upload.dropzone
|
||||||
heading="Dateien hochladen"
|
heading="Dateien hochladen"
|
||||||
text="Bilder (JPG, PNG, WebP, SVG) und Dokumente (PDF, DOC) — max. 10 MB pro Datei"
|
text="Bilder (JPG, PNG, WebP, SVG) und Dokumente (PDF, DOC) — max. 200 MB pro Datei"
|
||||||
with-progress />
|
with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,15 @@
|
||||||
</flux:badge>
|
</flux:badge>
|
||||||
<span class="font-semibold text-sm">{{ $item->content['title'] ?? $item->content['filename'] ?? '–' }}</span>
|
<span class="font-semibold text-sm">{{ $item->content['title'] ?? $item->content['filename'] ?? '–' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
|
@php
|
||||||
<span>{{ $item->content['filename'] ?? '–' }}</span>
|
$videoSource = $item->content['filename'] ?? '';
|
||||||
|
$isMediaLibrarySource = str_starts_with($videoSource, '/storage/') || str_starts_with($videoSource, 'http');
|
||||||
|
@endphp
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
|
||||||
|
<flux:badge size="sm" :color="$isMediaLibrarySource ? 'sky' : 'zinc'">
|
||||||
|
{{ $isMediaLibrarySource ? __('Mediathek') : __('Legacy-Datei') }}
|
||||||
|
</flux:badge>
|
||||||
|
<span class="truncate">{{ $videoSource ?: '–' }}</span>
|
||||||
<span>Position: {{ $item->content['position'] ?? 25 }}%</span>
|
<span>Position: {{ $item->content['position'] ?? 25 }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -272,16 +272,16 @@ new class extends Component
|
||||||
$rules['deliveryRadius'] = 'required|integer|min:1|max:500';
|
$rules['deliveryRadius'] = 'required|integer|min:1|max:500';
|
||||||
$rules['assemblyRadius'] = 'required|integer|min:1|max:500';
|
$rules['assemblyRadius'] = 'required|integer|min:1|max:500';
|
||||||
$rules['newTeamPhotos'] = 'nullable|array|max:10';
|
$rules['newTeamPhotos'] = 'nullable|array|max:10';
|
||||||
$rules['newTeamPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:10240';
|
$rules['newTeamPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:204800';
|
||||||
$rules['newShowroomPhotos'] = 'nullable|array|max:20';
|
$rules['newShowroomPhotos'] = 'nullable|array|max:20';
|
||||||
$rules['newShowroomPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:10240';
|
$rules['newShowroomPhotos.*'] = 'image|mimes:jpeg,jpg,png|max:204800';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isManufacturer()) {
|
if ($this->isManufacturer()) {
|
||||||
$rules['brandName'] = 'required|string|max:255';
|
$rules['brandName'] = 'required|string|max:255';
|
||||||
$rules['brandDescription'] = 'nullable|string|max:1000';
|
$rules['brandDescription'] = 'nullable|string|max:1000';
|
||||||
$rules['newBrandImages'] = 'nullable|array|max:10';
|
$rules['newBrandImages'] = 'nullable|array|max:10';
|
||||||
$rules['newBrandImages.*'] = 'image|mimes:jpeg,jpg,png|max:10240';
|
$rules['newBrandImages.*'] = 'image|mimes:jpeg,jpg,png|max:204800';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate($rules, [
|
$this->validate($rules, [
|
||||||
|
|
@ -305,11 +305,11 @@ new class extends Component
|
||||||
'foundedYear.min' => __('Das Gründungsjahr muss nach 1800 liegen.'),
|
'foundedYear.min' => __('Das Gründungsjahr muss nach 1800 liegen.'),
|
||||||
'foundedYear.max' => __('Das Gründungsjahr darf nicht in der Zukunft liegen.'),
|
'foundedYear.max' => __('Das Gründungsjahr darf nicht in der Zukunft liegen.'),
|
||||||
'newTeamPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
'newTeamPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
||||||
'newTeamPhotos.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
|
'newTeamPhotos.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
|
||||||
'newShowroomPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
'newShowroomPhotos.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
||||||
'newShowroomPhotos.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
|
'newShowroomPhotos.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
|
||||||
'newBrandImages.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
'newBrandImages.*.image' => __('Nur Bilder (JPG, PNG) erlaubt.'),
|
||||||
'newBrandImages.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
|
'newBrandImages.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$specialties = array_values(array_filter(
|
$specialties = array_values(array_filter(
|
||||||
|
|
@ -793,7 +793,7 @@ new class extends Component
|
||||||
<flux:card class="shadow-elegant">
|
<flux:card class="shadow-elegant">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:heading size="lg">{{ __('Team-Fotos') }}</flux:heading>
|
<flux:heading size="lg">{{ __('Team-Fotos') }}</flux:heading>
|
||||||
<flux:subheading>{{ __('Nur JPG/PNG – max. 10 MB pro Bild') }}</flux:subheading>
|
<flux:subheading>{{ __('Nur JPG/PNG – max. 200 MB pro Bild') }}</flux:subheading>
|
||||||
</div>
|
</div>
|
||||||
<flux:separator class="mb-6" />
|
<flux:separator class="mb-6" />
|
||||||
|
|
||||||
|
|
@ -867,7 +867,7 @@ new class extends Component
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<flux:file-upload wire:model="newTeamPhotos" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
<flux:file-upload wire:model="newTeamPhotos" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone heading="{{ __('Team-Fotos hochladen') }}" text="{{ __('JPEG oder PNG – max. 10 MB') }}" with-progress />
|
<flux:file-upload.dropzone heading="{{ __('Team-Fotos hochladen') }}" text="{{ __('JPEG oder PNG – max. 200 MB') }}" with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
@if (count($newTeamPhotos) > 0)
|
@if (count($newTeamPhotos) > 0)
|
||||||
|
|
@ -892,7 +892,7 @@ new class extends Component
|
||||||
<flux:card class="shadow-elegant">
|
<flux:card class="shadow-elegant">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:heading size="lg">{{ __('Showroom-Galerie') }}</flux:heading>
|
<flux:heading size="lg">{{ __('Showroom-Galerie') }}</flux:heading>
|
||||||
<flux:subheading>{{ __('Bilder Ihres Showrooms für das öffentliche Profil – nur JPG/PNG, max. 10 MB') }}</flux:subheading>
|
<flux:subheading>{{ __('Bilder Ihres Showrooms für das öffentliche Profil – nur JPG/PNG, max. 200 MB') }}</flux:subheading>
|
||||||
</div>
|
</div>
|
||||||
<flux:separator class="mb-6" />
|
<flux:separator class="mb-6" />
|
||||||
|
|
||||||
|
|
@ -966,7 +966,7 @@ new class extends Component
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<flux:file-upload wire:model="newShowroomPhotos" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
<flux:file-upload wire:model="newShowroomPhotos" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone heading="{{ __('Showroom-Bilder hochladen') }}" text="{{ __('JPEG oder PNG – max. 10 MB') }}" with-progress />
|
<flux:file-upload.dropzone heading="{{ __('Showroom-Bilder hochladen') }}" text="{{ __('JPEG oder PNG – max. 200 MB') }}" with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
@if (count($newShowroomPhotos) > 0)
|
@if (count($newShowroomPhotos) > 0)
|
||||||
|
|
@ -993,7 +993,7 @@ new class extends Component
|
||||||
<flux:card class="shadow-elegant">
|
<flux:card class="shadow-elegant">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:heading size="lg">{{ __('Marken-Bilder') }}</flux:heading>
|
<flux:heading size="lg">{{ __('Marken-Bilder') }}</flux:heading>
|
||||||
<flux:subheading>{{ __('Bilder für Ihre Marken-Präsentation (Atmosphäre, Brand-Story etc.) – nur JPG/PNG, max. 10 MB') }}</flux:subheading>
|
<flux:subheading>{{ __('Bilder für Ihre Marken-Präsentation (Atmosphäre, Brand-Story etc.) – nur JPG/PNG, max. 200 MB') }}</flux:subheading>
|
||||||
</div>
|
</div>
|
||||||
<flux:separator class="mb-6" />
|
<flux:separator class="mb-6" />
|
||||||
|
|
||||||
|
|
@ -1067,7 +1067,7 @@ new class extends Component
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<flux:file-upload wire:model="newBrandImages" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
<flux:file-upload wire:model="newBrandImages" multiple accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone heading="{{ __('Marken-Bilder hochladen') }}" text="{{ __('JPEG oder PNG – max. 10 MB') }}" with-progress />
|
<flux:file-upload.dropzone heading="{{ __('Marken-Bilder hochladen') }}" text="{{ __('JPEG oder PNG – max. 200 MB') }}" with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
@if (count($newBrandImages) > 0)
|
@if (count($newBrandImages) > 0)
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,7 @@ new class extends Component
|
||||||
'status' => 'required|in:active,draft',
|
'status' => 'required|in:active,draft',
|
||||||
// Bilder
|
// Bilder
|
||||||
'mainImages' => 'nullable|array|min:0|max:10',
|
'mainImages' => 'nullable|array|min:0|max:10',
|
||||||
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
|
'mainImages.*' => 'mimes:jpeg,jpg,png|max:204800',
|
||||||
// Maße & Material
|
// Maße & Material
|
||||||
'widthCm' => 'nullable|integer|min:1',
|
'widthCm' => 'nullable|integer|min:1',
|
||||||
'heightCm' => 'nullable|integer|min:1',
|
'heightCm' => 'nullable|integer|min:1',
|
||||||
|
|
@ -546,7 +546,7 @@ new class extends Component
|
||||||
'priceDisplayText.required_if' => __('Bei Ab-Preis ist eine Preisangabe erforderlich (z.B. "Ab 2.500 €").'),
|
'priceDisplayText.required_if' => __('Bei Ab-Preis ist eine Preisangabe erforderlich (z.B. "Ab 2.500 €").'),
|
||||||
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
|
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
|
||||||
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
|
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
|
||||||
'mainImages.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
|
'mainImages.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
|
||||||
'sku.unique' => __('Diese Artikelnummer ist bereits vergeben.'),
|
'sku.unique' => __('Diese Artikelnummer ist bereits vergeben.'),
|
||||||
'sellingPrice.min' => __('Der Verkaufspreis muss größer als 0 sein.'),
|
'sellingPrice.min' => __('Der Verkaufspreis muss größer als 0 sein.'),
|
||||||
'countryOfOrigin.size' => __('Bitte geben Sie einen gültigen 2-stelligen ISO-Ländercode ein (z.B. DE).'),
|
'countryOfOrigin.size' => __('Bitte geben Sie einen gültigen 2-stelligen ISO-Ländercode ein (z.B. DE).'),
|
||||||
|
|
@ -1229,7 +1229,7 @@ new class extends Component
|
||||||
<flux:card class="shadow-elegant">
|
<flux:card class="shadow-elegant">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:heading size="lg">{{ __('Produktbilder') }}</flux:heading>
|
<flux:heading size="lg">{{ __('Produktbilder') }}</flux:heading>
|
||||||
<flux:subheading>{{ __('Nur Bilder (JPG, JPEG, PNG) – max. 10 MB pro Bild, max. 10 Bilder') }}
|
<flux:subheading>{{ __('Nur Bilder (JPG, JPEG, PNG) – max. 200 MB pro Bild, max. 10 Bilder') }}
|
||||||
</flux:subheading>
|
</flux:subheading>
|
||||||
</div>
|
</div>
|
||||||
<flux:separator class="mb-6" />
|
<flux:separator class="mb-6" />
|
||||||
|
|
@ -1309,7 +1309,7 @@ new class extends Component
|
||||||
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
|
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
|
||||||
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone heading="{{ __('Bilder hochladen') }}"
|
<flux:file-upload.dropzone heading="{{ __('Bilder hochladen') }}"
|
||||||
text="{{ __('Nur JPEG oder PNG – max. 10 MB') }}" with-progress />
|
text="{{ __('Nur JPEG oder PNG – max. 200 MB') }}" with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
@if (isset($mainImages) && count($mainImages) > 0)
|
@if (isset($mainImages) && count($mainImages) > 0)
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ new class extends Component
|
||||||
'status' => 'required|in:active,draft',
|
'status' => 'required|in:active,draft',
|
||||||
'partnerProductNumber' => 'nullable|string|max:100',
|
'partnerProductNumber' => 'nullable|string|max:100',
|
||||||
'mainImages' => 'nullable|array|min:0|max:10',
|
'mainImages' => 'nullable|array|min:0|max:10',
|
||||||
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
|
'mainImages.*' => 'mimes:jpeg,jpg,png|max:204800',
|
||||||
];
|
];
|
||||||
|
|
||||||
$messages = [
|
$messages = [
|
||||||
|
|
@ -250,7 +250,7 @@ new class extends Component
|
||||||
'categoryId.required' => __('Bitte wählen Sie eine Kategorie aus.'),
|
'categoryId.required' => __('Bitte wählen Sie eine Kategorie aus.'),
|
||||||
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
|
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
|
||||||
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
|
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
|
||||||
'mainImages.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
|
'mainImages.*.max' => __('Bilder dürfen maximal 200 MB groß sein.'),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($isAdminWithoutPartner) {
|
if ($isAdminWithoutPartner) {
|
||||||
|
|
@ -459,7 +459,7 @@ new class extends Component
|
||||||
<flux:card class="shadow-elegant">
|
<flux:card class="shadow-elegant">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:heading size="lg">{{ $isEditing ? __('Produktbilder') : __('Produktbild') }}</flux:heading>
|
<flux:heading size="lg">{{ $isEditing ? __('Produktbilder') : __('Produktbild') }}</flux:heading>
|
||||||
<flux:subheading>{{ __('Nur Bilder (JPG, JPEG, PNG) – max. 10 MB pro Bild, max. 10 Bilder') }}
|
<flux:subheading>{{ __('Nur Bilder (JPG, JPEG, PNG) – max. 200 MB pro Bild, max. 10 Bilder') }}
|
||||||
</flux:subheading>
|
</flux:subheading>
|
||||||
</div>
|
</div>
|
||||||
<flux:separator class="mb-6" />
|
<flux:separator class="mb-6" />
|
||||||
|
|
@ -539,7 +539,7 @@ new class extends Component
|
||||||
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
|
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
|
||||||
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||||
<flux:file-upload.dropzone heading="{{ __('Bilder hochladen') }}"
|
<flux:file-upload.dropzone heading="{{ __('Bilder hochladen') }}"
|
||||||
text="{{ __('Nur JPEG oder PNG – max. 10 MB') }}" with-progress />
|
text="{{ __('Nur JPEG oder PNG – max. 200 MB') }}" with-progress />
|
||||||
</flux:file-upload>
|
</flux:file-upload>
|
||||||
|
|
||||||
@if (isset($mainImages) && count($mainImages) > 0)
|
@if (isset($mainImages) && count($mainImages) > 0)
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,6 @@ Route::middleware(['auth', 'partner.setup'])->group(function () {
|
||||||
// Display CMS
|
// Display CMS
|
||||||
Volt::route('admin/cms/display-dashboard', 'admin.cms.display-dashboard')->name('admin.cms.display-dashboard');
|
Volt::route('admin/cms/display-dashboard', 'admin.cms.display-dashboard')->name('admin.cms.display-dashboard');
|
||||||
Volt::route('admin/cms/display-media', 'admin.cms.display-media-library')->name('admin.cms.display-media');
|
Volt::route('admin/cms/display-media', 'admin.cms.display-media-library')->name('admin.cms.display-media');
|
||||||
Route::redirect('admin/cms/display-versions', 'admin/cms/display-modules', 301)->name('admin.cms.display-versions');
|
|
||||||
Route::get('admin/cms/display-versions/{displayVersion}/edit', function (\App\Models\DisplayVersion $displayVersion) {
|
|
||||||
return redirect()->route('admin.cms.display-module-edit', $displayVersion, 301);
|
|
||||||
})->name('admin.cms.display-version-edit');
|
|
||||||
Route::get('admin/cms/display-modules', \App\Livewire\Admin\Cms\DisplayVersionList::class)->name('admin.cms.display-modules');
|
Route::get('admin/cms/display-modules', \App\Livewire\Admin\Cms\DisplayVersionList::class)->name('admin.cms.display-modules');
|
||||||
Route::get('admin/cms/display-modules/{displayVersion}/edit', \App\Livewire\Admin\Cms\DisplayVersionEditor::class)->name('admin.cms.display-module-edit');
|
Route::get('admin/cms/display-modules/{displayVersion}/edit', \App\Livewire\Admin\Cms\DisplayVersionEditor::class)->name('admin.cms.display-module-edit');
|
||||||
Route::get('admin/cms/displays', \App\Livewire\Admin\Cms\DisplayList::class)->name('admin.cms.displays');
|
Route::get('admin/cms/displays', \App\Livewire\Admin\Cms\DisplayList::class)->name('admin.cms.displays');
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ Route::domain($domainPortal)->group(function () {
|
||||||
Route::get('/api/cabinet-tablet/check', [\App\Http\Controllers\Api\CabinetTabletController::class, 'check']);
|
Route::get('/api/cabinet-tablet/check', [\App\Http\Controllers\Api\CabinetTabletController::class, 'check']);
|
||||||
|
|
||||||
// Display Version API (per physical display)
|
// Display Version API (per physical display)
|
||||||
|
Route::get('/api/display/overview', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'overview']);
|
||||||
Route::get('/api/display/{display}/config', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'config']);
|
Route::get('/api/display/{display}/config', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'config']);
|
||||||
Route::get('/api/display/{display}/check', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'check']);
|
Route::get('/api/display/{display}/check', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'check']);
|
||||||
Route::get('/api/display/preview/{token}', [\App\Http\Controllers\Api\DisplayPreviewController::class, 'config']);
|
Route::get('/api/display/preview/{token}', [\App\Http\Controllers\Api\DisplayPreviewController::class, 'config']);
|
||||||
|
|
@ -127,6 +128,7 @@ Route::get('/api/cabinet-tablet/status', [\App\Http\Controllers\Api\CabinetTable
|
||||||
Route::get('/api/cabinet-tablet/check', [\App\Http\Controllers\Api\CabinetTabletController::class, 'check']);
|
Route::get('/api/cabinet-tablet/check', [\App\Http\Controllers\Api\CabinetTabletController::class, 'check']);
|
||||||
|
|
||||||
// Fallback: Display Version API
|
// Fallback: Display Version API
|
||||||
|
Route::get('/api/display/overview', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'overview']);
|
||||||
Route::get('/api/display/{display}/config', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'config']);
|
Route::get('/api/display/{display}/config', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'config']);
|
||||||
Route::get('/api/display/{display}/check', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'check']);
|
Route::get('/api/display/{display}/check', [\App\Http\Controllers\Api\DisplayVersionApiController::class, 'check']);
|
||||||
Route::get('/api/display/preview/{token}', [\App\Http\Controllers\Api\DisplayPreviewController::class, 'config']);
|
Route::get('/api/display/preview/{token}', [\App\Http\Controllers\Api\DisplayPreviewController::class, 'config']);
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,10 @@ test('media picker zeigt ausgewähltes medium ohne livewire property fehler', fu
|
||||||
->assertSee('test-image.jpg')
|
->assertSee('test-image.jpg')
|
||||||
->assertDontSee('Kein Medium ausgewählt');
|
->assertDontSee('Kein Medium ausgewählt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cms media picker und library uploader nutzen 200 mb dateigrenze', function () {
|
||||||
|
expect(file_get_contents(app_path('Livewire/Admin/Cms/MediaPicker.php')))
|
||||||
|
->toContain('max:204800')
|
||||||
|
->and(file_get_contents(app_path('Livewire/Admin/Cms/MediaLibraryUploader.php')))
|
||||||
|
->toContain('max:204800');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,6 @@ test('can assign versions to a display', function () {
|
||||||
->call('save');
|
->call('save');
|
||||||
|
|
||||||
$display->refresh();
|
$display->refresh();
|
||||||
expect($display->versions)->toHaveCount(2);
|
|
||||||
expect($display->versions->first()->id)->toBe($version1->id);
|
|
||||||
expect($display->versions->last()->id)->toBe($version2->id);
|
|
||||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version1->id, $version2->id]);
|
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version1->id, $version2->id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -100,8 +97,6 @@ test('can reorder versions in playlist', function () {
|
||||||
->call('save');
|
->call('save');
|
||||||
|
|
||||||
$display->refresh();
|
$display->refresh();
|
||||||
expect($display->versions->first()->id)->toBe($version2->id);
|
|
||||||
expect($display->versions->last()->id)->toBe($version1->id);
|
|
||||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version2->id, $version1->id]);
|
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version2->id, $version1->id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -110,10 +105,7 @@ test('can remove version from playlist', function () {
|
||||||
$version1 = DisplayVersion::factory()->create();
|
$version1 = DisplayVersion::factory()->create();
|
||||||
$version2 = DisplayVersion::factory()->create();
|
$version2 = DisplayVersion::factory()->create();
|
||||||
$display = Display::factory()->create();
|
$display = Display::factory()->create();
|
||||||
$display->versions()->attach([
|
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$version1->id, $version2->id]);
|
||||||
$version1->id => ['sort_order' => 0],
|
|
||||||
$version2->id => ['sort_order' => 1],
|
|
||||||
]);
|
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(DisplayList::class)
|
->test(DisplayList::class)
|
||||||
|
|
@ -122,8 +114,6 @@ test('can remove version from playlist', function () {
|
||||||
->call('save');
|
->call('save');
|
||||||
|
|
||||||
$display->refresh();
|
$display->refresh();
|
||||||
expect($display->versions)->toHaveCount(1);
|
|
||||||
expect($display->versions->first()->id)->toBe($version2->id);
|
|
||||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version2->id]);
|
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version2->id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -233,7 +223,6 @@ test('can publish a draft playlist over the live playlist', function () {
|
||||||
|
|
||||||
expect($display->draftPlaylist)->toBeNull();
|
expect($display->draftPlaylist)->toBeNull();
|
||||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id]);
|
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id]);
|
||||||
expect($display->versions->pluck('id')->all())->toBe([$draftVersion->id]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can edit live playlist without changing draft playlist', function () {
|
test('can edit live playlist without changing draft playlist', function () {
|
||||||
|
|
@ -277,7 +266,6 @@ test('can edit draft playlist without changing live playlist', function () {
|
||||||
expect($display->preview_token)->not->toBeNull();
|
expect($display->preview_token)->not->toBeNull();
|
||||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$liveVersion->id]);
|
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$liveVersion->id]);
|
||||||
expect($display->draftPlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id, $newDraftVersion->id]);
|
expect($display->draftPlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id, $newDraftVersion->id]);
|
||||||
expect($display->versions->pluck('id')->all())->toBe([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('draft editor renders iframe preview url', function () {
|
test('draft editor renders iframe preview url', function () {
|
||||||
|
|
@ -354,9 +342,26 @@ test('renders live and draft playlist columns', function () {
|
||||||
->test(DisplayList::class)
|
->test(DisplayList::class)
|
||||||
->assertSee('Live Modul')
|
->assertSee('Live Modul')
|
||||||
->assertSee('Draft Modul')
|
->assertSee('Draft Modul')
|
||||||
|
->assertSee('Öffentliche Display-Übersicht')
|
||||||
|
->assertSee('Display-Übersicht öffnen')
|
||||||
|
->assertSee('https://cabinet.b2in.eu/display/', false)
|
||||||
->assertSee('Live bearbeiten')
|
->assertSee('Live bearbeiten')
|
||||||
->assertSee('Live-URL zum Kopieren')
|
->assertSee('Live-URL zum Kopieren')
|
||||||
->assertSee(url('/_cabinet/display/index.html').'?id='.$display->id, false)
|
->assertSee('https://cabinet.b2in.eu/display/?id='.$display->id, false)
|
||||||
->assertSee('Entwurf bearbeiten')
|
->assertSee('Entwurf bearbeiten')
|
||||||
->assertSee('Test-Display');
|
->assertSee('Test-Display');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('display live urls fall back to cabinet domain when config is empty', function () {
|
||||||
|
config(['display.player_url' => '']);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$version = DisplayVersion::factory()->create(['name' => 'Live Modul']);
|
||||||
|
$display = Display::factory()->create();
|
||||||
|
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$version->id]);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(DisplayList::class)
|
||||||
|
->assertSee('https://cabinet.b2in.eu/display/', false)
|
||||||
|
->assertSee('https://cabinet.b2in.eu/display/?id='.$display->id, false);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ use App\Models\Display;
|
||||||
use App\Models\DisplayPlaylist;
|
use App\Models\DisplayPlaylist;
|
||||||
use App\Models\DisplayPlaylistItem;
|
use App\Models\DisplayPlaylistItem;
|
||||||
use App\Models\DisplayVersion;
|
use App\Models\DisplayVersion;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
it('creates the display_playlists table with unique status per display', function () {
|
it('creates the display_playlists table with unique status per display', function () {
|
||||||
|
|
@ -27,64 +26,8 @@ it('adds is_test and preview_token to displays', function () {
|
||||||
expect(Schema::hasColumns('displays', ['is_test', 'preview_token']))->toBeTrue();
|
expect(Schema::hasColumns('displays', ['is_test', 'preview_token']))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('migrates existing pivot entries into a published playlist with same ordering', function () {
|
it('drops the legacy display version pivot table', function () {
|
||||||
$display = Display::factory()->create();
|
expect(Schema::hasTable('display_display_version'))->toBeFalse();
|
||||||
$moduleA = DisplayVersion::factory()->create(['name' => 'Modul A']);
|
|
||||||
$moduleB = DisplayVersion::factory()->create(['name' => 'Modul B']);
|
|
||||||
|
|
||||||
DB::table('display_playlists')->where('display_id', $display->id)->delete();
|
|
||||||
|
|
||||||
DB::table('display_display_version')->insert([
|
|
||||||
['display_id' => $display->id, 'display_version_id' => $moduleA->id, 'sort_order' => 0],
|
|
||||||
['display_id' => $display->id, 'display_version_id' => $moduleB->id, 'sort_order' => 1],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$migration = require database_path('migrations/2026_05_11_113330_migrate_pivot_to_display_playlists.php');
|
|
||||||
$migration->up();
|
|
||||||
|
|
||||||
$playlist = DisplayPlaylist::query()
|
|
||||||
->where('display_id', $display->id)
|
|
||||||
->where('status', DisplayPlaylist::STATUS_PUBLISHED)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
expect($playlist)->not->toBeNull();
|
|
||||||
expect($playlist->published_at)->not->toBeNull();
|
|
||||||
|
|
||||||
$orderedIds = $playlist->items()->pluck('display_version_id')->all();
|
|
||||||
expect($orderedIds)->toBe([$moduleA->id, $moduleB->id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is idempotent and does not duplicate published playlists on re-run', function () {
|
|
||||||
$display = Display::factory()->create();
|
|
||||||
$module = DisplayVersion::factory()->create();
|
|
||||||
|
|
||||||
DB::table('display_playlists')->where('display_id', $display->id)->delete();
|
|
||||||
|
|
||||||
DB::table('display_display_version')->insert([
|
|
||||||
['display_id' => $display->id, 'display_version_id' => $module->id, 'sort_order' => 0],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$migration = require database_path('migrations/2026_05_11_113330_migrate_pivot_to_display_playlists.php');
|
|
||||||
$migration->up();
|
|
||||||
$migration->up();
|
|
||||||
|
|
||||||
$count = DisplayPlaylist::query()
|
|
||||||
->where('display_id', $display->id)
|
|
||||||
->where('status', DisplayPlaylist::STATUS_PUBLISHED)
|
|
||||||
->count();
|
|
||||||
|
|
||||||
expect($count)->toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not break the legacy versions() relation', function () {
|
|
||||||
$display = Display::factory()->create();
|
|
||||||
$module = DisplayVersion::factory()->create();
|
|
||||||
|
|
||||||
DB::table('display_display_version')->insert([
|
|
||||||
['display_id' => $display->id, 'display_version_id' => $module->id, 'sort_order' => 0],
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect($display->fresh()->versions)->toHaveCount(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exposes a live playlist relation and a draft playlist relation on display', function () {
|
it('exposes a live playlist relation and a draft playlist relation on display', function () {
|
||||||
|
|
|
||||||
|
|
@ -214,14 +214,34 @@ test('check endpoint returns only updated_at', function () {
|
||||||
$response->assertJsonStructure(['updated_at']);
|
$response->assertJsonStructure(['updated_at']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('display config ignores legacy pivot and reads published playlist', function () {
|
test('display overview lists active displays with published modules', function () {
|
||||||
$legacyVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Legacy']);
|
$liveVersion = DisplayVersion::factory()->create(['name' => 'Live Module']);
|
||||||
DisplayVersionItem::factory()->create([
|
$activeDisplay = Display::factory()->create([
|
||||||
'display_version_id' => $legacyVersion->id,
|
'name' => 'Showroom Eingang',
|
||||||
'item_type' => 'video',
|
'location' => 'Bielefeld',
|
||||||
'content' => ['filename' => 'legacy.mp4', 'title' => 'Legacy', 'position' => 25],
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
publishDisplayModules($activeDisplay, [$liveVersion->id]);
|
||||||
|
|
||||||
|
$inactiveDisplay = Display::factory()->create(['is_active' => false]);
|
||||||
|
publishDisplayModules($inactiveDisplay, [$liveVersion->id]);
|
||||||
|
|
||||||
|
Display::factory()->create(['name' => 'Ohne Live']);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/display/overview');
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonCount(1, 'displays')
|
||||||
|
->assertJsonPath('displays.0.id', $activeDisplay->id)
|
||||||
|
->assertJsonPath('displays.0.name', 'Showroom Eingang')
|
||||||
|
->assertJsonPath('displays.0.location', 'Bielefeld')
|
||||||
|
->assertJsonPath('displays.0.is_active', true)
|
||||||
|
->assertJsonPath('displays.0.is_live', true)
|
||||||
|
->assertJsonPath('displays.0.module_count', 1)
|
||||||
|
->assertJsonPath('displays.0.url', 'https://cabinet.b2in.eu/display/?id='.$activeDisplay->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('display config reads published playlist', function () {
|
||||||
$publishedVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Published']);
|
$publishedVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Published']);
|
||||||
DisplayVersionItem::factory()->create([
|
DisplayVersionItem::factory()->create([
|
||||||
'display_version_id' => $publishedVersion->id,
|
'display_version_id' => $publishedVersion->id,
|
||||||
|
|
@ -230,7 +250,6 @@ test('display config ignores legacy pivot and reads published playlist', functio
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$display = Display::factory()->create();
|
$display = Display::factory()->create();
|
||||||
$display->versions()->attach($legacyVersion->id, ['sort_order' => 0]);
|
|
||||||
publishDisplayModules($display, [$publishedVersion->id]);
|
publishDisplayModules($display, [$publishedVersion->id]);
|
||||||
|
|
||||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||||
|
|
@ -391,6 +410,11 @@ test('display player keeps previews in a strict 9 by 16 viewport', function () {
|
||||||
->toContain('width: min(100vw, calc(100vh * 9 / 16));')
|
->toContain('width: min(100vw, calc(100vh * 9 / 16));')
|
||||||
->toContain('height: min(100vh, calc(100vw * 16 / 9));')
|
->toContain('height: min(100vh, calc(100vw * 16 / 9));')
|
||||||
->toContain('container-type: size;')
|
->toContain('container-type: size;')
|
||||||
|
->toContain("if (hostname === 'cabinet.b2in.eu') {")
|
||||||
|
->toContain("return 'https://portal.b2in.eu';")
|
||||||
|
->toContain('/api/display/overview')
|
||||||
|
->toContain('Aktive Live-Displays')
|
||||||
|
->toContain('renderOverview(data.displays || [])')
|
||||||
->toContain('translate(${offsetX}px, ${offsetY}px) scale(${scale})')
|
->toContain('translate(${offsetX}px, ${offsetY}px) scale(${scale})')
|
||||||
->toContain('this.settings.logo_url')
|
->toContain('this.settings.logo_url')
|
||||||
->toContain('this.settings.footer_claim')
|
->toContain('this.settings.footer_claim')
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,20 @@ test('display version list renders for authenticated users', function () {
|
||||||
$response->assertSeeLivewire(DisplayVersionList::class);
|
$response->assertSeeLivewire(DisplayVersionList::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('display dashboard documentation describes current workflow', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$this->get(route('admin.cms.display-dashboard'))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Live und Entwurf')
|
||||||
|
->assertSee('Meta-Einstellungen pflegen')
|
||||||
|
->assertSee('SVG-Logos')
|
||||||
|
->assertSee('/storage/...')
|
||||||
|
->assertSee('Entwurf in der 9:16-Vorschau');
|
||||||
|
});
|
||||||
|
|
||||||
test('can create a display version', function () {
|
test('can create a display version', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
|
@ -104,19 +118,6 @@ test('display version editor renders with correct version data', function () {
|
||||||
$response->assertSeeLivewire(DisplayVersionEditor::class);
|
$response->assertSeeLivewire(DisplayVersionEditor::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('old display version routes redirect to module routes', function () {
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$version = DisplayVersion::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
$this->get(route('admin.cms.display-versions'))
|
|
||||||
->assertRedirect(route('admin.cms.display-modules'));
|
|
||||||
|
|
||||||
$this->get(route('admin.cms.display-version-edit', $version))
|
|
||||||
->assertRedirect(route('admin.cms.display-module-edit', $version));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('display module editor renders module preview', function () {
|
test('display module editor renders module preview', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$version = DisplayVersion::factory()->create(['name' => 'Preview Modul']);
|
$version = DisplayVersion::factory()->create(['name' => 'Preview Modul']);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue