feat: HEADING field type for registration forms — replace section property with structural field
Replace the per-field `section` text property with a dedicated HEADING field type that organizers add as a separate block for visual grouping. Also fixes duplicate heading bug on portal radio fields, replaces cramped VBtnToggle with VSelect for field width, and adds grouped field type dropdown with structure/input categories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,8 @@ enum FieldDisplayWidth: string
|
||||
RegistrationFieldType::RADIO,
|
||||
RegistrationFieldType::CHECKBOX,
|
||||
RegistrationFieldType::MULTISELECT,
|
||||
RegistrationFieldType::TAG_PICKER => self::FULL,
|
||||
RegistrationFieldType::TAG_PICKER,
|
||||
RegistrationFieldType::HEADING => self::FULL,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ enum RegistrationFieldType: string
|
||||
case BOOLEAN = 'boolean';
|
||||
case NUMBER = 'number';
|
||||
case TAG_PICKER = 'tag_picker';
|
||||
case HEADING = 'heading';
|
||||
|
||||
public function isMultiValue(): bool
|
||||
{
|
||||
@@ -28,6 +29,11 @@ enum RegistrationFieldType: string
|
||||
|
||||
public function prohibitsOptions(): bool
|
||||
{
|
||||
return in_array($this, [self::TEXT, self::TEXTAREA, self::BOOLEAN, self::NUMBER, self::TAG_PICKER], true);
|
||||
return in_array($this, [self::TEXT, self::TEXTAREA, self::BOOLEAN, self::NUMBER, self::TAG_PICKER, self::HEADING], true);
|
||||
}
|
||||
|
||||
public function isStructural(): bool
|
||||
{
|
||||
return $this === self::HEADING;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ final class PublicRegistrationDataController extends Controller
|
||||
'normalized_options' => $field->normalized_options,
|
||||
'tag_category' => $field->tag_category,
|
||||
'is_required' => $field->is_required,
|
||||
'section' => $field->section,
|
||||
'help_text' => $field->help_text,
|
||||
'display_width' => $field->display_width->value,
|
||||
];
|
||||
|
||||
@@ -22,9 +22,19 @@ final class StoreRegistrationFieldTemplateRequest extends FormRequest
|
||||
$fieldType = $this->input('field_type');
|
||||
$type = RegistrationFieldType::tryFrom($fieldType);
|
||||
|
||||
return [
|
||||
$rules = [
|
||||
'label' => ['required', 'string', 'max:255'],
|
||||
'field_type' => ['required', Rule::in(array_column(RegistrationFieldType::cases(), 'value'))],
|
||||
'help_text' => ['nullable', 'string', 'max:5000'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
'display_width' => ['sometimes', Rule::in(array_column(FieldDisplayWidth::cases(), 'value'))],
|
||||
];
|
||||
|
||||
if ($type?->isStructural()) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
return array_merge($rules, [
|
||||
'options' => [
|
||||
$type?->requiresOptions() ? 'required' : 'nullable',
|
||||
$type?->prohibitsOptions() ? 'prohibited' : 'nullable',
|
||||
@@ -42,10 +52,6 @@ final class StoreRegistrationFieldTemplateRequest extends FormRequest
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
'is_admin_only' => ['nullable', 'boolean'],
|
||||
'section' => ['nullable', 'string', 'max:100'],
|
||||
'help_text' => ['nullable', 'string', 'max:5000'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
'display_width' => ['sometimes', Rule::in(array_column(FieldDisplayWidth::cases(), 'value'))],
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,19 @@ final class StoreRegistrationFormFieldRequest extends FormRequest
|
||||
$fieldType = $this->input('field_type');
|
||||
$type = RegistrationFieldType::tryFrom($fieldType);
|
||||
|
||||
return [
|
||||
$rules = [
|
||||
'label' => ['required', 'string', 'max:255'],
|
||||
'field_type' => ['required', Rule::in(array_column(RegistrationFieldType::cases(), 'value'))],
|
||||
'help_text' => ['nullable', 'string', 'max:5000'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
'display_width' => ['sometimes', Rule::in(array_column(FieldDisplayWidth::cases(), 'value'))],
|
||||
];
|
||||
|
||||
if ($type?->isStructural()) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
return array_merge($rules, [
|
||||
'options' => [
|
||||
$type?->requiresOptions() ? 'required' : 'nullable',
|
||||
$type?->prohibitsOptions() ? 'prohibited' : 'nullable',
|
||||
@@ -42,10 +52,6 @@ final class StoreRegistrationFormFieldRequest extends FormRequest
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
'is_admin_only' => ['nullable', 'boolean'],
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
'section' => ['nullable', 'string', 'max:100'],
|
||||
'help_text' => ['nullable', 'string', 'max:5000'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
'display_width' => ['sometimes', Rule::in(array_column(FieldDisplayWidth::cases(), 'value'))],
|
||||
];
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ final class UpdateRegistrationFieldTemplateRequest extends FormRequest
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
'is_admin_only' => ['nullable', 'boolean'],
|
||||
'section' => ['nullable', 'string', 'max:100'],
|
||||
'help_text' => ['nullable', 'string', 'max:5000'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
'display_width' => ['sometimes', Rule::in(array_column(FieldDisplayWidth::cases(), 'value'))],
|
||||
|
||||
@@ -29,7 +29,6 @@ final class UpdateRegistrationFormFieldRequest extends FormRequest
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
'is_admin_only' => ['nullable', 'boolean'],
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
'section' => ['nullable', 'string', 'max:100'],
|
||||
'help_text' => ['nullable', 'string', 'max:5000'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0'],
|
||||
'display_width' => ['sometimes', Rule::in(array_column(FieldDisplayWidth::cases(), 'value'))],
|
||||
|
||||
@@ -24,7 +24,6 @@ final class RegistrationFieldTemplateResource extends JsonResource
|
||||
'is_filterable' => $this->is_filterable,
|
||||
'is_portal_visible' => $this->is_portal_visible,
|
||||
'is_admin_only' => $this->is_admin_only,
|
||||
'section' => $this->section,
|
||||
'help_text' => $this->help_text,
|
||||
'sort_order' => $this->sort_order,
|
||||
'display_width' => $this->display_width->value,
|
||||
|
||||
@@ -26,7 +26,6 @@ final class RegistrationFormFieldResource extends JsonResource
|
||||
'is_portal_visible' => $this->is_portal_visible,
|
||||
'is_admin_only' => $this->is_admin_only,
|
||||
'is_filterable' => $this->is_filterable,
|
||||
'section' => $this->section,
|
||||
'help_text' => $this->help_text,
|
||||
'sort_order' => $this->sort_order,
|
||||
'display_width' => $this->display_width->value,
|
||||
|
||||
@@ -34,7 +34,6 @@ final class RegistrationFieldTemplate extends Model
|
||||
'is_filterable',
|
||||
'is_portal_visible',
|
||||
'is_admin_only',
|
||||
'section',
|
||||
'help_text',
|
||||
'sort_order',
|
||||
'display_width',
|
||||
|
||||
@@ -38,7 +38,6 @@ final class RegistrationFormField extends Model
|
||||
'is_portal_visible',
|
||||
'is_admin_only',
|
||||
'is_filterable',
|
||||
'section',
|
||||
'help_text',
|
||||
'sort_order',
|
||||
'display_width',
|
||||
|
||||
@@ -115,7 +115,6 @@ final class RegistrationFieldTemplateService
|
||||
'is_portal_visible' => $template->is_portal_visible,
|
||||
'is_admin_only' => $template->is_admin_only,
|
||||
'is_filterable' => $template->is_filterable,
|
||||
'section' => $template->section,
|
||||
'help_text' => $template->help_text,
|
||||
'sort_order' => $maxOrder + 1,
|
||||
'display_width' => $template->display_width,
|
||||
@@ -140,21 +139,26 @@ final class RegistrationFieldTemplateService
|
||||
public static function seedSystemTemplates(Organisation $organisation): void
|
||||
{
|
||||
$templates = [
|
||||
['label' => 'Shirtmaat', 'field_type' => 'select', 'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'], 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 1],
|
||||
['label' => 'Dieetwensen', 'field_type' => 'multiselect', 'options' => ['Vegetarisch', 'Veganistisch', 'Halal', 'Glutenvrij', 'Lactosevrij', 'Geen pinda\'s', 'Geen noten'], 'is_filterable' => true, 'display_width' => 'full', 'sort_order' => 2],
|
||||
['label' => 'Vergoeding', 'field_type' => 'radio', 'options' => [
|
||||
['label' => 'Persoonlijke voorkeuren', 'field_type' => 'heading', 'help_text' => 'Vertel ons wat we over jou moeten weten', 'display_width' => 'full', 'sort_order' => 1],
|
||||
['label' => 'Shirtmaat', 'field_type' => 'select', 'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'], 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 2],
|
||||
['label' => 'Dieetwensen', 'field_type' => 'multiselect', 'options' => ['Vegetarisch', 'Veganistisch', 'Halal', 'Glutenvrij', 'Lactosevrij', 'Geen pinda\'s', 'Geen noten'], 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 3],
|
||||
['label' => 'Vergoeding', 'field_type' => 'heading', 'help_text' => 'Kies hoe je wilt worden bedankt voor je inzet', 'display_width' => 'full', 'sort_order' => 4],
|
||||
['label' => 'Vergoedingstype', 'field_type' => 'radio', 'options' => [
|
||||
['label' => 'Pro Deo', 'description' => 'Je werkt als vrijwilliger zonder financiële vergoeding'],
|
||||
['label' => 'Entreeticket', 'description' => 'Je ontvangt een gratis festivalticket als dank voor je inzet'],
|
||||
['label' => 'Vrijwilligersvergoeding', 'description' => 'Je ontvangt een vergoeding conform de vrijwilligersregeling'],
|
||||
], 'section' => 'Vergoeding', 'display_width' => 'full', 'sort_order' => 3],
|
||||
['label' => 'Toestemming gegevensverwerking', 'field_type' => 'boolean', 'is_required' => true, 'section' => 'Toestemming', 'help_text' => 'Ik geef toestemming voor de verwerking van mijn persoonsgegevens ten behoeve van de organisatie van dit evenement, conform de Algemene Verordening Gegevensbescherming (AVG).', 'display_width' => 'full', 'sort_order' => 4],
|
||||
['label' => 'Noodcontact naam', 'field_type' => 'text', 'section' => 'Noodcontact', 'display_width' => 'half', 'sort_order' => 5],
|
||||
['label' => 'Noodcontact telefoon', 'field_type' => 'text', 'section' => 'Noodcontact', 'display_width' => 'half', 'sort_order' => 6],
|
||||
['label' => 'EHBO / BHV diploma', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 7],
|
||||
['label' => 'Rijbewijs', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 8],
|
||||
['label' => 'Eerder vrijwilliger geweest', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 9],
|
||||
['label' => 'Certificaten & vaardigheden', 'field_type' => 'tag_picker', 'tag_category' => null, 'is_filterable' => true, 'display_width' => 'full', 'sort_order' => 10],
|
||||
['label' => 'Opmerkingen', 'field_type' => 'textarea', 'display_width' => 'full', 'sort_order' => 11],
|
||||
], 'is_required' => true, 'display_width' => 'full', 'sort_order' => 5],
|
||||
['label' => 'Noodcontact', 'field_type' => 'heading', 'help_text' => 'Wie kunnen we bereiken bij calamiteiten?', 'display_width' => 'full', 'sort_order' => 6],
|
||||
['label' => 'Noodcontact naam', 'field_type' => 'text', 'display_width' => 'half', 'sort_order' => 7],
|
||||
['label' => 'Noodcontact telefoon', 'field_type' => 'text', 'display_width' => 'half', 'sort_order' => 8],
|
||||
['label' => 'Ervaring & vaardigheden', 'field_type' => 'heading', 'help_text' => 'Welke diploma\'s en skills heb je?', 'display_width' => 'full', 'sort_order' => 9],
|
||||
['label' => 'EHBO / BHV diploma', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 10],
|
||||
['label' => 'Rijbewijs', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 11],
|
||||
['label' => 'Eerder vrijwilliger geweest', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 12],
|
||||
['label' => 'Certificaten & vaardigheden', 'field_type' => 'tag_picker', 'tag_category' => null, 'is_filterable' => true, 'display_width' => 'full', 'sort_order' => 13],
|
||||
['label' => 'Aanvullende informatie', 'field_type' => 'heading', 'display_width' => 'full', 'sort_order' => 14],
|
||||
['label' => 'Toestemming gegevensverwerking', 'field_type' => 'boolean', 'is_required' => true, 'help_text' => 'Ik geef toestemming voor de verwerking van mijn persoonsgegevens ten behoeve van de organisatie van dit evenement, conform de Algemene Verordening Gegevensbescherming (AVG).', 'display_width' => 'full', 'sort_order' => 15],
|
||||
['label' => 'Opmerkingen', 'field_type' => 'textarea', 'display_width' => 'full', 'sort_order' => 16],
|
||||
];
|
||||
|
||||
foreach ($templates as $data) {
|
||||
|
||||
@@ -111,7 +111,7 @@ final class RegistrationFormFieldService
|
||||
DB::transaction(function () use ($person, $values, $fields): void {
|
||||
foreach ($values as $slug => $rawValue) {
|
||||
$field = $fields->get($slug);
|
||||
if ($field === null) {
|
||||
if ($field === null || $field->field_type->isStructural()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,6 @@ final class RegistrationFormFieldService
|
||||
'is_portal_visible' => $sourceField->is_portal_visible,
|
||||
'is_admin_only' => $sourceField->is_admin_only,
|
||||
'is_filterable' => $sourceField->is_filterable,
|
||||
'section' => $sourceField->section,
|
||||
'help_text' => $sourceField->help_text,
|
||||
'sort_order' => ++$maxOrder,
|
||||
'display_width' => $sourceField->display_width,
|
||||
|
||||
@@ -32,7 +32,6 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'is_filterable' => false,
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
'section' => null,
|
||||
'help_text' => null,
|
||||
'sort_order' => fake()->numberBetween(0, 20),
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
@@ -84,4 +83,15 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
|
||||
public function headingField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Persoonlijke voorkeuren',
|
||||
'slug' => 'persoonlijke-voorkeuren',
|
||||
'field_type' => RegistrationFieldType::HEADING,
|
||||
'help_text' => 'Vertel ons wat we over jou moeten weten',
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
'is_filterable' => false,
|
||||
'section' => null,
|
||||
'help_text' => null,
|
||||
'sort_order' => fake()->numberBetween(0, 20),
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
@@ -45,7 +44,6 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'label' => 'Noodcontact naam',
|
||||
'slug' => 'noodcontact-naam',
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'section' => 'Noodcontact',
|
||||
'display_width' => FieldDisplayWidth::HALF,
|
||||
]);
|
||||
}
|
||||
@@ -81,7 +79,6 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'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.',
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
@@ -109,7 +106,6 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
['label' => 'Entreeticket', 'description' => 'Je ontvangt een gratis festivalticket als dank voor je inzet'],
|
||||
['label' => 'Vrijwilligersvergoeding', 'description' => 'Je ontvangt een vergoeding conform de vrijwilligersregeling'],
|
||||
],
|
||||
'section' => 'Vergoeding',
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
@@ -123,4 +119,15 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
|
||||
public function headingField(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'label' => 'Persoonlijke voorkeuren',
|
||||
'slug' => 'persoonlijke-voorkeuren',
|
||||
'field_type' => RegistrationFieldType::HEADING,
|
||||
'help_text' => 'Vertel ons wat we over jou moeten weten',
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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('registration_field_templates', function (Blueprint $table) {
|
||||
$table->dropColumn('section');
|
||||
});
|
||||
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->dropColumn('section');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->string('section', 100)->nullable()->after('is_admin_only');
|
||||
});
|
||||
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->string('section', 100)->nullable()->after('is_filterable');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -156,7 +156,7 @@ class DevSeeder extends Seeder
|
||||
|
||||
\App\Services\RegistrationFieldTemplateService::seedSystemTemplates($this->org);
|
||||
|
||||
$this->command->info(' Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags, 11 registration templates created');
|
||||
$this->command->info(' Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags, 16 registration templates created');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ class RegistrationFieldTemplateTest extends TestCase
|
||||
->where('is_system', true)
|
||||
->count();
|
||||
|
||||
$this->assertEquals(11, $count);
|
||||
$this->assertEquals(16, $count);
|
||||
}
|
||||
|
||||
public function test_create_field_from_template(): void
|
||||
|
||||
@@ -492,4 +492,58 @@ class RegistrationFormFieldTest extends TestCase
|
||||
$this->assertEquals('full', $field['display_width']);
|
||||
$this->assertNotNull($field['normalized_options']);
|
||||
}
|
||||
|
||||
public function test_store_heading_field_without_options(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Persoonlijke voorkeuren',
|
||||
'field_type' => 'heading',
|
||||
'help_text' => 'Vertel ons wat we over jou moeten weten',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.field_type', 'heading')
|
||||
->assertJsonPath('data.label', 'Persoonlijke voorkeuren')
|
||||
->assertJsonPath('data.help_text', 'Vertel ons wat we over jou moeten weten')
|
||||
->assertJsonPath('data.display_width', 'full');
|
||||
}
|
||||
|
||||
public function test_heading_field_skips_options_validation(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Sectiekop',
|
||||
'field_type' => 'heading',
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
}
|
||||
|
||||
public function test_heading_field_display_width_defaults_to_full(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Noodcontact',
|
||||
'field_type' => 'heading',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.display_width', 'full');
|
||||
}
|
||||
|
||||
public function test_seeder_creates_heading_fields(): void
|
||||
{
|
||||
\App\Services\RegistrationFieldTemplateService::seedSystemTemplates($this->organisation);
|
||||
|
||||
$headingCount = \App\Models\RegistrationFieldTemplate::where('organisation_id', $this->organisation->id)
|
||||
->where('is_system', true)
|
||||
->where('field_type', 'heading')
|
||||
->count();
|
||||
|
||||
$this->assertEquals(5, $headingCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,55 @@ function formatOptions(options: NormalizedOption[] | null): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Heading variant -->
|
||||
<div
|
||||
v-if="field.field_type === 'heading'"
|
||||
class="d-flex align-center rounded pa-3 mb-2"
|
||||
style="background: rgba(var(--v-theme-on-surface), 0.04);"
|
||||
>
|
||||
<div class="drag-handle d-flex align-center cursor-grab me-2">
|
||||
<VIcon
|
||||
icon="tabler-grip-vertical"
|
||||
size="20"
|
||||
class="text-disabled"
|
||||
/>
|
||||
</div>
|
||||
<VIcon
|
||||
icon="tabler-heading"
|
||||
size="18"
|
||||
class="me-2 text-medium-emphasis"
|
||||
/>
|
||||
<div class="flex-grow-1 min-width-0">
|
||||
<div class="text-subtitle-2 font-weight-medium">
|
||||
{{ field.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="field.help_text"
|
||||
class="text-caption text-medium-emphasis"
|
||||
>
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="small"
|
||||
title="Bewerken"
|
||||
@click="$emit('edit', field)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
title="Verwijderen"
|
||||
@click="$emit('delete', field)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Regular field variant -->
|
||||
<VCard
|
||||
v-else
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
>
|
||||
@@ -51,14 +99,6 @@ function formatOptions(options: NormalizedOption[] | null): string {
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<!-- Section -->
|
||||
<div
|
||||
v-if="field.section"
|
||||
class="text-body-2 text-medium-emphasis mb-1"
|
||||
>
|
||||
Sectie: {{ field.section }}
|
||||
</div>
|
||||
|
||||
<!-- Options preview -->
|
||||
<div
|
||||
v-if="field.normalized_options?.length"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { usePersonTagCategories } from '@/composables/api/usePersonTags'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { RegistrationFormField, RegistrationFormFieldCreateDTO } from '@/types/registration-form-field'
|
||||
import type { RegistrationFieldType, FieldDisplayWidth } from '@/types/registration-field-template'
|
||||
import { FIELD_TYPE_LABELS, FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
|
||||
import { FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/auth'
|
||||
|
||||
@@ -24,7 +24,21 @@ const modelValue = defineModel<boolean>({ default: false })
|
||||
const orgIdRef = computed(() => props.orgId)
|
||||
const { data: tagCategories } = usePersonTagCategories(orgIdRef)
|
||||
|
||||
const fieldTypeOptions = Object.entries(FIELD_TYPE_LABELS).map(([value, title]) => ({ title, value }))
|
||||
const fieldTypeItems = [
|
||||
{ type: 'subheader', title: 'Structuur' },
|
||||
{ title: 'Kop / Sectie-titel', value: 'heading', props: { prependIcon: 'tabler-heading' } },
|
||||
{ type: 'divider' },
|
||||
{ type: 'subheader', title: 'Invoervelden' },
|
||||
{ title: 'Tekstveld', value: 'text', props: { prependIcon: 'tabler-letter-t' } },
|
||||
{ title: 'Tekstvak (groot)', value: 'textarea', props: { prependIcon: 'tabler-text-size' } },
|
||||
{ title: 'Getal', value: 'number', props: { prependIcon: 'tabler-hash' } },
|
||||
{ title: 'Toggle (ja/nee)', value: 'boolean', props: { prependIcon: 'tabler-toggle-left' } },
|
||||
{ title: 'Dropdown', value: 'select', props: { prependIcon: 'tabler-list' } },
|
||||
{ title: 'Meervoudige keuze', value: 'multiselect', props: { prependIcon: 'tabler-checklist' } },
|
||||
{ title: 'Keuzerondjes', value: 'radio', props: { prependIcon: 'tabler-circle-dot' } },
|
||||
{ title: 'Checkbox', value: 'checkbox', props: { prependIcon: 'tabler-checkbox' } },
|
||||
{ title: 'Tags & Vaardigheden', value: 'tag_picker', props: { prependIcon: 'tabler-tags' } },
|
||||
]
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const refVForm = ref<VForm>()
|
||||
@@ -43,7 +57,6 @@ const defaultForm = () => ({
|
||||
is_filterable: false,
|
||||
is_portal_visible: true,
|
||||
is_admin_only: false,
|
||||
section: '',
|
||||
help_text: '',
|
||||
display_width: 'full' as FieldDisplayWidth,
|
||||
})
|
||||
@@ -54,6 +67,8 @@ const dialogTitle = computed(() =>
|
||||
props.field ? 'Veld bewerken' : 'Nieuw veld',
|
||||
)
|
||||
|
||||
const isHeading = computed(() => form.value.field_type === 'heading')
|
||||
|
||||
const showOptions = computed(() =>
|
||||
(FIELD_TYPES_WITH_OPTIONS as readonly string[]).includes(form.value.field_type),
|
||||
)
|
||||
@@ -75,7 +90,6 @@ watch(modelValue, (open) => {
|
||||
is_filterable: props.field.is_filterable,
|
||||
is_portal_visible: props.field.is_portal_visible,
|
||||
is_admin_only: props.field.is_admin_only,
|
||||
section: props.field.section ?? '',
|
||||
help_text: props.field.help_text ?? '',
|
||||
display_width: props.field.display_width ?? 'full',
|
||||
}
|
||||
@@ -101,11 +115,6 @@ function onSubmit() {
|
||||
errors.value = {}
|
||||
const payload: Partial<RegistrationFormFieldCreateDTO> = {
|
||||
label: form.value.label,
|
||||
is_required: form.value.is_required,
|
||||
is_filterable: form.value.is_filterable,
|
||||
is_portal_visible: form.value.is_portal_visible,
|
||||
is_admin_only: form.value.is_admin_only,
|
||||
section: form.value.section || null,
|
||||
help_text: form.value.help_text || null,
|
||||
display_width: form.value.display_width,
|
||||
}
|
||||
@@ -114,6 +123,12 @@ function onSubmit() {
|
||||
payload.field_type = form.value.field_type as RegistrationFieldType
|
||||
}
|
||||
|
||||
if (!isHeading.value) {
|
||||
payload.is_required = form.value.is_required
|
||||
payload.is_filterable = form.value.is_filterable
|
||||
payload.is_portal_visible = form.value.is_portal_visible
|
||||
payload.is_admin_only = form.value.is_admin_only
|
||||
|
||||
if (showOptions.value) {
|
||||
payload.options = form.value.options
|
||||
.filter(o => o.label.trim() !== '')
|
||||
@@ -132,6 +147,7 @@ function onSubmit() {
|
||||
else {
|
||||
payload.tag_category = null
|
||||
}
|
||||
}
|
||||
|
||||
emit('save', payload)
|
||||
})
|
||||
@@ -164,6 +180,56 @@ defineExpose({ setErrors })
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.field_type"
|
||||
label="Type"
|
||||
:items="fieldTypeItems"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.field_type"
|
||||
:disabled="!!field"
|
||||
:hint="field ? 'Type kan niet gewijzigd worden na aanmaken' : ''"
|
||||
:persistent-hint="!!field"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- HEADING fields: simplified UI -->
|
||||
<template v-if="isHeading">
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Kop tekst"
|
||||
placeholder="bijv. Persoonlijke gegevens"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.help_text"
|
||||
label="Omschrijving (optioneel)"
|
||||
placeholder="Korte toelichting onder de kop"
|
||||
:error-messages="errors.help_text"
|
||||
hint="Verschijnt in kleinere tekst onder de kop"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="tabler-info-circle"
|
||||
>
|
||||
Een kop groepeert de velden hieronder visueel. Het verzamelt geen antwoorden.
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</template>
|
||||
|
||||
<!-- Regular input fields: full UI -->
|
||||
<template v-else>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
@@ -175,18 +241,6 @@ defineExpose({ setErrors })
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.field_type"
|
||||
label="Type"
|
||||
:items="fieldTypeOptions"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.field_type"
|
||||
:disabled="!!field"
|
||||
:hint="field ? 'Type kan niet gewijzigd worden na aanmaken' : ''"
|
||||
:persistent-hint="!!field"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- Options (conditional) -->
|
||||
<VCol
|
||||
@@ -266,52 +320,17 @@ defineExpose({ setErrors })
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.section"
|
||||
label="Sectie"
|
||||
placeholder="bijv. Over jou"
|
||||
:error-messages="errors.section"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<label class="text-body-2 text-medium-emphasis d-block mb-1">
|
||||
Veldbreedte
|
||||
</label>
|
||||
<VBtnToggle
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.display_width"
|
||||
mandatory
|
||||
density="compact"
|
||||
>
|
||||
<VBtn
|
||||
value="full"
|
||||
size="small"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-distribute-horizontal"
|
||||
size="16"
|
||||
class="me-1"
|
||||
:items="[
|
||||
{ title: 'Volledige breedte', value: 'full' },
|
||||
{ title: 'Halve breedte', value: 'half' },
|
||||
]"
|
||||
label="Veldbreedte"
|
||||
:hint="form.display_width === 'half' ? 'Wordt naast een ander half-breed veld geplaatst' : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
Volledig
|
||||
</VBtn>
|
||||
<VBtn
|
||||
value="half"
|
||||
size="small"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-columns"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Half
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
@@ -352,6 +371,7 @@ defineExpose({ setErrors })
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -88,9 +88,6 @@ function onAdd(templateId: string) {
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
{{ FIELD_TYPE_LABELS[template.field_type] ?? template.field_type }}
|
||||
<template v-if="template.section">
|
||||
· {{ template.section }}
|
||||
</template>
|
||||
<template v-if="template.is_required">
|
||||
· Verplicht
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,22 @@ import { usePersonTagCategories } from '@/composables/api/usePersonTags'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { RegistrationFieldTemplate, RegistrationFieldTemplateCreateDTO, RegistrationFieldType, FieldDisplayWidth } from '@/types/registration-field-template'
|
||||
import { FIELD_TYPE_LABELS, FIELD_TYPES_WITH_OPTIONS } from '@/types/registration-field-template'
|
||||
|
||||
const fieldTypeItems = [
|
||||
{ type: 'subheader', title: 'Structuur' },
|
||||
{ title: 'Kop / Sectie-titel', value: 'heading', props: { prependIcon: 'tabler-heading' } },
|
||||
{ type: 'divider' },
|
||||
{ type: 'subheader', title: 'Invoervelden' },
|
||||
{ title: 'Tekstveld', value: 'text', props: { prependIcon: 'tabler-letter-t' } },
|
||||
{ title: 'Tekstvak (groot)', value: 'textarea', props: { prependIcon: 'tabler-text-size' } },
|
||||
{ title: 'Getal', value: 'number', props: { prependIcon: 'tabler-hash' } },
|
||||
{ title: 'Toggle (ja/nee)', value: 'boolean', props: { prependIcon: 'tabler-toggle-left' } },
|
||||
{ title: 'Dropdown', value: 'select', props: { prependIcon: 'tabler-list' } },
|
||||
{ title: 'Meervoudige keuze', value: 'multiselect', props: { prependIcon: 'tabler-checklist' } },
|
||||
{ title: 'Keuzerondjes', value: 'radio', props: { prependIcon: 'tabler-circle-dot' } },
|
||||
{ title: 'Checkbox', value: 'checkbox', props: { prependIcon: 'tabler-checkbox' } },
|
||||
{ title: 'Tags & Vaardigheden', value: 'tag_picker', props: { prependIcon: 'tabler-tags' } },
|
||||
]
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/auth'
|
||||
|
||||
@@ -25,15 +41,12 @@ const { mutate: createTemplate, isPending: isCreating } = useCreateRegistrationF
|
||||
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateRegistrationFieldTemplate(orgIdRef)
|
||||
const { mutate: deleteTemplate, isPending: isDeleting } = useDeleteRegistrationFieldTemplate(orgIdRef)
|
||||
|
||||
const fieldTypeOptions = Object.entries(FIELD_TYPE_LABELS).map(([value, title]) => ({ title, value }))
|
||||
|
||||
const activeTemplates = computed(() => templates.value?.filter(t => t.is_active) ?? [])
|
||||
const inactiveTemplates = computed(() => templates.value?.filter(t => !t.is_active) ?? [])
|
||||
|
||||
const headers = [
|
||||
{ title: 'Label', key: 'label' },
|
||||
{ title: 'Type', key: 'field_type' },
|
||||
{ title: 'Sectie', key: 'section' },
|
||||
{ title: 'Systeem', key: 'is_system', sortable: false },
|
||||
{ title: 'Acties', key: 'actions', sortable: false, align: 'end' as const },
|
||||
]
|
||||
@@ -60,7 +73,6 @@ const defaultForm = () => ({
|
||||
is_filterable: false,
|
||||
is_portal_visible: true,
|
||||
is_admin_only: false,
|
||||
section: '',
|
||||
help_text: '',
|
||||
sort_order: 0,
|
||||
display_width: 'full' as FieldDisplayWidth,
|
||||
@@ -74,6 +86,8 @@ const dialogTitle = computed(() =>
|
||||
|
||||
const isSaving = computed(() => isCreating.value || isUpdating.value)
|
||||
|
||||
const isHeading = computed(() => form.value.field_type === 'heading')
|
||||
|
||||
const showOptions = computed(() =>
|
||||
(FIELD_TYPES_WITH_OPTIONS as readonly string[]).includes(form.value.field_type),
|
||||
)
|
||||
@@ -104,7 +118,6 @@ function openEditDialog(template: RegistrationFieldTemplate) {
|
||||
is_filterable: template.is_filterable,
|
||||
is_portal_visible: template.is_portal_visible,
|
||||
is_admin_only: template.is_admin_only,
|
||||
section: template.section ?? '',
|
||||
help_text: template.help_text ?? '',
|
||||
sort_order: template.sort_order,
|
||||
display_width: template.display_width ?? 'full',
|
||||
@@ -128,11 +141,6 @@ function onSubmit() {
|
||||
errors.value = {}
|
||||
const payload: Partial<RegistrationFieldTemplateCreateDTO> = {
|
||||
label: form.value.label,
|
||||
is_required: form.value.is_required,
|
||||
is_filterable: form.value.is_filterable,
|
||||
is_portal_visible: form.value.is_portal_visible,
|
||||
is_admin_only: form.value.is_admin_only,
|
||||
section: form.value.section || null,
|
||||
help_text: form.value.help_text || null,
|
||||
sort_order: form.value.sort_order,
|
||||
display_width: form.value.display_width,
|
||||
@@ -142,6 +150,12 @@ function onSubmit() {
|
||||
payload.field_type = form.value.field_type as RegistrationFieldType
|
||||
}
|
||||
|
||||
if (!isHeading.value) {
|
||||
payload.is_required = form.value.is_required
|
||||
payload.is_filterable = form.value.is_filterable
|
||||
payload.is_portal_visible = form.value.is_portal_visible
|
||||
payload.is_admin_only = form.value.is_admin_only
|
||||
|
||||
if (showOptions.value) {
|
||||
payload.options = form.value.options
|
||||
.filter(o => o.label.trim() !== '')
|
||||
@@ -157,6 +171,7 @@ function onSubmit() {
|
||||
if (showTagCategory.value) {
|
||||
payload.tag_category = form.value.tag_category || null
|
||||
}
|
||||
}
|
||||
|
||||
if (editingTemplate.value) {
|
||||
updateTemplate(
|
||||
@@ -301,14 +316,6 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
{{ FIELD_TYPE_LABELS[item.field_type] ?? item.field_type }}
|
||||
</template>
|
||||
|
||||
<template #item.section="{ item }">
|
||||
<span v-if="item.section">{{ item.section }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-disabled"
|
||||
>-</span>
|
||||
</template>
|
||||
|
||||
<template #item.is_system="{ item }">
|
||||
<VChip
|
||||
v-if="item.is_system"
|
||||
@@ -398,6 +405,56 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.field_type"
|
||||
label="Type"
|
||||
:items="fieldTypeItems"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.field_type"
|
||||
:disabled="!!editingTemplate"
|
||||
:hint="editingTemplate ? 'Type kan niet gewijzigd worden na aanmaken' : ''"
|
||||
:persistent-hint="!!editingTemplate"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- HEADING fields: simplified UI -->
|
||||
<template v-if="isHeading">
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
label="Kop tekst"
|
||||
placeholder="bijv. Persoonlijke gegevens"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.label"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.help_text"
|
||||
label="Omschrijving (optioneel)"
|
||||
placeholder="Korte toelichting onder de kop"
|
||||
:error-messages="errors.help_text"
|
||||
hint="Verschijnt in kleinere tekst onder de kop"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="tabler-info-circle"
|
||||
>
|
||||
Een kop groepeert de velden hieronder visueel. Het verzamelt geen antwoorden.
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</template>
|
||||
|
||||
<!-- Regular input fields: full UI -->
|
||||
<template v-else>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="form.label"
|
||||
@@ -409,18 +466,6 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="form.field_type"
|
||||
label="Type"
|
||||
:items="fieldTypeOptions"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.field_type"
|
||||
:disabled="!!editingTemplate"
|
||||
:hint="editingTemplate ? 'Type kan niet gewijzigd worden na aanmaken' : ''"
|
||||
:persistent-hint="!!editingTemplate"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- Options (conditional) -->
|
||||
<VCol
|
||||
@@ -504,11 +549,15 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="form.section"
|
||||
label="Sectie"
|
||||
placeholder="bijv. Vergoeding"
|
||||
:error-messages="errors.section"
|
||||
<AppSelect
|
||||
v-model="form.display_width"
|
||||
:items="[
|
||||
{ title: 'Volledige breedte', value: 'full' },
|
||||
{ title: 'Halve breedte', value: 'half' },
|
||||
]"
|
||||
label="Veldbreedte"
|
||||
:hint="form.display_width === 'half' ? 'Wordt naast een ander half-breed veld geplaatst' : ''"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -522,42 +571,6 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
:error-messages="errors.sort_order"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<label class="text-body-2 text-medium-emphasis d-block mb-1">
|
||||
Veldbreedte
|
||||
</label>
|
||||
<VBtnToggle
|
||||
v-model="form.display_width"
|
||||
mandatory
|
||||
density="compact"
|
||||
>
|
||||
<VBtn
|
||||
value="full"
|
||||
size="small"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-distribute-horizontal"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Volledig
|
||||
</VBtn>
|
||||
<VBtn
|
||||
value="half"
|
||||
size="small"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-columns"
|
||||
size="16"
|
||||
class="me-1"
|
||||
/>
|
||||
Half
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="form.help_text"
|
||||
@@ -597,6 +610,7 @@ function activate(template: RegistrationFieldTemplate) {
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const RegistrationFieldType = {
|
||||
BOOLEAN: 'boolean',
|
||||
NUMBER: 'number',
|
||||
TAG_PICKER: 'tag_picker',
|
||||
HEADING: 'heading',
|
||||
} as const
|
||||
|
||||
export type RegistrationFieldType = typeof RegistrationFieldType[keyof typeof RegistrationFieldType]
|
||||
@@ -22,6 +23,7 @@ export const FIELD_TYPE_LABELS: Record<RegistrationFieldType, string> = {
|
||||
boolean: 'Ja/Nee',
|
||||
number: 'Getal',
|
||||
tag_picker: 'Tag-kiezer',
|
||||
heading: 'Kop / Sectie-titel',
|
||||
}
|
||||
|
||||
export const FIELD_TYPES_WITH_OPTIONS: RegistrationFieldType[] = [
|
||||
@@ -52,7 +54,6 @@ export interface RegistrationFieldTemplate {
|
||||
is_filterable: boolean
|
||||
is_portal_visible: boolean
|
||||
is_admin_only: boolean
|
||||
section: string | null
|
||||
help_text: string | null
|
||||
sort_order: number
|
||||
display_width: FieldDisplayWidth
|
||||
@@ -69,7 +70,6 @@ export interface RegistrationFieldTemplateCreateDTO {
|
||||
is_filterable?: boolean
|
||||
is_portal_visible?: boolean
|
||||
is_admin_only?: boolean
|
||||
section?: string | null
|
||||
help_text?: string | null
|
||||
sort_order?: number
|
||||
display_width?: FieldDisplayWidth
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface RegistrationFormField {
|
||||
is_portal_visible: boolean
|
||||
is_admin_only: boolean
|
||||
is_filterable: boolean
|
||||
section: string | null
|
||||
help_text: string | null
|
||||
sort_order: number
|
||||
display_width: FieldDisplayWidth
|
||||
@@ -40,7 +39,6 @@ export interface RegistrationFormFieldCreateDTO {
|
||||
is_portal_visible?: boolean
|
||||
is_admin_only?: boolean
|
||||
is_filterable?: boolean
|
||||
section?: string | null
|
||||
help_text?: string | null
|
||||
sort_order?: number
|
||||
display_width?: FieldDisplayWidth
|
||||
|
||||
@@ -173,20 +173,6 @@ const currentStepKind = computed(() => {
|
||||
|
||||
const registrationFieldsList = computed(() => registrationData.value?.registration_fields ?? [])
|
||||
|
||||
const groupedRegistrationFields = computed(() => {
|
||||
const fields = registrationFieldsList.value
|
||||
const groups: { section: string | null; fields: RegistrationField[] }[] = []
|
||||
for (const f of fields) {
|
||||
const last = groups[groups.length - 1]
|
||||
if (last && last.section === f.section)
|
||||
last.fields.push(f)
|
||||
else
|
||||
groups.push({ section: f.section, fields: [f] })
|
||||
}
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
function defaultFieldValue(type: RegistrationFieldType): unknown {
|
||||
switch (type) {
|
||||
case 'multiselect':
|
||||
@@ -196,6 +182,7 @@ function defaultFieldValue(type: RegistrationFieldType): unknown {
|
||||
case 'boolean':
|
||||
return false
|
||||
case 'number':
|
||||
case 'heading':
|
||||
return null
|
||||
default:
|
||||
return ''
|
||||
@@ -204,6 +191,7 @@ function defaultFieldValue(type: RegistrationFieldType): unknown {
|
||||
|
||||
watch(registrationFieldsList, fields => {
|
||||
for (const f of fields) {
|
||||
if (f.field_type === 'heading') continue
|
||||
if (!(f.slug in fieldFormData.value))
|
||||
fieldFormData.value[f.slug] = defaultFieldValue(f.field_type)
|
||||
}
|
||||
@@ -337,7 +325,7 @@ function validateDynamicFields(): boolean {
|
||||
fieldErrors.value = {}
|
||||
let firstErrorSlug: string | null = null
|
||||
for (const field of registrationFieldsList.value) {
|
||||
if (!field.is_required) continue
|
||||
if (field.field_type === 'heading' || !field.is_required) continue
|
||||
const val = fieldFormData.value[field.slug]
|
||||
if (isEmptyFieldValue(val, field.field_type)) {
|
||||
fieldErrors.value[field.slug] = 'Dit veld is verplicht.'
|
||||
@@ -434,6 +422,7 @@ function formatTimeRange(start: string, end: string): string {
|
||||
function buildFieldValuesPayload(): Record<string, unknown> | undefined {
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const field of registrationFieldsList.value) {
|
||||
if (field.field_type === 'heading') continue
|
||||
const val = fieldFormData.value[field.slug]
|
||||
if (field.field_type === 'boolean') {
|
||||
if (typeof val === 'boolean')
|
||||
@@ -1047,21 +1036,32 @@ async function onSubmit() {
|
||||
|
||||
<!-- Step 1: Extra informatie (dynamic registration_fields) -->
|
||||
<div v-show="currentStep === 1">
|
||||
<template
|
||||
v-for="(group, gi) in groupedRegistrationFields"
|
||||
:key="gi"
|
||||
>
|
||||
<div
|
||||
v-if="group.section"
|
||||
class="text-subtitle-2 text-medium-emphasis mt-4 mb-2"
|
||||
>
|
||||
{{ group.section }}
|
||||
</div>
|
||||
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="field in group.fields"
|
||||
<template
|
||||
v-for="(field, fieldIndex) in registrationFieldsList"
|
||||
:key="field.id"
|
||||
>
|
||||
<!-- HEADING field: full-width section header -->
|
||||
<VCol
|
||||
v-if="field.field_type === 'heading'"
|
||||
cols="12"
|
||||
:class="fieldIndex === 0 ? '' : 'mt-4'"
|
||||
>
|
||||
<div class="text-subtitle-1 font-weight-medium mb-1">
|
||||
{{ field.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="field.help_text"
|
||||
class="text-body-2 text-medium-emphasis mb-3"
|
||||
>
|
||||
{{ field.help_text }}
|
||||
</div>
|
||||
<VDivider class="mb-2" />
|
||||
</VCol>
|
||||
|
||||
<!-- Regular input field -->
|
||||
<VCol
|
||||
v-else
|
||||
cols="12"
|
||||
:md="field.display_width === 'half' ? 6 : 12"
|
||||
>
|
||||
@@ -1270,8 +1270,8 @@ async function onSubmit() {
|
||||
/>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
</VRow>
|
||||
|
||||
<p
|
||||
v-if="registrationFieldsList.length === 0"
|
||||
|
||||
@@ -8,6 +8,7 @@ export type RegistrationFieldType =
|
||||
| 'boolean'
|
||||
| 'number'
|
||||
| 'tag_picker'
|
||||
| 'heading'
|
||||
|
||||
export type FieldDisplayWidth = 'full' | 'half'
|
||||
|
||||
@@ -31,7 +32,6 @@ export interface RegistrationField {
|
||||
normalized_options: NormalizedOption[] | null
|
||||
tag_category: string | null
|
||||
is_required: boolean
|
||||
section: string | null
|
||||
help_text: string | null
|
||||
display_width: FieldDisplayWidth
|
||||
available_tags?: RegistrationTagOption[]
|
||||
|
||||
Reference in New Issue
Block a user