From 92621323251b0579254f5e9e998eefec37d0cb6d Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Wed, 13 May 2026 14:34:08 +0200 Subject: [PATCH 1/2] Display Module 13-05-2026 --- .devcontainer/docker-compose.dev.yml | 2 +- .devcontainer/php-upload-limits.ini | 2 + .../Commands/MigrateLegacyDisplays.php | 13 +- .../Api/DisplayVersionApiController.php | 29 ++++ app/Livewire/Admin/Cms/DisplayList.php | 21 +-- .../Admin/Cms/DisplayVersionEditor.php | 42 +----- app/Livewire/Admin/Cms/DisplayVersionList.php | 39 ++---- .../Admin/Cms/MediaLibraryUploader.php | 2 +- app/Livewire/Admin/Cms/MediaPicker.php | 2 +- app/Models/Display.php | 35 ----- app/Models/DisplayVersion.php | 9 +- app/Services/DisplayPlaylistConfigBuilder.php | 34 +---- app/Support/DisplayModuleSettings.php | 61 ++++++++ config/display.php | 3 + ...600_drop_display_display_version_table.php | 26 ++++ .../00-entwicklungskonzept.md | 37 ++--- dev/displays-11-05-2026/01-status.md | 38 ++++- docker-compose.yml | 2 +- .../cms/media-library-uploader.blade.php | 2 +- .../core/src/Helpers/MediaLibraryUploader.php | 2 +- .../flux-cms/core/src/Helpers/MediaPicker.php | 2 +- .../core/src/Helpers/MediaUploader.php | 2 +- public/_cabinet/_docs/QUICK_START.md | 4 +- .../_docs/VIDEO_OPTIMIZATION_README.md | 2 +- public/_cabinet/display/index.html | 130 +++++++++++++++++- .../components/layouts/app/sidebar.blade.php | 4 +- .../admin/cms/display-dashboard.blade.php | 32 +++-- .../livewire/admin/cms/display-list.blade.php | 26 +++- .../cms/display-version-editor.blade.php | 2 + .../cms/media-library-uploader.blade.php | 2 +- .../partials/version-editor-video.blade.php | 11 +- .../views/livewire/partner/my-data.blade.php | 24 ++-- .../livewire/products/form-standard.blade.php | 8 +- .../livewire/products/form-teaser.blade.php | 8 +- routes/admin.php | 4 - routes/domains.php | 2 + tests/Feature/Admin/Cms/MediaPickerTest.php | 7 + tests/Feature/DisplayListTest.php | 33 +++-- .../Feature/DisplayPlaylistMigrationTest.php | 61 +------- tests/Feature/DisplayVersionApiTest.php | 38 ++++- tests/Feature/DisplayVersionTest.php | 27 ++-- 41 files changed, 496 insertions(+), 334 deletions(-) create mode 100644 app/Support/DisplayModuleSettings.php create mode 100644 database/migrations/2026_05_13_103600_drop_display_display_version_table.php diff --git a/.devcontainer/docker-compose.dev.yml b/.devcontainer/docker-compose.dev.yml index df7d4f4..04f71ac 100644 --- a/.devcontainer/docker-compose.dev.yml +++ b/.devcontainer/docker-compose.dev.yml @@ -62,7 +62,7 @@ services: MYSQL_EXTRA_OPTIONS: --default-authentication-plugin=mysql_native_password volumes: - '../:/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: - sail depends_on: diff --git a/.devcontainer/php-upload-limits.ini b/.devcontainer/php-upload-limits.ini index ed99be6..4a13806 100644 --- a/.devcontainer/php-upload-limits.ini +++ b/.devcontainer/php-upload-limits.ini @@ -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 post_max_size = 210M diff --git a/app/Console/Commands/MigrateLegacyDisplays.php b/app/Console/Commands/MigrateLegacyDisplays.php index 2ea2dc0..4ddcb64 100644 --- a/app/Console/Commands/MigrateLegacyDisplays.php +++ b/app/Console/Commands/MigrateLegacyDisplays.php @@ -4,6 +4,8 @@ namespace App\Console\Commands; use App\Models\Display; use App\Models\DisplayFooterContent; +use App\Models\DisplayPlaylist; +use App\Models\DisplayPlaylistItem; use App\Models\DisplayVersion; use App\Models\DisplayVersionItem; use App\Models\DisplayVideo; @@ -75,7 +77,16 @@ class MigrateLegacyDisplays extends Command '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("Created version: {$version->name} (ID: {$version->id})"); diff --git a/app/Http/Controllers/Api/DisplayVersionApiController.php b/app/Http/Controllers/Api/DisplayVersionApiController.php index 37ca1ac..b101124 100644 --- a/app/Http/Controllers/Api/DisplayVersionApiController.php +++ b/app/Http/Controllers/Api/DisplayVersionApiController.php @@ -9,6 +9,35 @@ use Illuminate\Http\JsonResponse; 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 { if (! $display->is_active) { diff --git a/app/Livewire/Admin/Cms/DisplayList.php b/app/Livewire/Admin/Cms/DisplayList.php index dda18eb..1a32b5c 100644 --- a/app/Livewire/Admin/Cms/DisplayList.php +++ b/app/Livewire/Admin/Cms/DisplayList.php @@ -44,7 +44,7 @@ class DisplayList extends Component ], true) ? $playlistStatus : DisplayPlaylist::STATUS_PUBLISHED; 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->displayName = $display->name; $this->displayLocation = $display->location ?? ''; @@ -137,7 +137,6 @@ class DisplayList extends Component $this->previewFrameRefreshCounter++; } else { $this->syncPublishedPlaylist($display); - $this->syncLegacyPivot($display, $this->selectedVersionIds); } $this->closeModal(); @@ -202,24 +201,9 @@ class DisplayList extends Component return $display->draftPlaylist->fresh('modules'); }); - $this->syncLegacyPivot($display, $this->moduleIdsForPlaylist($publishedPlaylist)); - session()->flash('success', 'Entwurf wurde veröffentlicht.'); } - /** - * @param array $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 { $playlist = $display->playlists()->firstOrCreate( @@ -297,8 +281,7 @@ class DisplayList extends Component return $this->moduleIdsForPlaylist($display->draftPlaylist); } - return $this->moduleIdsForPlaylist($display->livePlaylist) - ?: $display->versions->pluck('id')->all(); + return $this->moduleIdsForPlaylist($display->livePlaylist); } public function deleteDisplay(int $id): void diff --git a/app/Livewire/Admin/Cms/DisplayVersionEditor.php b/app/Livewire/Admin/Cms/DisplayVersionEditor.php index c79d185..37d5561 100644 --- a/app/Livewire/Admin/Cms/DisplayVersionEditor.php +++ b/app/Livewire/Admin/Cms/DisplayVersionEditor.php @@ -5,6 +5,7 @@ namespace App\Livewire\Admin\Cms; use App\Enums\DisplayVersionType; use App\Models\DisplayVersion; use App\Models\DisplayVersionItem; +use App\Support\DisplayModuleSettings; use Illuminate\Support\Facades\File; use Livewire\Attributes\On; use Livewire\Component; @@ -480,46 +481,7 @@ class DisplayVersionEditor extends Component */ private function settingsWithDefaults(): array { - return array_replace_recursive($this->defaultSettings(), $this->version->settings ?? []); - } - - /** - * @return array - */ - 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, - ], - ], - }; + return DisplayModuleSettings::merge($this->version->type, $this->version->settings); } public function render() diff --git a/app/Livewire/Admin/Cms/DisplayVersionList.php b/app/Livewire/Admin/Cms/DisplayVersionList.php index 4a0ceb4..dad96a1 100644 --- a/app/Livewire/Admin/Cms/DisplayVersionList.php +++ b/app/Livewire/Admin/Cms/DisplayVersionList.php @@ -4,6 +4,8 @@ namespace App\Livewire\Admin\Cms; use App\Enums\DisplayVersionType; use App\Models\DisplayVersion; +use App\Support\DisplayModuleSettings; +use Illuminate\Support\Facades\DB; use Livewire\Component; class DisplayVersionList extends Component @@ -70,40 +72,17 @@ class DisplayVersionList extends Component */ private function defaultSettingsForType(string $type): array { - return match ($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 => [], - }; + return DisplayModuleSettings::defaults($type); } 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') ->get(); diff --git a/app/Livewire/Admin/Cms/MediaLibraryUploader.php b/app/Livewire/Admin/Cms/MediaLibraryUploader.php index 8b495b6..90c1986 100644 --- a/app/Livewire/Admin/Cms/MediaLibraryUploader.php +++ b/app/Livewire/Admin/Cms/MediaLibraryUploader.php @@ -17,7 +17,7 @@ class MediaLibraryUploader extends Component { $this->validate([ '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); diff --git a/app/Livewire/Admin/Cms/MediaPicker.php b/app/Livewire/Admin/Cms/MediaPicker.php index 14eb146..ee4e9af 100644 --- a/app/Livewire/Admin/Cms/MediaPicker.php +++ b/app/Livewire/Admin/Cms/MediaPicker.php @@ -73,7 +73,7 @@ class MediaPicker extends Component { $this->validate([ '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); diff --git a/app/Models/Display.php b/app/Models/Display.php index 279ec51..f73082a 100644 --- a/app/Models/Display.php +++ b/app/Models/Display.php @@ -4,7 +4,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; 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 */ @@ -67,29 +55,6 @@ class Display extends Model ->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 { if (! $this->preview_token) { diff --git a/app/Models/DisplayVersion.php b/app/Models/DisplayVersion.php index 20cf372..539a805 100644 --- a/app/Models/DisplayVersion.php +++ b/app/Models/DisplayVersion.php @@ -6,7 +6,6 @@ use App\Enums\DisplayVersionType; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; class DisplayVersion extends Model @@ -35,10 +34,12 @@ class DisplayVersion extends Model return $this->hasMany(DisplayVersionItem::class)->orderBy('sort_order'); } - public function displays(): BelongsToMany + /** + * @return HasMany + */ + public function playlistItems(): HasMany { - return $this->belongsToMany(Display::class, 'display_display_version') - ->withPivot('sort_order'); + return $this->hasMany(DisplayPlaylistItem::class); } /** diff --git a/app/Services/DisplayPlaylistConfigBuilder.php b/app/Services/DisplayPlaylistConfigBuilder.php index ea05231..2163ed8 100644 --- a/app/Services/DisplayPlaylistConfigBuilder.php +++ b/app/Services/DisplayPlaylistConfigBuilder.php @@ -4,6 +4,7 @@ namespace App\Services; use App\Models\DisplayPlaylist; use App\Models\DisplayVersion; +use App\Support\DisplayModuleSettings; use Illuminate\Database\Eloquent\Collection; class DisplayPlaylistConfigBuilder @@ -95,9 +96,7 @@ class DisplayPlaylistConfigBuilder return [ 'type' => 'video-display', 'version_name' => $module->name, - 'settings' => array_replace([ - 'qr_label' => 'Website', - ], $module->settings ?? []), + 'settings' => DisplayModuleSettings::merge($module->type, $module->settings), 'videoPlaylist' => $videos, 'footerContent' => $footerContent, ]; @@ -133,20 +132,7 @@ class DisplayPlaylistConfigBuilder return [ 'type' => 'b2in', 'version_name' => $module->name, - 'settings' => array_replace_recursive([ - '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 ?? []), + 'settings' => DisplayModuleSettings::merge($module->type, $module->settings), 'items' => $mediaItems, ]; } @@ -180,19 +166,7 @@ class DisplayPlaylistConfigBuilder return [ 'type' => 'offers', 'version_name' => $module->name, - 'settings' => array_replace_recursive([ - '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 ?? []), + 'settings' => DisplayModuleSettings::merge($module->type, $module->settings), 'slides' => $slides, ]; } diff --git a/app/Support/DisplayModuleSettings.php b/app/Support/DisplayModuleSettings.php new file mode 100644 index 0000000..fc3bc1d --- /dev/null +++ b/app/Support/DisplayModuleSettings.php @@ -0,0 +1,61 @@ + + */ + 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|null $settings + * @return array + */ + public static function merge(DisplayVersionType|string $type, ?array $settings): array + { + return array_replace_recursive(self::defaults($type), $settings ?? []); + } +} diff --git a/config/display.php b/config/display.php index 9443e45..f1fff65 100644 --- a/config/display.php +++ b/config/display.php @@ -22,4 +22,7 @@ return [ // Haupt-Domain 'domain' => env('DISPLAY_DOMAIN', 'b2in.eu'), + // Öffentliche Player-URL der Display-Domain + 'player_url' => env('DISPLAY_PLAYER_URL', 'https://cabinet.b2in.eu/display'), + ]; diff --git a/database/migrations/2026_05_13_103600_drop_display_display_version_table.php b/database/migrations/2026_05_13_103600_drop_display_display_version_table.php new file mode 100644 index 0000000..cfb9b9a --- /dev/null +++ b/database/migrations/2026_05_13_103600_drop_display_display_version_table.php @@ -0,0 +1,26 @@ +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']); + }); + } +}; diff --git a/dev/displays-11-05-2026/00-entwicklungskonzept.md b/dev/displays-11-05-2026/00-entwicklungskonzept.md index a6fdce2..07d7534 100644 --- a/dev/displays-11-05-2026/00-entwicklungskonzept.md +++ b/dev/displays-11-05-2026/00-entwicklungskonzept.md @@ -16,8 +16,8 @@ Im Admin-Portal unter `portal.b2in.test/admin/cms/` existiert der Bereich **Stor |---|---| | `cms/display-dashboard` | Übersicht / Einstieg | | `cms/display-media` | Mediathek (eigene Display-Mediathek, getrennt von Flux CMS) | -| `cms/display-versions` | Inhalts-„Versionen" | -| `cms/display-versions/{id}/edit` | Editor für eine Version | +| `cms/display-modules` | Inhalts-Module | +| `cms/display-modules/{id}/edit` | Editor für ein Modul | | `cms/displays` | Physische Displays + Playlist-Zuweisung | | `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) -└── m:n via display_display_version (sort_order = Playlist-Reihenfolge) - └── display_versions (5 Datensätze live) - ├── type: video-display | b2in | offers - ├── settings: JSON - └── 1:n display_version_items (17 Datensätze live) - ├── item_type: video | footer | media | slide - └── content: JSON +└── 1:n display_playlists (Live/Entwurf) + └── 1:n display_playlist_items (sort_order = Playlist-Reihenfolge) + └── display_versions (technisch), fachlich Module + ├── type: video-display | b2in | offers + ├── settings: JSON + └── 1:n display_version_items + ├── item_type: video | footer | media | slide + └── content: JSON ``` ### 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. | | 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 @@ -181,7 +182,7 @@ für jedes Display D: 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: 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. @@ -355,15 +356,15 @@ Jede Phase liefert ein in sich getestetes, deploybares Inkrement. - [ ] Player-Templates: Single-Module-Modus ### Phase 6 – Umbenennung & Onboarding (Tag 3) -- [ ] Routen: `display-versions` → `display-modules` (mit 301-Redirect) -- [ ] Komponenten / Views umbenennen -- [ ] Dashboard-Texte / Hilfe-Bausteine aktualisieren -- [ ] Tooltips an Schlüsselstellen +- [x] Routen: `display-versions` → `display-modules` +- [x] Komponenten / Views umbenennen +- [x] Dashboard-Texte / Hilfe-Bausteine aktualisieren +- [x] Tooltips an Schlüsselstellen ### Phase 7 – Aufräumen (Tag 4) -- [ ] `display_display_version`-Tabelle dropped -- [ ] Alte Routen entfernt -- [ ] `DISPLAY_CMS_README.md` aktualisiert (in `dev/` ablegen) +- [x] `display_display_version`-Tabelle dropped +- [x] Alte Routen entfernt +- [x] Entwicklerdoku in `dev/displays-11-05-2026` aktualisiert - [ ] Vollständiger Test-Run --- diff --git a/dev/displays-11-05-2026/01-status.md b/dev/displays-11-05-2026/01-status.md index e29d514..8389a3d 100644 --- a/dev/displays-11-05-2026/01-status.md +++ b/dev/displays-11-05-2026/01-status.md @@ -245,7 +245,7 @@ Umsetzung: ## 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 @@ -261,9 +261,9 @@ Dateien: Umsetzung: - 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` -- 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“ -- 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 @@ -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. +--- + +## 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 + diff --git a/docker-compose.yml b/docker-compose.yml index e3a8b9e..f77ab25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: REDIS_HOST: global-redis volumes: - '.:/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: - sail - proxy diff --git a/packages/flux-cms/core/resources/views/admin-reference/cms/media-library-uploader.blade.php b/packages/flux-cms/core/resources/views/admin-reference/cms/media-library-uploader.blade.php index f571e05..4924e6f 100644 --- a/packages/flux-cms/core/resources/views/admin-reference/cms/media-library-uploader.blade.php +++ b/packages/flux-cms/core/resources/views/admin-reference/cms/media-library-uploader.blade.php @@ -3,7 +3,7 @@ accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.pdf,.doc,.docx,.jpg,.jpeg,.png"> diff --git a/packages/flux-cms/core/src/Helpers/MediaLibraryUploader.php b/packages/flux-cms/core/src/Helpers/MediaLibraryUploader.php index 8b495b6..90c1986 100644 --- a/packages/flux-cms/core/src/Helpers/MediaLibraryUploader.php +++ b/packages/flux-cms/core/src/Helpers/MediaLibraryUploader.php @@ -17,7 +17,7 @@ class MediaLibraryUploader extends Component { $this->validate([ '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); diff --git a/packages/flux-cms/core/src/Helpers/MediaPicker.php b/packages/flux-cms/core/src/Helpers/MediaPicker.php index 14eb146..ee4e9af 100644 --- a/packages/flux-cms/core/src/Helpers/MediaPicker.php +++ b/packages/flux-cms/core/src/Helpers/MediaPicker.php @@ -73,7 +73,7 @@ class MediaPicker extends Component { $this->validate([ '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); diff --git a/packages/flux-cms/core/src/Helpers/MediaUploader.php b/packages/flux-cms/core/src/Helpers/MediaUploader.php index c06737d..059cdd0 100644 --- a/packages/flux-cms/core/src/Helpers/MediaUploader.php +++ b/packages/flux-cms/core/src/Helpers/MediaUploader.php @@ -18,7 +18,7 @@ class MediaUploader extends Component public string $directory = 'cms/uploads'; - #[Validate('file|max:10240')] + #[Validate('file|max:204800')] public $file; public function updatedFile(): void diff --git a/public/_cabinet/_docs/QUICK_START.md b/public/_cabinet/_docs/QUICK_START.md index 69a1f80..3d2f744 100644 --- a/public/_cabinet/_docs/QUICK_START.md +++ b/public/_cabinet/_docs/QUICK_START.md @@ -85,11 +85,11 @@ Beim Hochladen neuer Videos beachten: - [ ] Format: **MP4** (H.264 + AAC) - [ ] Auflösung: **Max 1920x1080** - [ ] Bitrate: **5-10 Mbps** -- [ ] Dateigröße: **Max 100 MB** +- [ ] Dateigröße: **Max 200 MB** - [ ] Länge: **15-60 Sekunden** (optimal) ### ⚠️ Vermeiden: -- ❌ Zu große Dateien (>100MB) +- ❌ Zu große Dateien (>200MB) - ❌ Zu hohe Bitrate (>10 Mbps) - ❌ Zu lange Videos (>3 Min) - ❌ Exotische Formate (MOV, AVI, WMV) diff --git a/public/_cabinet/_docs/VIDEO_OPTIMIZATION_README.md b/public/_cabinet/_docs/VIDEO_OPTIMIZATION_README.md index a8b86d3..7e69043 100644 --- a/public/_cabinet/_docs/VIDEO_OPTIMIZATION_README.md +++ b/public/_cabinet/_docs/VIDEO_OPTIMIZATION_README.md @@ -214,7 +214,7 @@ setTimeout(() => { ### 3. **Dateigrößen** - **Optimal:** 10-50 MB pro Video -- **Maximum:** 100 MB pro Video +- **Maximum:** 200 MB pro Video - **Warum:** Schnelleres Laden, weniger Buffering ### 4. **Playlist-Größe** diff --git a/public/_cabinet/display/index.html b/public/_cabinet/display/index.html index 4873654..96e9eff 100644 --- a/public/_cabinet/display/index.html +++ b/public/_cabinet/display/index.html @@ -411,6 +411,56 @@ .status-message { font-weight: 300; opacity: 0.7; } .status-error { color: #ef4444; font-weight: 500; } .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); + } @@ -434,6 +484,18 @@
Neustart in Kürze...
+ + - - - diff --git a/resources/views/components/layouts/auth/split.blade.php b/resources/views/components/layouts/auth/split.blade.php index 0627b6c..8af6917 100644 --- a/resources/views/components/layouts/auth/split.blade.php +++ b/resources/views/components/layouts/auth/split.blade.php @@ -43,6 +43,7 @@ + @livewireScripts @fluxScripts @include('partials.theme-toggle-script') diff --git a/resources/views/components/media-thumb.blade.php b/resources/views/components/media-thumb.blade.php new file mode 100644 index 0000000..7a8eb53 --- /dev/null +++ b/resources/views/components/media-thumb.blade.php @@ -0,0 +1,27 @@ +@props(['url' => '', 'size' => 'h-10 w-10']) + +@php + $url = (string) $url; + $path = parse_url($url, PHP_URL_PATH) ?? ''; + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $isVideo = in_array($ext, ['mp4', 'webm', 'mov', 'm4v', 'ogv'], true); +@endphp + +
merge(['class' => "relative {$size} shrink-0 flex items-center justify-center overflow-hidden rounded-lg border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800"]) }}> + @if($url === '') + + @elseif($isVideo) + + + + + @else + + + @endif +
diff --git a/resources/views/livewire/admin/cms/display-list.blade.php b/resources/views/livewire/admin/cms/display-list.blade.php index 766f88b..0d129a2 100644 --- a/resources/views/livewire/admin/cms/display-list.blade.php +++ b/resources/views/livewire/admin/cms/display-list.blade.php @@ -220,6 +220,13 @@ {{ __('Test-URL') }} + + {{ __('Link erneuern') }} + isNotEmpty())
- + @foreach($availableVersions as $version) - + {{ $version->name }} ({{ $version->type->label() }}) @endforeach
- + {{ __('Hinzufügen') }}
@else diff --git a/resources/views/livewire/admin/cms/display-media-library.blade.php b/resources/views/livewire/admin/cms/display-media-library.blade.php index fe843f6..d490583 100644 --- a/resources/views/livewire/admin/cms/display-media-library.blade.php +++ b/resources/views/livewire/admin/cms/display-media-library.blade.php @@ -6,11 +6,12 @@ use Flux\Flux; use Illuminate\Support\Facades\Storage; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; use Livewire\WithFileUploads; +use Livewire\WithPagination; use function Livewire\Volt\{layout, title, state, computed, on, uses}; layout('components.layouts.app'); title('Display-Mediathek'); -uses([WithFileUploads::class]); +uses([WithFileUploads::class, WithPagination::class]); state([ 'search' => '', @@ -47,12 +48,16 @@ $media = computed( default => $q, }) ->when($this->filterCollection, fn ($q) => $q->inCollection($this->filterCollection)) - ->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%") - ->orWhere('title', 'like', "%{$this->search}%")) + ->when($this->search, fn ($q) => $q->search($this->search)) ->orderByDesc('created_at') ->paginate(48), ); +$updatedSearch = fn () => $this->resetPage(); +$updatedFilterType = fn () => $this->resetPage(); +$updatedFilterSource = fn () => $this->resetPage(); +$updatedFilterCollection = fn () => $this->resetPage(); + $collections = computed(fn () => DisplayMedia::query() ->whereNotNull('collection') ->where('collection', '!=', '') @@ -283,27 +288,37 @@ $closeDetail = function () { class="group relative cursor-pointer overflow-hidden rounded-lg border transition-all {{ $editingId === $item->id ? 'border-blue-500 ring-2 ring-blue-200 dark:ring-blue-800' : 'border-zinc-200 hover:border-zinc-400 dark:border-zinc-700' }}" wire:click="startEdit({{ $item->id }})"> -
- @if ($item->isImage() && $item->isUpload()) - getThumbnailUrl() ?? ($item->isImage() && $item->isExternal() ? $item->external_url : null); + $videoFrameSrc = (! $thumbSrc && $item->isVideo() && $item->isUpload()) ? $item->getUrl() : null; + @endphp +
+ @if ($thumbSrc) + {{ $item->alt_text ?? $item->filename }} + @elseif ($videoFrameSrc) + @elseif ($item->isVideo())
Video
- @elseif ($item->isExternal() && $item->isImage()) -
- - Extern -
@else
Link
@endif + @if ($item->isVideo() && ($thumbSrc || $videoFrameSrc)) +
+ + + +
+ @endif
@if ($item->isVideo()) @@ -352,9 +367,17 @@ $closeDetail = function () { class="cursor-pointer transition {{ $editingId === $item->id ? 'bg-blue-50 dark:bg-blue-900/20' : 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50' }}" wire:click="startEdit({{ $item->id }})"> -
- @if ($item->isImage() && $item->isUpload()) - + @php + $rowThumb = $item->getThumbnailUrl() ?? ($item->isImage() && $item->isExternal() ? $item->external_url : null); + $rowVideoFrame = (! $rowThumb && $item->isVideo() && $item->isUpload()) ? $item->getUrl() : null; + @endphp +
+ @if ($rowThumb) + + @elseif ($rowVideoFrame) + @elseif ($item->isVideo()) @else @@ -418,17 +441,18 @@ $closeDetail = function () { {{-- Preview --}}
- @if ($editMedia->isImage() && $editMedia->isUpload()) + @if ($editMedia->isImage()) {{ $editMedia->filename }} @elseif ($editMedia->isVideo() && $editMedia->isUpload()) -
diff --git a/resources/views/livewire/admin/cms/display-media-picker.blade.php b/resources/views/livewire/admin/cms/display-media-picker.blade.php index 53e97ef..f28e9dc 100644 --- a/resources/views/livewire/admin/cms/display-media-picker.blade.php +++ b/resources/views/livewire/admin/cms/display-media-picker.blade.php @@ -2,11 +2,20 @@
@if ($selectedMedia) + @php + $selectedThumb = $selectedMedia->getThumbnailUrl() + ?? ($selectedMedia->isImage() && $selectedMedia->isExternal() ? $selectedMedia->external_url : null); + $selectedVideoFrame = (! $selectedThumb && $selectedMedia->isVideo() && $selectedMedia->isUpload()) ? $selectedMedia->getUrl() : null; + @endphp
- @if ($selectedMedia->isImage() && $selectedMedia->isUpload()) - {{ $selectedMedia->filename }} + @elseif ($selectedVideoFrame) + @elseif ($selectedMedia->isVideo())
@@ -88,9 +97,18 @@ {{ $value === $item->id ? 'border-blue-500 ring-2 ring-blue-200' : 'border-zinc-200 hover:border-blue-300 dark:border-zinc-700' }}" wire:click="selectMedia({{ $item->id }})">
- @if ($item->isImage() && $item->isUpload()) - getThumbnailUrl() + ?? ($item->isImage() && $item->isExternal() ? $item->external_url : null); + $pickVideoFrame = (! $pickThumb && $item->isVideo() && $item->isUpload()) ? $item->getUrl() : null; + @endphp + @if ($pickThumb) + {{ $item->filename }} + @elseif ($pickVideoFrame) + @elseif ($item->isVideo())
diff --git a/resources/views/livewire/admin/cms/display-version-editor.blade.php b/resources/views/livewire/admin/cms/display-version-editor.blade.php index 572d5df..70e3992 100644 --- a/resources/views/livewire/admin/cms/display-version-editor.blade.php +++ b/resources/views/livewire/admin/cms/display-version-editor.blade.php @@ -121,9 +121,13 @@ type="video" label="Video aus Mediathek" :key="'picker-video-' . ($itemId ?? 'new')" /> - - +
+ + +
+ @@ -140,22 +144,29 @@ {{-- Media fields (B2in) --}} @if($itemType === 'media') - - - - - - - - - +
+ + +
+
+ {{ __('Medientyp:') }} + + {{ $mediaType === 'video' ? __('Video') : __('Bild') }} + + {{ __('wird automatisch aus dem gewählten Medium erkannt') }} +
+ + + + + @if($mediaType === 'image') @@ -165,47 +176,79 @@ @endif - {{-- Slide fields (Offers) --}} + {{-- Slide fields (Offers) – einheitliches Detail-Layout mit Ein-/Ausblende-Schaltern --}} @if($itemType === 'slide') - {{-- Basis --}} - - - - - - - - - - - - + + {{ __('Jedes Angebot nutzt dasselbe Detail-Layout. Über die Schalter blendest du einzelne Bausteine ein oder aus und befüllst sie mit Inhalten.') }} + - {{-- Intro-spezifisch --}} - @if($slideType === 'intro') - - - @if($slideShowBrandText) - + {{-- Logo & Marke (Kopfbereich) --}} +
+ {{ __('Logo & Marke') }} + + @if($slideShowLogo) + +
+ + +
+ + @endif - @endif +
- {{-- Product-Hero --}} - @if($slideType === 'product-hero') - - - @endif + {{-- Bild & Badge (wichtigstes Element – farblich hervorgehoben) --}} +
+
+ {{ __('Bild & Badge') }} + {{ __('Wichtigstes Element') }} +
+ +
+ + +
+ + @if($slideShowBadge) + + @endif +
- {{-- Product-Details --}} - @if($slideType === 'product-details') -
- {{ __('Aufzählungspunkte') }} + {{-- Texte --}} +
+ {{ __('Texte') }} + + + @if($slideShowEyebrow) + + @endif + + @if($slideShowSubline) + + @endif +
+ + {{-- Aufzählung --}} +
+ {{ __('Aufzählungspunkte') }} + + @if($slideShowBullets)
@foreach($slideBullets as $i => $bullet)
@@ -214,30 +257,58 @@
@endforeach
- + {{ __('Punkt hinzufügen') }} -
- @endif - - {{-- Product-Impulse --}} - @if($slideType === 'product-impulse') - - - - @endif - - {{-- QR --}} -
- {{ __('QR-Code & Kontakt') }} -
- - - -
+ @endif
- + {{-- Preis --}} +
+ {{ __('Preis') }} + + @if($slideShowPrice) + + + @if(trim($slideOriginalPrice) !== '') + + @endif + + @endif +
+ + {{-- Hinweis --}} +
+ {{ __('Hinweis') }} + + @if($slideShowDisclaimer) + + @endif +
+ + {{-- QR & Kontakt --}} +
+ {{ __('QR-Code & Kontakt') }} + + @if($slideShowQr) + + + @endif + + @if($slideShowContact) + + @endif +
+ + {{-- Anzeige --}} +
+ {{ __('Anzeige') }} + + +
@endif
@@ -246,22 +317,37 @@ {{ __('Zeigt nur den aktuell bearbeiteten Inhalt im Display-Player') }}
-
+ {{-- Stable iframe element: only its `src` changes between the + "new" (about:blank) and "saved" state. Swapping the element + structure inside the teleported Flux modal crashes Livewire's + morph/cleanup, so we keep the DOM shape constant. --}} +
+ + @unless($itemId) +
+
+ + {{ __('Die Vorschau erscheint, sobald der Inhalt gespeichert wurde.') }} +
+
+ @endunless
- - - {{ __('Vollbild öffnen') }} - + @if($itemId) + + + {{ __('Vollbild öffnen') }} + + @endif
diff --git a/resources/views/livewire/admin/cms/display-version-list.blade.php b/resources/views/livewire/admin/cms/display-version-list.blade.php index 69866d0..4caecae 100644 --- a/resources/views/livewire/admin/cms/display-version-list.blade.php +++ b/resources/views/livewire/admin/cms/display-version-list.blade.php @@ -10,6 +10,15 @@ @endif + @if (session()->has('error')) +
+
+ +

{{ session('error') }}

+
+
+ @endif +
diff --git a/resources/views/livewire/admin/cms/partials/version-editor-offers.blade.php b/resources/views/livewire/admin/cms/partials/version-editor-offers.blade.php index 29eccd3..dfbc8e6 100644 --- a/resources/views/livewire/admin/cms/partials/version-editor-offers.blade.php +++ b/resources/views/livewire/admin/cms/partials/version-editor-offers.blade.php @@ -6,18 +6,18 @@
- {{ __('Slides') }} - {{ __('Angebots-Slides werden in der angegebenen Reihenfolge angezeigt') }} + {{ __('Angebote') }} + {{ __('Angebote werden im einheitlichen Detail-Layout in der angegebenen Reihenfolge angezeigt') }}
- {{ __('Slide hinzufügen') }} + {{ __('Angebot hinzufügen') }}
@if($slides->isEmpty())
-

{{ __('Noch keine Slides vorhanden.') }}

+

{{ __('Noch keine Angebote vorhanden.') }}

@else
@@ -42,29 +42,30 @@
+ @php + $c = $item->content; + $enabledBlocks = collect([ + 'Badge' => ($c['show_badge'] ?? ! empty($c['badge_text'])) && ! empty($c['badge_text']), + 'Aufzählung' => ($c['show_bullets'] ?? ! empty($c['bullets'])) && ! empty($c['bullets']), + 'Preis' => ($c['show_price'] ?? ! empty($c['price'])) && ! empty($c['price']), + 'QR' => ($c['show_qr'] ?? ! empty($c['qr_url'])) && ! empty($c['qr_url']), + 'Kontakt' => ($c['show_contact'] ?? ! empty($c['contact'])) && ! empty($c['contact']), + ])->filter()->keys(); + @endphp
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }} - - {{ match($item->content['type'] ?? '') { - 'intro' => 'Intro', - 'product-hero' => 'Produkt-Hero', - 'product-details' => 'Produkt-Details', - 'product-impulse' => 'Produkt-Impuls', - default => $item->content['type'] ?? '–', - } }} - - {{ $item->content['title'] ?? '–' }} + {{ $c['title'] ?? '–' }}
-
- @if(!empty($item->content['price'])) - {{ $item->content['price'] }} - @endif - {{ number_format(($item->content['duration'] ?? 8000) / 1000, 1) }}s - @if(!empty($item->content['badge_text'])) - {{ $item->content['badge_text'] }} +
+ {{ number_format(($c['duration'] ?? 8000) / 1000, 1) }}s + @if(!empty($c['price']) && ($c['show_price'] ?? ! empty($c['price']))) + {{ $c['price'] }} @endif + @foreach($enabledBlocks as $block) + {{ $block }} + @endforeach
diff --git a/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php b/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php index 91180e8..ceda105 100644 --- a/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php +++ b/resources/views/livewire/admin/cms/partials/version-editor-settings.blade.php @@ -1,25 +1,74 @@ @if($version->type->value === 'b2in') + @php + $footerShown = ($settings['show_footer'] ?? true) !== false; + $showLogo = ($settings['show_logo'] ?? true) !== false; + $showClaim = ($settings['show_claim'] ?? true) !== false; + $logoPos = $settings['logo_position'] ?? 'top-left'; + $claimPos = $settings['claim_position'] ?? 'top-right'; + $brandPositions = [ + 'top-left' => __('Oben links'), + 'top-right' => __('Oben rechts'), + 'bottom-left' => __('Unten links'), + 'bottom-right' => __('Unten rechts'), + ]; + @endphp
- {{ __('Header') }} + {{ __('Marke') }} + {{ __('Logo und Claim. Standardmäßig oben im Header. Die Ecken lassen sich frei wählen.') }} - +
+ + +
+ +
+ + +
+ + @if($showLogo) + + @foreach($brandPositions as $value => $label) + + @endforeach + + @endif + + @if($showClaim) + + @foreach($brandPositions as $value => $label) + + @endforeach + + @endif + + @if($footerShown) + + {{ __('Untere Ecken sind nur verfügbar, wenn der Footer ausgeblendet ist.') }} + + @endif
{{ __('Footer & QR') }} + - - +
@@ -32,30 +81,14 @@ @elseif($version->type->value === 'offers') -
- {{ __('Branding') }} - - - -
-
- {{ __('Footer & QR für alle Slides') }} - - - - -
+ + {{ __('Logo, Marken-Text, QR-Code und Kontakt werden je Angebot direkt am Element gepflegt.') }} + @elseif($version->type->value === 'video-display') @endif diff --git a/resources/views/livewire/admin/cms/partials/version-editor-video.blade.php b/resources/views/livewire/admin/cms/partials/version-editor-video.blade.php index 4d727c3..53f59ae 100644 --- a/resources/views/livewire/admin/cms/partials/version-editor-video.blade.php +++ b/resources/views/livewire/admin/cms/partials/version-editor-video.blade.php @@ -16,40 +16,43 @@
- @if($videos->isEmpty()) + @if ($videos->isEmpty())

{{ __('Noch keine Videos vorhanden.') }}

@else
- @foreach($videos as $index => $item) + @foreach ($videos as $index => $item)
+ class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 transition">
- @if($index > 0) - + @if ($index > 0) + @endif - @if($index < count($videos) - 1) - + @if ($index < count($videos) - 1) + + @endif
-
- Video -
+
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }} - {{ $item->content['title'] ?? $item->content['filename'] ?? '–' }} + {{ $item->content['title'] ?? ($item->content['filename'] ?? '–') }}
@php $videoSource = $item->content['filename'] ?? ''; - $isMediaLibrarySource = str_starts_with($videoSource, '/storage/') || str_starts_with($videoSource, 'http'); + $isMediaLibrarySource = + str_starts_with($videoSource, '/storage/') || str_starts_with($videoSource, 'http'); @endphp
@@ -61,9 +64,13 @@
- - - + + +
@endforeach @@ -76,34 +83,40 @@
{{ __('Footer-Inhalte') }} - {{ __('Inhalte werden im Footer rotiert') }} + {{ __('Inhalte werden im Footer rotiert / ohne Inhalte bleibt der untere Teil frei.') }} +
{{ __('Inhalt hinzufügen') }}
- @if($footers->isEmpty()) + @if ($footers->isEmpty())

{{ __('Noch keine Footer-Inhalte vorhanden.') }}

@else
- @foreach($footers as $index => $item) + @foreach ($footers as $index => $item)
+ class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 transition">
- @if($index > 0) - + @if ($index > 0) + + @endif - @if($index < count($footers) - 1) - + @if ($index < count($footers) - 1) + + @endif
-
+
{{ $item->content['headline'] ?? 'Footer' }}
{{ $item->content['subline'] ?? '' }}
@@ -117,16 +130,20 @@
{{ $item->content['subline'] ?? '' }} - @if(!empty($item->content['url'])) + @if (!empty($item->content['url'])) {{ Str::limit($item->content['url'], 40) }} @endif
- - - + + +
@endforeach diff --git a/resources/views/livewire/admin/hubs/manage.blade.php b/resources/views/livewire/admin/hubs/manage.blade.php index 59cb86f..25aa71b 100644 --- a/resources/views/livewire/admin/hubs/manage.blade.php +++ b/resources/views/livewire/admin/hubs/manage.blade.php @@ -35,26 +35,26 @@ mount(function ($hubId = null) { // Auto-generate slug from name $updatedName = function ($value) { - if (!$this->hubId) { // Only auto-generate for new hubs + if (!$this->hubId) { + // Only auto-generate for new hubs $this->slug = \Illuminate\Support\Str::slug($value); } }; $locations = computed(function () { - if (!$this->hubId) return collect(); + if (!$this->hubId) { + return collect(); + } - return \App\Models\HubLocation::where('hub_id', $this->hubId) - ->when($this->zipSearch, fn($q) => $q->where('zip_code', 'like', "%{$this->zipSearch}%") - ->orWhere('city_name', 'like', "%{$this->zipSearch}%")) - ->orderBy('zip_code') - ->paginate(50); + return \App\Models\HubLocation::where('hub_id', $this->hubId)->when($this->zipSearch, fn($q) => $q->where('zip_code', 'like', "%{$this->zipSearch}%")->orWhere('city_name', 'like', "%{$this->zipSearch}%"))->orderBy('zip_code')->paginate(50); }); $partners = computed(function () { - if (!$this->hubId) return collect(); + if (!$this->hubId) { + return collect(); + } - return \App\Models\Partner::where('hub_id', $this->hubId) - ->get(); + return \App\Models\Partner::where('hub_id', $this->hubId)->get(); }); // Dummy save function @@ -103,12 +103,12 @@ $deleteLocation = function ($id) { {{-- Flash Message --}} @if (session()->has('message')) - -
- - {{ session('message') }} -
-
+ +
+ + {{ session('message') }} +
+
@endif {{-- Tabs --}} @@ -119,364 +119,360 @@ $deleteLocation = function ($id) { {{-- TAB 1: Identität & Design --}} - @if($activeTab === 'identity') - -
- {{-- Basis-Informationen --}} -
- - {{ __('Hub-Name') }} * - - {{ __('Wird dem Kunden angezeigt, z.B. "Region Ostwestfalen-Lippe"') }} - + @if ($activeTab === 'identity') + +
+ {{-- Basis-Informationen --}} +
+ + {{ __('Hub-Name') }} * + + {{ __('Wird dem Kunden angezeigt, z.B. "Region Ostwestfalen-Lippe"') }} + + - - {{ __('URL-Slug') }} * - - {{ __('Für saubere URLs, z.B. b2in.de/region/owl') }} - + + {{ __('URL-Slug') }} * + + {{ __('Für saubere URLs, z.B. b2in.eu/region/owl') }} + - - {{ __('Status') }} - - {{ __('Hub ist aktiv und für Kunden sichtbar') }} - - - {{ __('Inaktive Hubs sind im "Coming Soon"-Modus') }} - - -
+ + {{ __('Status') }} + + {{ __('Hub ist aktiv und für Kunden sichtbar') }} + + + {{ __('Inaktive Hubs sind im "Coming Soon"-Modus') }} + + +
- {{-- Vorschau --}} -
-

{{ __('Vorschau: Kunden-Landingpage') }}

-
- @if($keyvisual) - Keyvisual - @else -
-
- -

{{ __('Keyvisual hochladen') }}

+ {{-- Vorschau --}} +
+

+ {{ __('Vorschau: Kunden-Landingpage') }}

+
+ @if ($keyvisual) + Keyvisual + @else +
+
+ +

{{ __('Keyvisual hochladen') }}

+
+
+ @endif +
+

+ {{ $name ?: __('Ihr Hub-Name') }} +

+

+ {{ __('Die besten Marken Europas, von Händlern aus Ihrer Nachbarschaft') }} +

+ @if ($emblem) +
+ Emblem +
+ @endif
- @endif -
-

- {{ $name ?: __('Ihr Hub-Name') }} -

-

- {{ __('Die besten Marken Europas, von Händlern aus Ihrer Nachbarschaft') }} -

-
- @if($emblem) -
- Emblem -
- @endif
-
- + - {{-- Keyvisual Upload --}} - - {{ __('Keyvisual (Hintergrundbild)') }} - {{-- --}} - - {{ __('Atmosphärisches Regionalbild für emotionalen Einstieg') }} -
- - {{ __('Beispiele: Hermannsdenkmal (OWL), Skyline (Frankfurt), Hafen (Hamburg)') }} - • {{ __('Empfohlen: 1920x800px, max. 2MB') }} - -
-
+ {{-- Keyvisual Upload --}} + + {{ __('Keyvisual (Hintergrundbild)') }} + {{-- --}} + + {{ __('Atmosphärisches Regionalbild für emotionalen Einstieg') }} +
+ + {{ __('Beispiele: Hermannsdenkmal (OWL), Skyline (Frankfurt), Hafen (Hamburg)') }} + • {{ __('Empfohlen: 1920x800px, max. 2MB') }} + +
+
- {{-- Wappen Upload --}} - - {{ __('Wappen / Emblem') }} - {{-- --}} - - {{ __('Offizielles Wappen oder Logo der Region für Vertrauen & Offizialität') }} -
- - {{ __('Beispiele: Sparrenburg-Logo, Landeswappen') }} - • {{ __('Empfohlen: 256x256px, PNG mit transparentem Hintergrund') }} - -
-
- + {{-- Wappen Upload --}} + + {{ __('Wappen / Emblem') }} + {{-- --}} + + {{ __('Offizielles Wappen oder Logo der Region für Vertrauen & Offizialität') }} +
+ + {{ __('Beispiele: Sparrenburg-Logo, Landeswappen') }} + • {{ __('Empfohlen: 256x256px, PNG mit transparentem Hintergrund') }} + +
+
+ @endif {{-- TAB 2: Geografie & PLZ --}} - @if($activeTab === 'geography') -
+ @if ($activeTab === 'geography') +
- {{-- Info-Box --}} - -
- -
-
- {{ __('Die Mapping-Engine') }} -
-
- {{ __('Hier ordnen Sie Postleitzahlen diesem Hub zu. Gibt ein Kunde seine PLZ ein, wird er automatisch diesem regionalen Marktplatz zugewiesen und sieht lokale Händler.') }} + {{-- Info-Box --}} + +
+ +
+
+ {{ __('Die Mapping-Engine') }} +
+
+ {{ __('Hier ordnen Sie Postleitzahlen diesem Hub zu. Gibt ein Kunde seine PLZ ein, wird er automatisch diesem regionalen Marktplatz zugewiesen und sieht lokale Händler.') }} +
-
- + - {{-- PLZ-Import Tools --}} - - {{ __('Postleitzahlen hinzufügen') }} + {{-- PLZ-Import Tools --}} + + {{ __('Postleitzahlen hinzufügen') }} - - {{ __('Einzeln') }} - {{ __('Bereich') }} - {{ __('CSV-Import') }} - + + {{ __('Einzeln') }} + {{ __('Bereich') }} + {{ __('CSV-Import') }} + -
- {{-- Einzelne PLZ --}} - @if($importMethod === 'single') -
-
- - {{ __('Postleitzahl') }} * - - - - {{ __('Stadt') }} * - - +
+ {{-- Einzelne PLZ --}} + @if ($importMethod === 'single') +
+
+ + {{ __('Postleitzahl') }} * + + + + + {{ __('Stadt') }} * + + +
+ + {{ __('PLZ hinzufügen') }} + +
+ @endif + + {{-- PLZ-Bereich --}} + @if ($importMethod === 'range') +
+
+ + {{ __('Von PLZ') }} * + + + + {{ __('Bis PLZ') }} * + + +
+ + {{ __('Bereich importieren') }} + + + ⚠️ + {{ __('Generiert automatisch alle PLZs im Bereich. Kann bei großen Bereichen mehrere Minuten dauern!') }} + +
+ @endif + + {{-- CSV-Import --}} + @if ($importMethod === 'csv') +
+ + {{ __('CSV-Datei') }} * + + + {{ __('Format: PLZ,Stadt (eine Zeile pro Eintrag)') }} +
+ {{ __('Beispiel:') }} + + 33602,Bielefeld
33603,Bielefeld
33604,Bielefeld +
+
+
+ + {{ __('CSV importieren') }} + +
+ @endif +
+ + + {{-- PLZ-Liste --}} + +
+
+ + {{ __('Zugeordnete Postleitzahlen') }} + @if ($hubId) + + ({{ $this->locations->total() }} {{ __('gesamt') }}) + + @endif + +
- - {{ __('PLZ hinzufügen') }} - -
- @endif - {{-- PLZ-Bereich --}} - @if($importMethod === 'range') -
-
- - {{ __('Von PLZ') }} * - - - - {{ __('Bis PLZ') }} * - - -
- - {{ __('Bereich importieren') }} - - - ⚠️ {{ __('Generiert automatisch alle PLZs im Bereich. Kann bei großen Bereichen mehrere Minuten dauern!') }} - -
- @endif + @if ($hubId) + + + {{ __('PLZ') }} + {{ __('Stadt') }} + {{ __('Aktion') }} + - {{-- CSV-Import --}} - @if($importMethod === 'csv') -
- - {{ __('CSV-Datei') }} * - - - {{ __('Format: PLZ,Stadt (eine Zeile pro Eintrag)') }} -
- {{ __('Beispiel:') }} - - 33602,Bielefeld
33603,Bielefeld
33604,Bielefeld -
-
-
- - {{ __('CSV importieren') }} - -
- @endif -
- + + @forelse($this->locations as $location) + + + {{ $location->zip_code }} + + {{ $location->city_name }} + + + + + @empty + + + +

+ {{ __('Noch keine PLZs zugeordnet') }} +

+
+
+ @endforelse +
+ - {{-- PLZ-Liste --}} - -
-
- - {{ __('Zugeordnete Postleitzahlen') }} - @if($hubId) - - ({{ $this->locations->total() }} {{ __('gesamt') }}) - + {{-- Pagination --}} + @if ($this->locations->hasPages()) +
+ {{ $this->locations->links() }} +
@endif -
- + @else +
+ {{ __('Speichern Sie zuerst den Hub, um PLZs hinzuzufügen') }} +
+ @endif
- - @if($hubId) - - - {{ __('PLZ') }} - {{ __('Stadt') }} - {{ __('Aktion') }} - - - - @forelse($this->locations as $location) - - - {{ $location->zip_code }} - - {{ $location->city_name }} - - - - - @empty - - - -

- {{ __('Noch keine PLZs zugeordnet') }} -

-
-
- @endforelse -
-
- - {{-- Pagination --}} - @if($this->locations->hasPages()) -
- {{ $this->locations->links() }} -
- @endif - @else -
- {{ __('Speichern Sie zuerst den Hub, um PLZs hinzuzufügen') }} -
- @endif -
-
-
+ +
@endif {{-- TAB 3: Partner-Monitor --}} - @if($activeTab === 'partners') - - - {{ __('Partner in diesem Hub') }} - @if($hubId) - - ({{ $this->partners->count() }} {{ __('Partner') }}) - + @if ($activeTab === 'partners') + + + {{ __('Partner in diesem Hub') }} + @if ($hubId) + + ({{ $this->partners->count() }} {{ __('Partner') }}) + + @endif + + + {{-- Info --}} + +
+ {{ __('Logik:') }} +
    +
  • {{ __('Händler (Retailer) → Werden diesem Hub fest zugeordnet') }}
  • +
  • {{ __('Hersteller (Manufacturer) → Sind "Hub-agnostisch", in allen Hubs sichtbar') }}
  • +
  • {{ __('Makler (Estate-Agent) → Werden Hub zugeordnet für regionale Kundenakquise') }}
  • +
+
+
+ + @if ($hubId) + + + {{ __('Name') }} + {{ __('Typ') }} + {{ __('Stadt') }} + {{ __('Lieferradius') }} + {{ __('Status') }} + {{ __('Aktion') }} + + + + @forelse($this->partners as $partner) + + +
+ {{ $partner->company_name }} +
+ @if ($partner->display_name && $partner->display_name !== $partner->company_name) +
{{ $partner->display_name }}
+ @endif +
+ + @php + $typeColors = [ + 'Retailer' => 'blue', + 'Manufacturer' => 'purple', + 'Estate-Agent' => 'green', + ]; + @endphp + + {{ $partner->type }} + + + + {{ $partner->city ?? '-' }} + + + @if ($partner->delivery_radius_km) + {{ $partner->delivery_radius_km }} km + @else + - + @endif + + + + {{ $partner->is_active ? __('Aktiv') : __('Inaktiv') }} + + + + + {{ __('Details') }} + + +
+ @empty + + + +

+ {{ __('Noch keine Partner in diesem Hub') }} +

+

+ {{ __('Partner werden automatisch beim Onboarding zugeordnet (basierend auf PLZ)') }} +

+
+
+ @endforelse +
+
+ @else +
+ {{ __('Speichern Sie zuerst den Hub, um Partner-Zuordnungen zu sehen') }} +
@endif -
- - {{-- Info --}} - -
- {{ __('Logik:') }} -
    -
  • {{ __('Händler (Retailer) → Werden diesem Hub fest zugeordnet') }}
  • -
  • {{ __('Hersteller (Manufacturer) → Sind "Hub-agnostisch", in allen Hubs sichtbar') }}
  • -
  • {{ __('Makler (Estate-Agent) → Werden Hub zugeordnet für regionale Kundenakquise') }}
  • -
-
- - @if($hubId) - - - {{ __('Name') }} - {{ __('Typ') }} - {{ __('Stadt') }} - {{ __('Lieferradius') }} - {{ __('Status') }} - {{ __('Aktion') }} - - - - @forelse($this->partners as $partner) - - -
- {{ $partner->company_name }} -
- @if($partner->display_name && $partner->display_name !== $partner->company_name) -
{{ $partner->display_name }}
- @endif -
- - @php - $typeColors = [ - 'Retailer' => 'blue', - 'Manufacturer' => 'purple', - 'Estate-Agent' => 'green', - ]; - @endphp - - {{ $partner->type }} - - - - {{ $partner->city ?? '-' }} - - - @if($partner->delivery_radius_km) - {{ $partner->delivery_radius_km }} km - @else - - - @endif - - - - {{ $partner->is_active ? __('Aktiv') : __('Inaktiv') }} - - - - - {{ __('Details') }} - - -
- @empty - - - -

- {{ __('Noch keine Partner in diesem Hub') }} -

-

- {{ __('Partner werden automatisch beim Onboarding zugeordnet (basierend auf PLZ)') }} -

-
-
- @endforelse -
-
- @else -
- {{ __('Speichern Sie zuerst den Hub, um Partner-Zuordnungen zu sehen') }} -
- @endif -
@endif
- diff --git a/tests/Feature/DisplayListTest.php b/tests/Feature/DisplayListTest.php index 4dc5b30..c289592 100644 --- a/tests/Feature/DisplayListTest.php +++ b/tests/Feature/DisplayListTest.php @@ -197,7 +197,7 @@ test('can create a draft playlist from live modules', function () { test('can discard a draft playlist', function () { $user = User::factory()->create(); $version = DisplayVersion::factory()->create(); - $display = Display::factory()->create(); + $display = Display::factory()->create(['preview_token' => 'token-discard-123456789012345678901234']); createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$version->id]); Livewire::actingAs($user) @@ -205,6 +205,57 @@ test('can discard a draft playlist', function () { ->call('discardDraft', $display->id); expect($display->fresh()->draftPlaylist)->toBeNull(); + expect($display->fresh()->preview_token)->toBeNull(); +}); + +test('publishing a draft clears the preview token', function () { + $user = User::factory()->create(); + $liveVersion = DisplayVersion::factory()->create(); + $draftVersion = DisplayVersion::factory()->create(); + $display = Display::factory()->create(['preview_token' => 'token-publish-12345678901234567890123456']); + createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$liveVersion->id]); + createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]); + + Livewire::actingAs($user) + ->test(DisplayList::class) + ->call('publishDraft', $display->id); + + expect($display->fresh()->preview_token)->toBeNull(); +}); + +test('can rotate the preview token of a draft', function () { + $user = User::factory()->create(); + $version = DisplayVersion::factory()->create(); + $display = Display::factory()->create(['preview_token' => 'token-old-1234567890123456789012345678901']); + createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$version->id]); + + Livewire::actingAs($user) + ->test(DisplayList::class) + ->call('rotatePreviewToken', $display->id); + + $newToken = $display->fresh()->preview_token; + + expect($newToken)->not->toBeNull() + ->and($newToken)->not->toBe('token-old-1234567890123456789012345678901'); +}); + +test('can add multiple modules to a playlist at once', function () { + $user = User::factory()->create(); + $version1 = DisplayVersion::factory()->create(); + $version2 = DisplayVersion::factory()->create(); + $version3 = DisplayVersion::factory()->create(); + $display = Display::factory()->create(); + createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, []); + + Livewire::actingAs($user) + ->test(DisplayList::class) + ->call('openModal', $display->id, DisplayPlaylist::STATUS_PUBLISHED) + ->set('versionsToAdd', [$version1->id, $version3->id]) + ->call('addSelectedVersions') + ->assertSet('selectedVersionIds', [$version1->id, $version3->id]) + ->assertSet('versionsToAdd', []); + + expect(true)->toBeTrue(); }); test('can publish a draft playlist over the live playlist', function () { @@ -313,8 +364,8 @@ test('module select only shows modules that can still be added', function () { Livewire::actingAs($user) ->test(DisplayList::class) ->call('openModal', $display->id, DisplayPlaylist::STATUS_PUBLISHED) - ->assertDontSeeHtml('