feat: schema v1.7 + sections/shifts frontend

- Universeel festival/event model (parent_event_id, event_type)
- event_person_activations pivot tabel
- Event model: parent/children relaties + helper scopes
- DevSeeder: festival structuur met sub-events
- Sections & Shifts frontend (twee-kolom layout)
- BACKLOG.md aangemaakt met 22 gedocumenteerde wensen
This commit is contained in:
2026-04-08 07:23:56 +02:00
parent 6f69b30fb6
commit 6848bc2c49
19 changed files with 2560 additions and 87 deletions

View File

@@ -21,12 +21,19 @@ final class Event extends Model
protected $fillable = [
'organisation_id',
'parent_event_id',
'name',
'slug',
'start_date',
'end_date',
'timezone',
'status',
'event_type',
'event_type_label',
'sub_event_label',
'is_recurring',
'recurrence_rule',
'recurrence_exceptions',
];
protected function casts(): array
@@ -34,6 +41,9 @@ final class Event extends Model
return [
'start_date' => 'date',
'end_date' => 'date',
'is_recurring' => 'boolean',
'recurrence_exceptions' => 'array',
'event_type' => 'string',
];
}
@@ -79,6 +89,68 @@ final class Event extends Model
return $this->hasMany(CrowdList::class);
}
public function parent(): BelongsTo
{
return $this->belongsTo(Event::class, 'parent_event_id');
}
public function children(): HasMany
{
return $this->hasMany(Event::class, 'parent_event_id')
->orderBy('start_date')
->orderBy('name');
}
// ----- Scopes -----
public function scopeTopLevel(Builder $query): Builder
{
return $query->whereNull('parent_event_id');
}
public function scopeChildren(Builder $query): Builder
{
return $query->whereNotNull('parent_event_id');
}
public function scopeFestivals(Builder $query): Builder
{
return $query->whereIn('event_type', ['festival', 'series']);
}
public function scopeWithChildren(Builder $query): Builder
{
return $query->where(function (Builder $q) {
$q->whereIn('id', function ($sub) {
$sub->select('id')->from('events')->whereNull('parent_event_id');
})->orWhereIn('parent_event_id', function ($sub) {
$sub->select('id')->from('events')->whereNull('parent_event_id');
});
});
}
// ----- Helpers -----
public function isFestival(): bool
{
return $this->event_type !== 'event' && $this->parent_event_id === null;
}
public function isSubEvent(): bool
{
return $this->parent_event_id !== null;
}
public function isFlatEvent(): bool
{
return $this->parent_event_id === null && $this->children()->count() === 0;
}
public function hasChildren(): bool
{
return $this->children()->exists();
}
public function scopeDraft(Builder $query): Builder
{
return $query->where('status', 'draft');

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('events', function (Blueprint $table) {
$table->foreignUlid('parent_event_id')
->nullable()
->after('organisation_id')
->constrained('events')
->nullOnDelete();
$table->enum('event_type', ['event', 'festival', 'series'])
->default('event')
->after('status');
$table->string('event_type_label')
->nullable()
->after('event_type');
$table->string('sub_event_label')
->nullable()
->after('event_type_label');
$table->boolean('is_recurring')
->default(false)
->after('sub_event_label');
$table->string('recurrence_rule')
->nullable()
->after('is_recurring');
$table->json('recurrence_exceptions')
->nullable()
->after('recurrence_rule');
$table->index('parent_event_id');
});
}
public function down(): void
{
Schema::table('events', function (Blueprint $table) {
$table->dropForeign(['parent_event_id']);
$table->dropIndex(['parent_event_id']);
$table->dropColumn([
'parent_event_id',
'event_type',
'event_type_label',
'sub_event_label',
'is_recurring',
'recurrence_rule',
'recurrence_exceptions',
]);
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('event_person_activations', function (Blueprint $table) {
$table->id();
$table->foreignUlid('event_id')->constrained('events')->cascadeOnDelete();
$table->foreignUlid('person_id')->constrained('persons')->cascadeOnDelete();
$table->unique(['event_id', 'person_id']);
$table->index('person_id');
$table->index('event_id');
});
}
public function down(): void
{
Schema::dropIfExists('event_person_activations');
}
};

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Database\Seeders;
use App\Models\CrowdType;
use App\Models\Event;
use App\Models\Organisation;
use App\Models\User;
use Illuminate\Database\Seeder;
@@ -99,5 +100,59 @@ class DevSeeder extends Seeder
],
);
}
// 5. Flat event (backward compatible single event)
Event::firstOrCreate(
['organisation_id' => $org->id, 'slug' => 'test-event-01'],
[
'name' => 'Test Event 01',
'start_date' => '2026-08-15',
'end_date' => '2026-08-15',
'status' => 'draft',
'event_type' => 'event',
'parent_event_id' => null,
],
);
// 6. Festival with sub-events
$festival = Event::firstOrCreate(
['organisation_id' => $org->id, 'slug' => 'echt-zomer-feesten-2026'],
[
'name' => 'Echt Zomer Feesten 2026',
'start_date' => '2026-07-10',
'end_date' => '2026-07-11',
'status' => 'draft',
'event_type' => 'festival',
'event_type_label' => 'Festival',
'sub_event_label' => 'Programmaonderdeel',
'parent_event_id' => null,
],
);
// Sub-event 1: Dance Festival
Event::firstOrCreate(
['organisation_id' => $org->id, 'slug' => 'dance-festival-2026'],
[
'name' => 'Dance Festival',
'start_date' => '2026-07-10',
'end_date' => '2026-07-10',
'status' => 'draft',
'event_type' => 'event',
'parent_event_id' => $festival->id,
],
);
// Sub-event 2: Zomerfestival
Event::firstOrCreate(
['organisation_id' => $org->id, 'slug' => 'zomerfestival-2026'],
[
'name' => 'Zomerfestival',
'start_date' => '2026-07-11',
'end_date' => '2026-07-11',
'status' => 'draft',
'event_type' => 'event',
'parent_event_id' => $festival->id,
],
);
}
}