tenant admin startseite schicker gestaltet und super-admin und tenant admin (filament) aufgesplittet.

Es gibt nun task collections und vordefinierte tasks für alle. Onboarding verfeinert und webseite-carousel gefixt (logging später entfernen!)
This commit is contained in:
Codex Agent
2025-10-14 15:17:52 +02:00
parent 64a5411fb9
commit 1a4bdb1fe1
92 changed files with 6027 additions and 515 deletions

View File

@@ -0,0 +1,278 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('task_collections')) {
if (Schema::hasColumn('task_collections', 'tenant_id')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->dropForeign(['tenant_id']);
});
Schema::table('task_collections', function (Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable()->change();
});
Schema::table('task_collections', function (Blueprint $table) {
$table->foreign('tenant_id')
->references('id')
->on('tenants')
->nullOnDelete();
});
}
Schema::table('task_collections', function (Blueprint $table) {
if (! Schema::hasColumn('task_collections', 'slug')) {
$table->string('slug')->nullable()->after('tenant_id');
}
if (! Schema::hasColumn('task_collections', 'name_translations')) {
$table->json('name_translations')->nullable()->after('slug');
}
if (! Schema::hasColumn('task_collections', 'description_translations')) {
$table->json('description_translations')->nullable()->after('name_translations');
}
if (! Schema::hasColumn('task_collections', 'event_type_id')) {
$table->foreignId('event_type_id')
->nullable()
->after('description_translations')
->constrained()
->nullOnDelete();
}
});
if (Schema::hasColumn('task_collections', 'name')) {
DB::table('task_collections')
->select('id', 'name', 'description', 'slug')
->orderBy('id')
->chunk(100, function ($rows) {
foreach ($rows as $row) {
$name = $row->name;
$description = $row->description;
$translations = [
'de' => $name,
];
$descriptionTranslations = $description
? [
'de' => $description,
]
: null;
$slugBase = Str::slug($name ?: ('collection-' . $row->id));
if (empty($slugBase)) {
$slugBase = 'collection-' . $row->id;
}
$slug = $row->slug ?: ($slugBase . '-' . $row->id);
DB::table('task_collections')
->where('id', $row->id)
->update([
'name_translations' => json_encode($translations, JSON_UNESCAPED_UNICODE),
'description_translations' => $descriptionTranslations
? json_encode($descriptionTranslations, JSON_UNESCAPED_UNICODE)
: null,
'slug' => $slug,
]);
}
});
Schema::table('task_collections', function (Blueprint $table) {
$table->dropColumn(['name', 'description']);
});
Schema::table('task_collections', function (Blueprint $table) {
$table->unique('slug');
});
}
}
if (Schema::hasTable('tasks')) {
if (Schema::hasColumn('tasks', 'tenant_id')) {
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign(['tenant_id']);
});
Schema::table('tasks', function (Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable()->change();
});
Schema::table('tasks', function (Blueprint $table) {
$table->foreign('tenant_id')
->references('id')
->on('tenants')
->nullOnDelete();
});
}
Schema::table('tasks', function (Blueprint $table) {
if (! Schema::hasColumn('tasks', 'slug')) {
$table->string('slug')->nullable()->after('id');
}
});
if (! Schema::hasColumn('tasks', 'slug')) {
return;
}
DB::table('tasks')
->select('id', 'slug', 'title')
->orderBy('id')
->chunk(100, function ($rows) {
foreach ($rows as $row) {
if (! empty($row->slug)) {
continue;
}
$titleData = $row->title;
if (is_string($titleData)) {
$json = json_decode($titleData, true);
} else {
$json = $titleData;
}
$base = $json['de']
?? $json['en']
?? ('task-' . $row->id);
$slug = Str::slug($base);
if (empty($slug)) {
$slug = 'task-' . $row->id;
}
DB::table('tasks')
->where('id', $row->id)
->update([
'slug' => $slug . '-' . $row->id,
]);
}
});
Schema::table('tasks', function (Blueprint $table) {
$table->unique('slug');
});
}
}
public function down(): void
{
if (Schema::hasTable('tasks')) {
$fallbackTenantId = DB::table('tenants')->orderBy('id')->value('id');
if ($fallbackTenantId) {
DB::table('tasks')->whereNull('tenant_id')->update(['tenant_id' => $fallbackTenantId]);
}
if (Schema::hasColumn('tasks', 'slug')) {
Schema::table('tasks', function (Blueprint $table) {
$table->dropUnique(['slug']);
$table->dropColumn('slug');
});
}
if (Schema::hasColumn('tasks', 'tenant_id')) {
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign(['tenant_id']);
});
Schema::table('tasks', function (Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable(false)->change();
});
Schema::table('tasks', function (Blueprint $table) {
$table->foreign('tenant_id')
->references('id')
->on('tenants')
->cascadeOnDelete();
});
}
}
if (Schema::hasTable('task_collections')) {
$fallbackTenantId = DB::table('tenants')->orderBy('id')->value('id');
if ($fallbackTenantId) {
DB::table('task_collections')->whereNull('tenant_id')->update(['tenant_id' => $fallbackTenantId]);
}
if (Schema::hasColumn('task_collections', 'name_translations') &&
! Schema::hasColumn('task_collections', 'name')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->string('name')->default('')->after('tenant_id');
$table->text('description')->nullable()->after('name');
});
DB::table('task_collections')
->select('id', 'name_translations', 'description_translations')
->orderBy('id')
->chunk(100, function ($rows) {
foreach ($rows as $row) {
$names = is_string($row->name_translations)
? json_decode($row->name_translations, true) ?: []
: ($row->name_translations ?? []);
$descriptions = is_string($row->description_translations)
? json_decode($row->description_translations, true) ?: []
: ($row->description_translations ?? []);
DB::table('task_collections')
->where('id', $row->id)
->update([
'name' => $names['de'] ?? $names['en'] ?? 'Collection ' . $row->id,
'description' => $descriptions['de'] ?? $descriptions['en'] ?? null,
]);
}
});
Schema::table('task_collections', function (Blueprint $table) {
if (Schema::hasColumn('task_collections', 'description_translations')) {
$table->dropColumn('description_translations');
}
if (Schema::hasColumn('task_collections', 'name_translations')) {
$table->dropColumn('name_translations');
}
});
}
if (Schema::hasColumn('task_collections', 'event_type_id')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->dropForeign(['event_type_id']);
$table->dropColumn('event_type_id');
});
}
if (Schema::hasColumn('task_collections', 'slug')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->dropUnique(['slug']);
$table->dropColumn('slug');
});
}
if (Schema::hasColumn('task_collections', 'tenant_id')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->dropForeign(['tenant_id']);
$table->unsignedBigInteger('tenant_id')->nullable(false)->change();
$table->foreign('tenant_id')
->references('id')
->on('tenants')
->cascadeOnDelete();
});
}
}
}
};

View File

@@ -0,0 +1,68 @@
<?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
{
if (Schema::hasTable('tasks')) {
Schema::table('tasks', function (Blueprint $table) {
if (! Schema::hasColumn('tasks', 'source_task_id')) {
$table->foreignId('source_task_id')->nullable()->after('tenant_id')->constrained('tasks')->nullOnDelete();
}
if (! Schema::hasColumn('tasks', 'source_collection_id')) {
$table->foreignId('source_collection_id')->nullable()->after('collection_id')->constrained('task_collections')->nullOnDelete();
}
});
}
if (Schema::hasTable('task_collections') && ! Schema::hasColumn('task_collections', 'source_collection_id')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->foreignId('source_collection_id')->nullable()->after('event_type_id')->constrained('task_collections')->nullOnDelete();
});
}
if (Schema::hasTable('emotions') && ! Schema::hasColumn('emotions', 'tenant_id')) {
Schema::table('emotions', function (Blueprint $table) {
$table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
$table->index('tenant_id');
});
}
}
public function down(): void
{
if (Schema::hasTable('tasks')) {
Schema::table('tasks', function (Blueprint $table) {
if (Schema::hasColumn('tasks', 'source_task_id')) {
$table->dropForeign(['source_task_id']);
$table->dropColumn('source_task_id');
}
if (Schema::hasColumn('tasks', 'source_collection_id')) {
$table->dropForeign(['source_collection_id']);
$table->dropColumn('source_collection_id');
}
});
}
if (Schema::hasTable('task_collections') && Schema::hasColumn('task_collections', 'source_collection_id')) {
Schema::table('task_collections', function (Blueprint $table) {
$table->dropForeign(['source_collection_id']);
$table->dropColumn('source_collection_id');
});
}
if (Schema::hasTable('emotions') && Schema::hasColumn('emotions', 'tenant_id')) {
Schema::table('emotions', function (Blueprint $table) {
$table->dropForeign(['tenant_id']);
$table->dropIndex(['tenant_id']);
$table->dropColumn('tenant_id');
});
}
}
};

View File

@@ -43,7 +43,8 @@ return new class extends Migration
if (!Schema::hasTable('tasks')) {
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('slug')->nullable()->unique();
$table->unsignedBigInteger('emotion_id')->nullable();
$table->unsignedBigInteger('event_type_id')->nullable();
$table->json('title');
@@ -75,9 +76,11 @@ return new class extends Migration
if (!Schema::hasTable('task_collections')) {
Schema::create('task_collections', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->text('description')->nullable();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('slug')->nullable()->unique();
$table->json('name_translations');
$table->json('description_translations')->nullable();
$table->foreignId('event_type_id')->nullable()->constrained()->nullOnDelete();
$table->boolean('is_default')->default(false);
$table->integer('position')->default(0);
$table->timestamps();

View File

@@ -30,7 +30,7 @@ return new class extends Migration
});
// Seed standard packages if empty
if (DB::table('packages')->count() == 0) {
/*if (DB::table('packages')->count() == 0) {
DB::table('packages')->insert([
[
'name' => 'Free/Test',
@@ -82,7 +82,7 @@ return new class extends Migration
],
// Add more as needed
]);
}
}*/
}
// Event Packages

View File

@@ -0,0 +1,150 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
$this->ensureCollectionSlugs();
$this->ensureTaskSlugs();
}
public function down(): void
{
$this->rollbackCollectionSlugs();
$this->rollbackTaskSlugs();
}
protected function ensureCollectionSlugs(): void
{
if (! Schema::hasTable('task_collections') || Schema::hasColumn('task_collections', 'slug')) {
return;
}
Schema::table('task_collections', function (Blueprint $table) {
$table->string('slug')->nullable()->after('tenant_id');
});
DB::table('task_collections')
->select('id', 'slug', 'name_translations')
->orderBy('id')
->chunk(200, function ($rows) {
foreach ($rows as $row) {
if (! empty($row->slug)) {
continue;
}
$translations = $this->decodeTranslations($row->name_translations);
$base = $translations['en'] ?? $translations['de'] ?? reset($translations) ?? ('collection-' . $row->id);
$slug = $this->buildUniqueSlug($base, 'collection-', function ($candidate) {
return DB::table('task_collections')->where('slug', $candidate)->exists();
});
DB::table('task_collections')
->where('id', $row->id)
->update(['slug' => $slug]);
}
});
Schema::table('task_collections', function (Blueprint $table) {
$table->unique('slug');
});
}
protected function ensureTaskSlugs(): void
{
if (! Schema::hasTable('tasks') || Schema::hasColumn('tasks', 'slug')) {
return;
}
Schema::table('tasks', function (Blueprint $table) {
$table->string('slug')->nullable()->after('id');
});
DB::table('tasks')
->select('id', 'slug', 'title')
->orderBy('id')
->chunk(200, function ($rows) {
foreach ($rows as $row) {
if (! empty($row->slug)) {
continue;
}
$translations = $this->decodeTranslations($row->title);
$base = $translations['en'] ?? $translations['de'] ?? reset($translations) ?? ('task-' . $row->id);
$slug = $this->buildUniqueSlug($base, 'task-', function ($candidate) {
return DB::table('tasks')->where('slug', $candidate)->exists();
});
DB::table('tasks')
->where('id', $row->id)
->update(['slug' => $slug]);
}
});
Schema::table('tasks', function (Blueprint $table) {
$table->unique('slug');
});
}
protected function rollbackCollectionSlugs(): void
{
if (! Schema::hasTable('task_collections') || ! Schema::hasColumn('task_collections', 'slug')) {
return;
}
Schema::table('task_collections', function (Blueprint $table) {
$table->dropUnique('task_collections_slug_unique');
$table->dropColumn('slug');
});
}
protected function rollbackTaskSlugs(): void
{
if (! Schema::hasTable('tasks') || ! Schema::hasColumn('tasks', 'slug')) {
return;
}
Schema::table('tasks', function (Blueprint $table) {
$table->dropUnique('tasks_slug_unique');
$table->dropColumn('slug');
});
}
/**
* @return array<string, string>
*/
protected function decodeTranslations(mixed $value): array
{
if (is_array($value)) {
return $value;
}
if (is_string($value)) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
return $decoded;
}
return ['de' => $value];
}
return [];
}
protected function buildUniqueSlug(string $base, string $prefix, callable $exists): string
{
$slugBase = Str::slug($base) ?: ($prefix . Str::random(4));
do {
$candidate = $slugBase . '-' . Str::random(4);
} while ($exists($candidate));
return $candidate;
}
};