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:
@@ -2,9 +2,12 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\EventType;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskCollection;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TaskCollectionFactory extends Factory
|
||||
{
|
||||
@@ -12,13 +15,21 @@ class TaskCollectionFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$categories = ['Allgemein', 'Vorbereitung', 'Event-Tag', 'Aufräumen', 'Follow-up'];
|
||||
$colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
|
||||
$label = ucfirst($this->faker->unique()->words(2, true));
|
||||
$description = $this->faker->sentence(12);
|
||||
|
||||
return [
|
||||
'tenant_id' => Tenant::factory(),
|
||||
'name' => $this->faker->randomElement($categories),
|
||||
'description' => $this->faker->sentence(),
|
||||
'event_type_id' => EventType::factory(),
|
||||
'slug' => Str::slug($label . '-' . $this->faker->unique()->numberBetween(1, 9999)),
|
||||
'name_translations' => [
|
||||
'de' => $label,
|
||||
'en' => $label,
|
||||
],
|
||||
'description_translations' => [
|
||||
'de' => $description,
|
||||
'en' => $description,
|
||||
],
|
||||
'is_default' => $this->faker->boolean(20),
|
||||
'position' => $this->faker->numberBetween(1, 10),
|
||||
];
|
||||
@@ -28,7 +39,10 @@ class TaskCollectionFactory extends Factory
|
||||
{
|
||||
return $this->afterCreating(function (TaskCollection $collection) use ($count) {
|
||||
\App\Models\Task::factory($count)
|
||||
->create(['tenant_id' => $collection->tenant_id])
|
||||
->create([
|
||||
'tenant_id' => $collection->tenant_id,
|
||||
'event_type_id' => $collection->event_type_id,
|
||||
])
|
||||
->each(function ($task) use ($collection) {
|
||||
$task->taskCollection()->associate($collection);
|
||||
$task->save();
|
||||
@@ -43,4 +57,4 @@ class TaskCollectionFactory extends Factory
|
||||
'position' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Task;
|
||||
use App\Models\TaskCollection;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TaskFactory extends Factory
|
||||
{
|
||||
@@ -13,10 +14,24 @@ class TaskFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$title = ucfirst($this->faker->unique()->words(4, true));
|
||||
$description = $this->faker->paragraph(2);
|
||||
|
||||
return [
|
||||
'tenant_id' => Tenant::factory(),
|
||||
'title' => $this->faker->sentence(4),
|
||||
'description' => $this->faker->paragraph(),
|
||||
'slug' => Str::slug($title . '-' . $this->faker->unique()->numberBetween(1, 9999)),
|
||||
'title' => [
|
||||
'de' => $title,
|
||||
'en' => $title,
|
||||
],
|
||||
'description' => [
|
||||
'de' => $description,
|
||||
'en' => $description,
|
||||
],
|
||||
'example_text' => [
|
||||
'de' => $this->faker->sentence(),
|
||||
'en' => $this->faker->sentence(),
|
||||
],
|
||||
'due_date' => $this->faker->dateTimeBetween('now', '+1 month'),
|
||||
'is_completed' => $this->faker->boolean(20), // 20% chance completed
|
||||
'collection_id' => null,
|
||||
@@ -53,4 +68,4 @@ class TaskFactory extends Factory
|
||||
$task->assignedEvents()->attach($event);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ namespace Database\Seeders;
|
||||
use App\Models\OAuthClient;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class OAuthClientSeeder extends Seeder
|
||||
@@ -14,14 +15,19 @@ class OAuthClientSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$clientId = 'tenant-admin-app';
|
||||
$serviceConfig = config('services.oauth.tenant_admin', []);
|
||||
|
||||
$clientId = $serviceConfig['id'] ?? 'tenant-admin-app';
|
||||
$tenantId = Tenant::where('slug', 'demo')->value('id')
|
||||
?? Tenant::query()->orderBy('id')->value('id');
|
||||
|
||||
$redirectUris = [
|
||||
'http://localhost:5174/auth/callback',
|
||||
'http://localhost:8000/auth/callback',
|
||||
];
|
||||
$redirectUris = Arr::wrap($serviceConfig['redirects'] ?? []);
|
||||
if (empty($redirectUris)) {
|
||||
$redirectUris = [
|
||||
'http://localhost:5173/event-admin/auth/callback',
|
||||
'http://localhost:8000/event-admin/auth/callback',
|
||||
];
|
||||
}
|
||||
|
||||
$scopes = [
|
||||
'tenant:read',
|
||||
|
||||
@@ -2,79 +2,293 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Emotion;
|
||||
use App\Models\EventType;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskCollection;
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\{Event, Task, TaskCollection, Tenant};
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TaskCollectionsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Get demo tenant
|
||||
$demoTenant = Tenant::where('slug', 'demo')->first();
|
||||
if (!$demoTenant) {
|
||||
$this->command->info('Demo tenant not found, skipping task collections seeding');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get demo event ID
|
||||
$demoEvent = Event::where('slug', 'demo-wedding-2025')->first();
|
||||
if (!$demoEvent) {
|
||||
$this->command->info('Demo event not found, skipping task collections seeding');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some task IDs for demo (assuming TasksSeeder was run)
|
||||
$taskIds = Task::where('tenant_id', $demoTenant->id)->limit(6)->get('id')->pluck('id')->toArray();
|
||||
if (empty($taskIds)) {
|
||||
$this->command->info('No tasks found, skipping task collections seeding');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Wedding Task Collection using Eloquent
|
||||
$weddingCollection = TaskCollection::create([
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'name' => [
|
||||
'de' => 'Hochzeitsaufgaben',
|
||||
'en' => 'Wedding Tasks'
|
||||
$collections = [
|
||||
[
|
||||
'slug' => 'wedding-classics',
|
||||
'event_type' => [
|
||||
'slug' => 'wedding',
|
||||
'name' => [
|
||||
'de' => 'Hochzeit',
|
||||
'en' => 'Wedding',
|
||||
],
|
||||
'icon' => 'lucide-heart',
|
||||
],
|
||||
'name' => [
|
||||
'de' => 'Hochzeitsklassiker',
|
||||
'en' => 'Wedding Classics',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Kuratierte Aufgaben rund um Trauung, Emotionen und besondere Momente.',
|
||||
'en' => 'Curated prompts for vows, emotions, and memorable wedding highlights.',
|
||||
],
|
||||
'is_default' => true,
|
||||
'position' => 10,
|
||||
'tasks' => [
|
||||
[
|
||||
'slug' => 'wedding-first-look',
|
||||
'title' => [
|
||||
'de' => 'Erster Blick des Brautpaares festhalten',
|
||||
'en' => 'Capture the couple’s first look',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Halte den Moment fest, in dem sich Braut und Bräutigam zum ersten Mal sehen.',
|
||||
'en' => 'Capture the moment when the bride and groom see each other for the first time.',
|
||||
],
|
||||
'example' => [
|
||||
'de' => 'Fotografiere die Reaktionen aus verschiedenen Blickwinkeln.',
|
||||
'en' => 'Photograph their reactions from different angles.',
|
||||
],
|
||||
'emotion' => [
|
||||
'name' => [
|
||||
'de' => 'Romantik',
|
||||
'en' => 'Romance',
|
||||
],
|
||||
'icon' => 'lucide-heart',
|
||||
'color' => '#ec4899',
|
||||
'sort_order' => 10,
|
||||
],
|
||||
'difficulty' => 'easy',
|
||||
'sort_order' => 10,
|
||||
],
|
||||
[
|
||||
'slug' => 'wedding-family-hug',
|
||||
'title' => [
|
||||
'de' => 'Familienumarmung organisieren',
|
||||
'en' => 'Organise a family group hug',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Bitte die wichtigsten Menschen, das Paar gleichzeitig zu umarmen.',
|
||||
'en' => 'Ask the closest friends and family to hug the couple at the same time.',
|
||||
],
|
||||
'example' => [
|
||||
'de' => 'Kombiniere die Umarmung mit einem Toast.',
|
||||
'en' => 'Combine the hug with a heartfelt toast.',
|
||||
],
|
||||
'emotion' => [
|
||||
'name' => [
|
||||
'de' => 'Freude',
|
||||
'en' => 'Joy',
|
||||
],
|
||||
'icon' => 'lucide-smile',
|
||||
'color' => '#f59e0b',
|
||||
'sort_order' => 20,
|
||||
],
|
||||
'difficulty' => 'medium',
|
||||
'sort_order' => 20,
|
||||
],
|
||||
[
|
||||
'slug' => 'wedding-midnight-sparkler',
|
||||
'title' => [
|
||||
'de' => 'Mitternachtsfunkeln mit Wunderkerzen',
|
||||
'en' => 'Midnight sparkler moment',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Verteile Wunderkerzen und schafft ein leuchtendes Spalier für das Paar.',
|
||||
'en' => 'Hand out sparklers and form a glowing aisle for the couple.',
|
||||
],
|
||||
'example' => [
|
||||
'de' => 'Koordiniere die Musik und kündige den Countdown an.',
|
||||
'en' => 'Coordinate music and announce a countdown.',
|
||||
],
|
||||
'emotion' => [
|
||||
'name' => [
|
||||
'de' => 'Ekstase',
|
||||
'en' => 'Euphoria',
|
||||
],
|
||||
'icon' => 'lucide-stars',
|
||||
'color' => '#6366f1',
|
||||
'sort_order' => 30,
|
||||
],
|
||||
'difficulty' => 'medium',
|
||||
'sort_order' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Spezielle Aufgaben für Hochzeitsgäste',
|
||||
'en' => 'Special tasks for wedding guests'
|
||||
[
|
||||
'slug' => 'birthday-celebration',
|
||||
'event_type' => [
|
||||
'slug' => 'birthday',
|
||||
'name' => [
|
||||
'de' => 'Geburtstag',
|
||||
'en' => 'Birthday',
|
||||
],
|
||||
'icon' => 'lucide-cake',
|
||||
],
|
||||
'name' => [
|
||||
'de' => 'Geburtstags-Highlights',
|
||||
'en' => 'Birthday Highlights',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Aufgaben für Überraschungen, Gratulationen und gemeinsames Feiern.',
|
||||
'en' => 'Prompts covering surprises, wishes, and shared celebrations.',
|
||||
],
|
||||
'is_default' => false,
|
||||
'position' => 20,
|
||||
'tasks' => [
|
||||
[
|
||||
'slug' => 'birthday-surprise-wall',
|
||||
'title' => [
|
||||
'de' => 'Überraschungswand mit Polaroids gestalten',
|
||||
'en' => 'Create a surprise wall filled with instant photos',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Sammle Schnappschüsse der Gäste und befestige sie als Fotowand.',
|
||||
'en' => 'Collect snapshots from guests and mount them on a photo wall.',
|
||||
],
|
||||
'example' => [
|
||||
'de' => 'Schreibe zu jedem Bild einen kurzen Gruß.',
|
||||
'en' => 'Add a short message to each picture.',
|
||||
],
|
||||
'emotion' => [
|
||||
'name' => [
|
||||
'de' => 'Nostalgie',
|
||||
'en' => 'Nostalgia',
|
||||
],
|
||||
'icon' => 'lucide-images',
|
||||
'color' => '#f97316',
|
||||
'sort_order' => 40,
|
||||
],
|
||||
'difficulty' => 'easy',
|
||||
'sort_order' => 10,
|
||||
],
|
||||
[
|
||||
'slug' => 'birthday-toast-circle',
|
||||
'title' => [
|
||||
'de' => 'Gratulationskreis mit kurzen Toasts',
|
||||
'en' => 'Circle of toasts',
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Bildet einen Kreis und bittet jede Person um einen 10-Sekunden-Toast.',
|
||||
'en' => 'Form a circle and ask everyone for a 10-second toast.',
|
||||
],
|
||||
'example' => [
|
||||
'de' => 'Nimm die Reaktionen als Video auf.',
|
||||
'en' => 'Record the reactions on video.',
|
||||
],
|
||||
'emotion' => [
|
||||
'name' => [
|
||||
'de' => 'Dankbarkeit',
|
||||
'en' => 'Gratitude',
|
||||
],
|
||||
'icon' => 'lucide-hands',
|
||||
'color' => '#22c55e',
|
||||
'sort_order' => 50,
|
||||
],
|
||||
'difficulty' => 'easy',
|
||||
'sort_order' => 20,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
];
|
||||
|
||||
// Assign first 4 tasks to wedding collection using Eloquent
|
||||
$weddingTasks = collect($taskIds)->take(4);
|
||||
$weddingCollection->tasks()->attach($weddingTasks);
|
||||
DB::transaction(function () use ($collections) {
|
||||
foreach ($collections as $definition) {
|
||||
$eventType = $this->ensureEventType($definition['event_type']);
|
||||
|
||||
// Link wedding collection to demo event using Eloquent
|
||||
$demoEvent->taskCollections()->attach($weddingCollection, ['sort_order' => 1]);
|
||||
$collection = TaskCollection::updateOrCreate(
|
||||
['slug' => $definition['slug']],
|
||||
[
|
||||
'tenant_id' => null,
|
||||
'event_type_id' => $eventType->id,
|
||||
'name_translations' => $definition['name'],
|
||||
'description_translations' => $definition['description'],
|
||||
'is_default' => $definition['is_default'] ?? false,
|
||||
'position' => $definition['position'] ?? 0,
|
||||
]
|
||||
);
|
||||
|
||||
// Create General Fun Tasks Collection (fallback) using Eloquent
|
||||
$funCollection = TaskCollection::create([
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'name' => [
|
||||
'de' => 'Spaß-Aufgaben',
|
||||
'en' => 'Fun Tasks'
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Allgemeine unterhaltsame Aufgaben',
|
||||
'en' => 'General entertaining tasks'
|
||||
],
|
||||
]);
|
||||
$syncPayload = [];
|
||||
|
||||
// Assign remaining tasks to fun collection using Eloquent
|
||||
$funTasks = collect($taskIds)->slice(4);
|
||||
$funCollection->tasks()->attach($funTasks);
|
||||
foreach ($definition['tasks'] as $taskDefinition) {
|
||||
$emotion = $this->ensureEmotion($taskDefinition['emotion'] ?? [], $eventType->id);
|
||||
|
||||
// Link fun collection to demo event as fallback using Eloquent
|
||||
$demoEvent->taskCollections()->attach($funCollection, ['sort_order' => 2]);
|
||||
$task = Task::updateOrCreate(
|
||||
['slug' => $taskDefinition['slug']],
|
||||
[
|
||||
'tenant_id' => null,
|
||||
'event_type_id' => $eventType->id,
|
||||
'collection_id' => $collection->id,
|
||||
'emotion_id' => $emotion?->id,
|
||||
'title' => $taskDefinition['title'],
|
||||
'description' => $taskDefinition['description'] ?? null,
|
||||
'example_text' => $taskDefinition['example'] ?? null,
|
||||
'difficulty' => $taskDefinition['difficulty'] ?? 'easy',
|
||||
'priority' => 'medium',
|
||||
'sort_order' => $taskDefinition['sort_order'] ?? 0,
|
||||
'is_active' => true,
|
||||
'is_completed' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$this->command->info("✅ Created 2 task collections with " . count($taskIds) . " tasks for demo event");
|
||||
$this->command->info("Wedding Collection ID: {$weddingCollection->id}");
|
||||
$this->command->info("Fun Collection ID: {$funCollection->id}");
|
||||
$syncPayload[$task->id] = ['sort_order' => $taskDefinition['sort_order'] ?? 0];
|
||||
}
|
||||
|
||||
if (! empty($syncPayload)) {
|
||||
$collection->tasks()->sync($syncPayload);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected function ensureEventType(array $definition): EventType
|
||||
{
|
||||
$payload = [
|
||||
'name' => $definition['name'],
|
||||
'icon' => $definition['icon'] ?? null,
|
||||
];
|
||||
|
||||
return EventType::updateOrCreate(
|
||||
['slug' => $definition['slug']],
|
||||
$payload
|
||||
);
|
||||
}
|
||||
|
||||
protected function ensureEmotion(array $definition, ?int $eventTypeId): ?Emotion
|
||||
{
|
||||
if (empty($definition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = Emotion::query();
|
||||
|
||||
$name = $definition['name'] ?? [];
|
||||
|
||||
if (isset($name['en'])) {
|
||||
$query->orWhere('name->en', $name['en']);
|
||||
}
|
||||
|
||||
if (isset($name['de'])) {
|
||||
$query->orWhere('name->de', $name['de']);
|
||||
}
|
||||
|
||||
$emotion = $query->first();
|
||||
|
||||
if (! $emotion) {
|
||||
$emotion = Emotion::create([
|
||||
'name' => $name,
|
||||
'icon' => $definition['icon'] ?? 'lucide-smile',
|
||||
'color' => $definition['color'] ?? '#6366f1',
|
||||
'description' => $definition['description'] ?? null,
|
||||
'sort_order' => $definition['sort_order'] ?? 0,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($eventTypeId && ! $emotion->eventTypes()->where('event_type_id', $eventTypeId)->exists()) {
|
||||
$emotion->eventTypes()->attach($eventTypeId);
|
||||
}
|
||||
|
||||
return $emotion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\{Emotion, Task, EventType};
|
||||
|
||||
class TasksSeeder extends Seeder
|
||||
@@ -43,10 +44,11 @@ class TasksSeeder extends Seeder
|
||||
$emotion = Emotion::where('name->de', $emotionNameDe)->first();
|
||||
if (!$emotion) continue;
|
||||
foreach ($tasks as $t) {
|
||||
$slugBase = Str::slug($t['title']['en'] ?? $t['title']['de']);
|
||||
$slug = $slugBase ? $slugBase . '-' . $emotion->id : Str::uuid()->toString();
|
||||
|
||||
Task::updateOrCreate([
|
||||
'emotion_id' => $emotion->id,
|
||||
'title->de' => $t['title']['de'],
|
||||
'tenant_id' => $demoTenant->id
|
||||
'slug' => $slug,
|
||||
], [
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'emotion_id' => $emotion->id,
|
||||
@@ -55,6 +57,7 @@ class TasksSeeder extends Seeder
|
||||
'description' => $t['description'],
|
||||
'difficulty' => $t['difficulty'],
|
||||
'is_active' => true,
|
||||
'sort_order' => $t['sort_order'] ?? 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user