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:
82
api/database/factories/RegistrationFieldTemplateFactory.php
Normal file
82
api/database/factories/RegistrationFieldTemplateFactory.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\RegistrationFieldTemplate;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** @extends Factory<RegistrationFieldTemplate> */
|
||||
final class RegistrationFieldTemplateFactory extends Factory
|
||||
{
|
||||
protected $model = RegistrationFieldTemplate::class;
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function definition(): array
|
||||
{
|
||||
$label = fake('nl_NL')->unique()->words(2, true);
|
||||
|
||||
return [
|
||||
'organisation_id' => Organisation::factory(),
|
||||
'label' => ucfirst($label),
|
||||
'slug' => Str::slug($label),
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'options' => null,
|
||||
'tag_category' => null,
|
||||
'is_required' => false,
|
||||
'is_filterable' => false,
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
'section' => null,
|
||||
'help_text' => null,
|
||||
'sort_order' => fake()->numberBetween(0, 20),
|
||||
'is_system' => false,
|
||||
'is_active' => true,
|
||||
];
|
||||
}
|
||||
|
||||
public function system(): static
|
||||
{
|
||||
return $this->state(fn () => ['is_system' => true]);
|
||||
}
|
||||
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn () => ['is_active' => false]);
|
||||
}
|
||||
|
||||
public function selectField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Shirtmaat',
|
||||
'slug' => 'shirtmaat',
|
||||
'field_type' => RegistrationFieldType::SELECT,
|
||||
'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function booleanField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'EHBO / BHV diploma',
|
||||
'slug' => 'ehbo-bhv-diploma',
|
||||
'field_type' => RegistrationFieldType::BOOLEAN,
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function tagPickerField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Certificaten & vaardigheden',
|
||||
'slug' => 'certificaten-vaardigheden',
|
||||
'field_type' => RegistrationFieldType::TAG_PICKER,
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
113
api/database/factories/RegistrationFormFieldFactory.php
Normal file
113
api/database/factories/RegistrationFormFieldFactory.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Event;
|
||||
use App\Models\RegistrationFormField;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** @extends Factory<RegistrationFormField> */
|
||||
final class RegistrationFormFieldFactory extends Factory
|
||||
{
|
||||
protected $model = RegistrationFormField::class;
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function definition(): array
|
||||
{
|
||||
$label = fake('nl_NL')->unique()->words(2, true);
|
||||
|
||||
return [
|
||||
'event_id' => Event::factory(),
|
||||
'label' => ucfirst($label),
|
||||
'slug' => Str::slug($label),
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'options' => null,
|
||||
'tag_category' => null,
|
||||
'is_required' => false,
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
'is_filterable' => false,
|
||||
'section' => null,
|
||||
'help_text' => null,
|
||||
'sort_order' => fake()->numberBetween(0, 20),
|
||||
];
|
||||
}
|
||||
|
||||
public function textField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Noodcontact naam',
|
||||
'slug' => 'noodcontact-naam',
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'section' => 'Noodcontact',
|
||||
]);
|
||||
}
|
||||
|
||||
public function selectField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Shirtmaat',
|
||||
'slug' => 'shirtmaat',
|
||||
'field_type' => RegistrationFieldType::SELECT,
|
||||
'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function multiselectField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Dieetwensen',
|
||||
'slug' => 'dieetwensen',
|
||||
'field_type' => RegistrationFieldType::MULTISELECT,
|
||||
'options' => ['Vegetarisch', 'Veganistisch', 'Halal', 'Glutenvrij', 'Lactosevrij', 'Geen pinda\'s', 'Geen noten'],
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function booleanField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Toestemming gegevensverwerking',
|
||||
'slug' => 'toestemming-gegevensverwerking',
|
||||
'field_type' => RegistrationFieldType::BOOLEAN,
|
||||
'is_required' => true,
|
||||
'section' => 'Toestemming',
|
||||
'help_text' => 'Ik geef toestemming voor de verwerking van mijn persoonsgegevens conform de AVG.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function tagPickerField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Vaardigheden',
|
||||
'slug' => 'vaardigheden',
|
||||
'field_type' => RegistrationFieldType::TAG_PICKER,
|
||||
'is_filterable' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function radioField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Vergoeding',
|
||||
'slug' => 'vergoeding',
|
||||
'field_type' => RegistrationFieldType::RADIO,
|
||||
'options' => ['Pro Deo', 'Entreeticket', 'Vrijwilligersvergoeding'],
|
||||
'section' => 'Vergoeding',
|
||||
]);
|
||||
}
|
||||
|
||||
public function textareaField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Opmerkingen',
|
||||
'slug' => 'opmerkingen',
|
||||
'field_type' => RegistrationFieldType::TEXTAREA,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -150,7 +150,11 @@ class DevSeeder extends Seeder
|
||||
$this->personTags[$data['name']] = $tag;
|
||||
}
|
||||
|
||||
$this->command->info(' Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags created');
|
||||
// ── Registration Field Templates (system defaults) ──
|
||||
|
||||
\App\Services\RegistrationFieldTemplateService::seedSystemTemplates($this->org);
|
||||
|
||||
$this->command->info(' Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags, 11 registration templates created');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user