12-05-2026 admin, Panel Displays

This commit is contained in:
Kevin Adametz 2026-05-12 18:28:38 +02:00
parent 0762e3beac
commit 6a65354f4c
43 changed files with 3273 additions and 410 deletions

View file

@ -30,6 +30,17 @@ class DisplayFactory extends Factory
'name' => fake()->words(2, true),
'location' => fake()->optional()->words(3, true),
'is_active' => true,
'is_test' => false,
'preview_token' => null,
];
}
public function test(): static
{
return $this->state(fn () => [
'is_test' => true,
'name' => 'Test-Display',
'location' => 'Vorschau / Test',
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Database\Factories;
use App\Models\Display;
use App\Models\DisplayPlaylist;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @template TModel of \App\Models\DisplayPlaylist
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
*/
class DisplayPlaylistFactory extends Factory
{
/**
* @var class-string<TModel>
*/
protected $model = DisplayPlaylist::class;
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'display_id' => Display::factory(),
'status' => DisplayPlaylist::STATUS_PUBLISHED,
'published_at' => now(),
'published_by' => null,
'notes' => null,
];
}
public function draft(): static
{
return $this->state(fn () => [
'status' => DisplayPlaylist::STATUS_DRAFT,
'published_at' => null,
'published_by' => null,
]);
}
public function published(): static
{
return $this->state(fn () => [
'status' => DisplayPlaylist::STATUS_PUBLISHED,
'published_at' => now(),
]);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\DisplayPlaylist;
use App\Models\DisplayPlaylistItem;
use App\Models\DisplayVersion;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @template TModel of \App\Models\DisplayPlaylistItem
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
*/
class DisplayPlaylistItemFactory extends Factory
{
/**
* @var class-string<TModel>
*/
protected $model = DisplayPlaylistItem::class;
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'display_playlist_id' => DisplayPlaylist::factory(),
'display_version_id' => DisplayVersion::factory(),
'sort_order' => 0,
];
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Erweitert die displays-Tabelle um:
* - is_test: Kennzeichnet ein Display als Test-Display (eigene Kachel im Admin,
* beliebige Module/Bespielungen können dort gefahrlos kombiniert werden).
* - preview_token: Eindeutiger Token für die Vorschau einer Entwurfs-Bespielung
* über /preview/{token} ohne Login.
*/
public function up(): void
{
Schema::table('displays', function (Blueprint $table) {
$table->boolean('is_test')->default(false)->after('is_active');
$table->string('preview_token', 64)->nullable()->unique()->after('is_test');
});
}
public function down(): void
{
Schema::table('displays', function (Blueprint $table) {
$table->dropUnique(['preview_token']);
$table->dropColumn(['is_test', 'preview_token']);
});
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Eine Display-Bespielung pro Display existiert maximal eine Live-Bespielung
* (status = published) und optional eine Entwurfs-Bespielung (status = draft).
*
* Der Player liest immer die published-Playlist. Die Vorschau-Route greift
* über den preview_token des Displays auf die draft-Playlist zu.
*/
public function up(): void
{
Schema::create('display_playlists', function (Blueprint $table) {
$table->id();
$table->foreignId('display_id')->constrained()->cascadeOnDelete();
$table->string('status'); // 'published' | 'draft'
$table->timestamp('published_at')->nullable();
$table->foreignId('published_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('notes')->nullable();
$table->timestamps();
$table->unique(['display_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('display_playlists');
}
};

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Eintrag in einer Display-Bespielung. Ersetzt funktional die alte
* Pivot-Tabelle display_display_version. display_versions selbst bleibt
* unverändert Module sind weiterhin wiederverwendbar (shared) zwischen
* Displays.
*/
public function up(): void
{
Schema::create('display_playlist_items', function (Blueprint $table) {
$table->id();
$table->foreignId('display_playlist_id')->constrained()->cascadeOnDelete();
$table->foreignId('display_version_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->index(['display_playlist_id', 'sort_order']);
});
}
public function down(): void
{
Schema::dropIfExists('display_playlist_items');
}
};

View file

@ -0,0 +1,72 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Überführt bestehende Pivot-Daten aus display_display_version in die neue
* Struktur display_playlists + display_playlist_items. Pro Display wird eine
* Live-Bespielung (status = published) erzeugt und die bestehende Reihenfolge
* 1:1 übernommen. Die alte Pivot-Tabelle bleibt erhalten und wird erst in
* Phase 7 dropped (Bestandsfunktionen müssen weiterlaufen).
*
* Idempotent: Existiert bereits eine Published-Playlist für ein Display, wird
* sie übersprungen.
*/
public function up(): void
{
if (! Schema::hasTable('display_display_version')) {
return;
}
$pivots = DB::table('display_display_version')
->orderBy('display_id')
->orderBy('sort_order')
->get()
->groupBy('display_id');
$now = now();
foreach ($pivots as $displayId => $entries) {
$existing = DB::table('display_playlists')
->where('display_id', $displayId)
->where('status', 'published')
->first();
if ($existing) {
continue;
}
$playlistId = DB::table('display_playlists')->insertGetId([
'display_id' => $displayId,
'status' => 'published',
'published_at' => $now,
'published_by' => null,
'notes' => 'Initial-Import aus display_display_version',
'created_at' => $now,
'updated_at' => $now,
]);
foreach ($entries as $entry) {
DB::table('display_playlist_items')->insert([
'display_playlist_id' => $playlistId,
'display_version_id' => $entry->display_version_id,
'sort_order' => $entry->sort_order,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
public function down(): void
{
DB::table('display_playlist_items')->delete();
DB::table('display_playlists')->delete();
}
};