feat: registration form fields, section preferences, tag sync & schema updates

Implement EAV system for dynamic event-specific registration fields
with organisation-level templates, person section preferences with
priority ranking, and TagSyncService for deferred tag_picker sync.

New tables: registration_field_templates, registration_form_fields,
person_field_values, person_section_preferences.
New columns: persons.remarks, events.registration_show_section_preferences,
events.registration_show_availability.

58 tests, 126 assertions — all 432 tests pass (zero regressions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 22:10:16 +02:00
parent fcff3b0344
commit f6e3568011
51 changed files with 3774 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
<?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('persons', function (Blueprint $table) {
$table->text('remarks')->nullable()->after('admin_notes');
});
}
public function down(): void
{
Schema::table('persons', function (Blueprint $table) {
$table->dropColumn('remarks');
});
}
};

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::table('events', function (Blueprint $table) {
$table->boolean('registration_show_section_preferences')->default(true)->after('registration_logo_url');
$table->boolean('registration_show_availability')->default(true)->after('registration_show_section_preferences');
});
}
public function down(): void
{
Schema::table('events', function (Blueprint $table) {
$table->dropColumn([
'registration_show_section_preferences',
'registration_show_availability',
]);
});
}
};

View File

@@ -0,0 +1,41 @@
<?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('registration_field_templates', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
$table->string('label');
$table->string('slug', 100);
$table->string('field_type', 50);
$table->json('options')->nullable();
$table->string('tag_category', 50)->nullable();
$table->boolean('is_required')->default(false);
$table->boolean('is_filterable')->default(false);
$table->boolean('is_portal_visible')->default(true);
$table->boolean('is_admin_only')->default(false);
$table->string('section', 100)->nullable();
$table->text('help_text')->nullable();
$table->integer('sort_order')->default(0);
$table->boolean('is_system')->default(false);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->unique(['organisation_id', 'slug'], 'rft_org_slug_unique');
$table->index(['organisation_id', 'is_active', 'sort_order'], 'rft_org_active_order_index');
});
}
public function down(): void
{
Schema::dropIfExists('registration_field_templates');
}
};

View File

@@ -0,0 +1,40 @@
<?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('registration_form_fields', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->foreignUlid('event_id')->constrained()->cascadeOnDelete();
$table->string('label');
$table->string('slug', 100);
$table->string('field_type', 50);
$table->json('options')->nullable();
$table->string('tag_category', 50)->nullable();
$table->boolean('is_required')->default(false);
$table->boolean('is_portal_visible')->default(true);
$table->boolean('is_admin_only')->default(false);
$table->boolean('is_filterable')->default(false);
$table->string('section', 100)->nullable();
$table->text('help_text')->nullable();
$table->integer('sort_order')->default(0);
$table->timestamps();
$table->unique(['event_id', 'slug'], 'rff_event_slug_unique');
$table->index(['event_id', 'sort_order'], 'rff_event_order_index');
$table->index(['event_id', 'is_portal_visible', 'sort_order'], 'rff_event_visible_order_index');
});
}
public function down(): void
{
Schema::dropIfExists('registration_form_fields');
}
};

View File

@@ -0,0 +1,29 @@
<?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('person_field_values', function (Blueprint $table) {
$table->id();
$table->foreignUlid('person_id')->constrained('persons')->cascadeOnDelete();
$table->foreignUlid('registration_form_field_id')->nullable()->constrained('registration_form_fields')->nullOnDelete();
$table->text('value')->nullable();
$table->json('selected_options')->nullable();
$table->unique(['person_id', 'registration_form_field_id'], 'pfv_person_field_unique');
$table->index('registration_form_field_id', 'pfv_field_index');
});
}
public function down(): void
{
Schema::dropIfExists('person_field_values');
}
};

View File

@@ -0,0 +1,29 @@
<?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('person_section_preferences', function (Blueprint $table) {
$table->id();
$table->foreignUlid('person_id')->constrained('persons')->cascadeOnDelete();
$table->foreignUlid('festival_section_id')->constrained('festival_sections')->cascadeOnDelete();
$table->tinyInteger('priority');
$table->unique(['person_id', 'festival_section_id'], 'psp_person_section_unique');
$table->index(['festival_section_id', 'priority']);
$table->index('person_id', 'psp_person_index');
});
}
public function down(): void
{
Schema::dropIfExists('person_section_preferences');
}
};