feat: registration form field display_width and option descriptions
Add configurable column widths (full/half) and optional descriptions for radio/select/checkbox options on registration form fields. - Migration adds display_width column to both tables - FieldDisplayWidth enum with smart defaults per field type - normalized_options accessor for backwards-compatible option format - Portal form renderer uses display_width for VRow/VCol grid layout - Radio/select/checkbox options render with descriptions - Admin field editor supports display_width toggle and description input - System templates updated with appropriate widths and descriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
api/app/Enums/FieldDisplayWidth.php
Normal file
38
api/app/Enums/FieldDisplayWidth.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum FieldDisplayWidth: string
|
||||
{
|
||||
case FULL = 'full';
|
||||
case HALF = 'half';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::FULL => 'Volledige breedte',
|
||||
self::HALF => 'Halve breedte',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default display width for a given field type.
|
||||
*/
|
||||
public static function defaultForFieldType(RegistrationFieldType $fieldType): self
|
||||
{
|
||||
return match ($fieldType) {
|
||||
RegistrationFieldType::TEXT,
|
||||
RegistrationFieldType::NUMBER,
|
||||
RegistrationFieldType::SELECT,
|
||||
RegistrationFieldType::BOOLEAN => self::HALF,
|
||||
|
||||
RegistrationFieldType::TEXTAREA,
|
||||
RegistrationFieldType::RADIO,
|
||||
RegistrationFieldType::CHECKBOX,
|
||||
RegistrationFieldType::MULTISELECT,
|
||||
RegistrationFieldType::TAG_PICKER => self::FULL,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -97,10 +97,12 @@ final class PublicRegistrationDataController extends Controller
|
||||
'slug' => $field->slug,
|
||||
'field_type' => $field->field_type->value,
|
||||
'options' => $field->options,
|
||||
'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,
|
||||
];
|
||||
|
||||
if ($field->field_type === \App\Enums\RegistrationFieldType::TAG_PICKER) {
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -29,7 +30,9 @@ final class StoreRegistrationFieldTemplateRequest extends FormRequest
|
||||
$type?->prohibitsOptions() ? 'prohibited' : 'nullable',
|
||||
'array',
|
||||
],
|
||||
'options.*' => ['string', 'max:255'],
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => [
|
||||
$type === RegistrationFieldType::TAG_PICKER ? 'nullable' : 'prohibited',
|
||||
'string',
|
||||
@@ -42,6 +45,7 @@ final class StoreRegistrationFieldTemplateRequest extends FormRequest
|
||||
'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'))],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -29,7 +30,9 @@ final class StoreRegistrationFormFieldRequest extends FormRequest
|
||||
$type?->prohibitsOptions() ? 'prohibited' : 'nullable',
|
||||
'array',
|
||||
],
|
||||
'options.*' => ['string', 'max:255'],
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => [
|
||||
$type === RegistrationFieldType::TAG_PICKER ? 'nullable' : 'prohibited',
|
||||
'string',
|
||||
@@ -42,6 +45,7 @@ final class StoreRegistrationFormFieldRequest extends FormRequest
|
||||
'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'))],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class UpdateRegistrationFieldTemplateRequest extends FormRequest
|
||||
{
|
||||
@@ -20,7 +22,9 @@ final class UpdateRegistrationFieldTemplateRequest extends FormRequest
|
||||
return [
|
||||
'label' => ['sometimes', 'string', 'max:255'],
|
||||
'options' => ['nullable', 'array'],
|
||||
'options.*' => ['string', 'max:255'],
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => ['nullable', 'string', 'max:50'],
|
||||
'is_required' => ['nullable', 'boolean'],
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
@@ -29,6 +33,7 @@ final class UpdateRegistrationFieldTemplateRequest extends FormRequest
|
||||
'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'))],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
final class UpdateRegistrationFormFieldRequest extends FormRequest
|
||||
{
|
||||
@@ -19,7 +21,9 @@ final class UpdateRegistrationFormFieldRequest extends FormRequest
|
||||
return [
|
||||
'label' => ['sometimes', 'string', 'max:255'],
|
||||
'options' => ['nullable', 'array'],
|
||||
'options.*' => ['string', 'max:255'],
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => ['nullable', 'string', 'max:50'],
|
||||
'is_required' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
@@ -28,6 +32,7 @@ final class UpdateRegistrationFormFieldRequest extends FormRequest
|
||||
'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'))],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ final class RegistrationFieldTemplateResource extends JsonResource
|
||||
'slug' => $this->slug,
|
||||
'field_type' => $this->field_type->value,
|
||||
'options' => $this->options,
|
||||
'normalized_options' => $this->normalized_options,
|
||||
'tag_category' => $this->tag_category,
|
||||
'is_required' => $this->is_required,
|
||||
'is_filterable' => $this->is_filterable,
|
||||
@@ -26,6 +27,7 @@ final class RegistrationFieldTemplateResource extends JsonResource
|
||||
'section' => $this->section,
|
||||
'help_text' => $this->help_text,
|
||||
'sort_order' => $this->sort_order,
|
||||
'display_width' => $this->display_width->value,
|
||||
'is_system' => $this->is_system,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
|
||||
@@ -20,6 +20,7 @@ final class RegistrationFormFieldResource extends JsonResource
|
||||
'slug' => $this->slug,
|
||||
'field_type' => $this->field_type->value,
|
||||
'options' => $this->options,
|
||||
'normalized_options' => $this->normalized_options,
|
||||
'tag_category' => $this->tag_category,
|
||||
'is_required' => $this->is_required,
|
||||
'is_portal_visible' => $this->is_portal_visible,
|
||||
@@ -28,6 +29,7 @@ final class RegistrationFormFieldResource extends JsonResource
|
||||
'section' => $this->section,
|
||||
'help_text' => $this->help_text,
|
||||
'sort_order' => $this->sort_order,
|
||||
'display_width' => $this->display_width->value,
|
||||
'created_at' => $this->created_at->toIso8601String(),
|
||||
'updated_at' => $this->updated_at->toIso8601String(),
|
||||
'available_tags' => $this->when(
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Scopes\OrganisationScope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -36,6 +37,7 @@ final class RegistrationFieldTemplate extends Model
|
||||
'section',
|
||||
'help_text',
|
||||
'sort_order',
|
||||
'display_width',
|
||||
'is_system',
|
||||
'is_active',
|
||||
];
|
||||
@@ -50,11 +52,31 @@ final class RegistrationFieldTemplate extends Model
|
||||
'is_portal_visible' => 'boolean',
|
||||
'is_admin_only' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'display_width' => FieldDisplayWidth::class,
|
||||
'is_system' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int, array{label: string, description: string|null}>|null */
|
||||
public function getNormalizedOptionsAttribute(): ?array
|
||||
{
|
||||
if ($this->options === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collect($this->options)->map(function (mixed $option): array {
|
||||
if (is_string($option)) {
|
||||
return ['label' => $option, 'description' => null];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => $option['label'] ?? (string) $option,
|
||||
'description' => $option['description'] ?? null,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organisation::class);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Scopes\OrganisationScope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -40,6 +41,7 @@ final class RegistrationFormField extends Model
|
||||
'section',
|
||||
'help_text',
|
||||
'sort_order',
|
||||
'display_width',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -52,6 +54,7 @@ final class RegistrationFormField extends Model
|
||||
'is_admin_only' => 'boolean',
|
||||
'is_filterable' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'display_width' => FieldDisplayWidth::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -65,6 +68,25 @@ final class RegistrationFormField extends Model
|
||||
return $this->hasMany(PersonFieldValue::class, 'registration_form_field_id');
|
||||
}
|
||||
|
||||
/** @return array<int, array{label: string, description: string|null}>|null */
|
||||
public function getNormalizedOptionsAttribute(): ?array
|
||||
{
|
||||
if ($this->options === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collect($this->options)->map(function (mixed $option): array {
|
||||
if (is_string($option)) {
|
||||
return ['label' => $option, 'description' => null];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => $option['label'] ?? (string) $option,
|
||||
'description' => $option['description'] ?? null,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function isMultiValue(): bool
|
||||
{
|
||||
return $this->field_type->isMultiValue();
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Event;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\RegistrationFieldTemplate;
|
||||
@@ -24,6 +26,11 @@ final class RegistrationFieldTemplateService
|
||||
{
|
||||
$data['slug'] = $this->generateUniqueSlug($organisation, $data['label']);
|
||||
|
||||
if (!isset($data['display_width'])) {
|
||||
$fieldType = RegistrationFieldType::from($data['field_type']);
|
||||
$data['display_width'] = FieldDisplayWidth::defaultForFieldType($fieldType)->value;
|
||||
}
|
||||
|
||||
$template = $organisation->registrationFieldTemplates()->create($data);
|
||||
|
||||
$activityLogger = activity('registration_templates')
|
||||
@@ -111,6 +118,7 @@ final class RegistrationFieldTemplateService
|
||||
'section' => $template->section,
|
||||
'help_text' => $template->help_text,
|
||||
'sort_order' => $maxOrder + 1,
|
||||
'display_width' => $template->display_width,
|
||||
]);
|
||||
|
||||
$activityLogger = activity('registration_fields')
|
||||
@@ -132,17 +140,21 @@ 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, 'sort_order' => 1],
|
||||
['label' => 'Dieetwensen', 'field_type' => 'multiselect', 'options' => ['Vegetarisch', 'Veganistisch', 'Halal', 'Glutenvrij', 'Lactosevrij', 'Geen pinda\'s', 'Geen noten'], 'is_filterable' => true, 'sort_order' => 2],
|
||||
['label' => 'Vergoeding', 'field_type' => 'radio', 'options' => ['Pro Deo', 'Entreeticket', 'Vrijwilligersvergoeding'], 'section' => 'Vergoeding', '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).', 'sort_order' => 4],
|
||||
['label' => 'Noodcontact naam', 'field_type' => 'text', 'section' => 'Noodcontact', 'sort_order' => 5],
|
||||
['label' => 'Noodcontact telefoon', 'field_type' => 'text', 'section' => 'Noodcontact', 'sort_order' => 6],
|
||||
['label' => 'EHBO / BHV diploma', 'field_type' => 'boolean', 'is_filterable' => true, 'sort_order' => 7],
|
||||
['label' => 'Rijbewijs', 'field_type' => 'boolean', 'is_filterable' => true, 'sort_order' => 8],
|
||||
['label' => 'Eerder vrijwilliger geweest', 'field_type' => 'boolean', 'is_filterable' => true, 'sort_order' => 9],
|
||||
['label' => 'Certificaten & vaardigheden', 'field_type' => 'tag_picker', 'tag_category' => null, 'is_filterable' => true, 'sort_order' => 10],
|
||||
['label' => 'Opmerkingen', 'field_type' => 'textarea', 'sort_order' => 11],
|
||||
['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' => '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],
|
||||
];
|
||||
|
||||
foreach ($templates as $data) {
|
||||
@@ -155,6 +167,7 @@ final class RegistrationFieldTemplateService
|
||||
'is_filterable' => $data['is_filterable'] ?? false,
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
'display_width' => $data['display_width'] ?? 'full',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Event;
|
||||
use App\Models\Person;
|
||||
@@ -30,6 +31,11 @@ final class RegistrationFormFieldService
|
||||
{
|
||||
$data['slug'] = $this->generateUniqueSlug($event, $data['label']);
|
||||
|
||||
if (!isset($data['display_width'])) {
|
||||
$fieldType = RegistrationFieldType::from($data['field_type']);
|
||||
$data['display_width'] = FieldDisplayWidth::defaultForFieldType($fieldType)->value;
|
||||
}
|
||||
|
||||
$field = RegistrationFormField::create([
|
||||
'event_id' => $event->id,
|
||||
...$data,
|
||||
@@ -173,6 +179,7 @@ final class RegistrationFormFieldService
|
||||
'section' => $sourceField->section,
|
||||
'help_text' => $sourceField->help_text,
|
||||
'sort_order' => ++$maxOrder,
|
||||
'display_width' => $sourceField->display_width,
|
||||
]);
|
||||
|
||||
$created->push($field);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\RegistrationFieldTemplate;
|
||||
@@ -34,6 +35,7 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'section' => null,
|
||||
'help_text' => null,
|
||||
'sort_order' => fake()->numberBetween(0, 20),
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
'is_system' => false,
|
||||
'is_active' => true,
|
||||
];
|
||||
@@ -57,6 +59,7 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'field_type' => RegistrationFieldType::SELECT,
|
||||
'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'],
|
||||
'is_filterable' => true,
|
||||
'display_width' => FieldDisplayWidth::HALF,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -67,6 +70,7 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'slug' => 'ehbo-bhv-diploma',
|
||||
'field_type' => RegistrationFieldType::BOOLEAN,
|
||||
'is_filterable' => true,
|
||||
'display_width' => FieldDisplayWidth::HALF,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -77,6 +81,7 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'slug' => 'certificaten-vaardigheden',
|
||||
'field_type' => RegistrationFieldType::TAG_PICKER,
|
||||
'is_filterable' => true,
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\FieldDisplayWidth;
|
||||
use App\Enums\RegistrationFieldType;
|
||||
use App\Models\Event;
|
||||
use App\Models\RegistrationFormField;
|
||||
@@ -34,6 +35,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'section' => null,
|
||||
'help_text' => null,
|
||||
'sort_order' => fake()->numberBetween(0, 20),
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -44,6 +46,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'slug' => 'noodcontact-naam',
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'section' => 'Noodcontact',
|
||||
'display_width' => FieldDisplayWidth::HALF,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -55,6 +58,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'field_type' => RegistrationFieldType::SELECT,
|
||||
'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'],
|
||||
'is_filterable' => true,
|
||||
'display_width' => FieldDisplayWidth::HALF,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -66,6 +70,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'field_type' => RegistrationFieldType::MULTISELECT,
|
||||
'options' => ['Vegetarisch', 'Veganistisch', 'Halal', 'Glutenvrij', 'Lactosevrij', 'Geen pinda\'s', 'Geen noten'],
|
||||
'is_filterable' => true,
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -78,6 +83,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'is_required' => true,
|
||||
'section' => 'Toestemming',
|
||||
'help_text' => 'Ik geef toestemming voor de verwerking van mijn persoonsgegevens conform de AVG.',
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -88,6 +94,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'slug' => 'vaardigheden',
|
||||
'field_type' => RegistrationFieldType::TAG_PICKER,
|
||||
'is_filterable' => true,
|
||||
'display_width' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,8 +104,13 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'label' => 'Vergoeding',
|
||||
'slug' => 'vergoeding',
|
||||
'field_type' => RegistrationFieldType::RADIO,
|
||||
'options' => ['Pro Deo', 'Entreeticket', 'Vrijwilligersvergoeding'],
|
||||
'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' => FieldDisplayWidth::FULL,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -108,6 +120,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'label' => 'Opmerkingen',
|
||||
'slug' => 'opmerkingen',
|
||||
'field_type' => RegistrationFieldType::TEXTAREA,
|
||||
'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_form_fields', function (Blueprint $table) {
|
||||
$table->string('display_width', 10)->default('full')->after('sort_order');
|
||||
});
|
||||
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->string('display_width', 10)->default('full')->after('sort_order');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->dropColumn('display_width');
|
||||
});
|
||||
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->dropColumn('display_width');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -360,4 +360,136 @@ class RegistrationFormFieldTest extends TestCase
|
||||
|
||||
$response->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_store_field_with_display_width(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Noodcontact naam',
|
||||
'field_type' => 'text',
|
||||
'display_width' => 'half',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.display_width', 'half');
|
||||
|
||||
$this->assertDatabaseHas('registration_form_fields', [
|
||||
'event_id' => $this->event->id,
|
||||
'slug' => 'noodcontact-naam',
|
||||
'display_width' => 'half',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_store_field_defaults_display_width_by_type(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
// Text fields default to 'half'
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Korte tekst',
|
||||
'field_type' => 'text',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.display_width', 'half');
|
||||
|
||||
// Textarea fields default to 'full'
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Opmerkingen',
|
||||
'field_type' => 'textarea',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.display_width', 'full');
|
||||
}
|
||||
|
||||
public function test_update_field_display_width(): void
|
||||
{
|
||||
$field = RegistrationFormField::factory()->create([
|
||||
'event_id' => $this->event->id,
|
||||
'display_width' => 'full',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields/{$field->id}", [
|
||||
'display_width' => 'half',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.display_width', 'half');
|
||||
}
|
||||
|
||||
public function test_store_field_with_option_descriptions(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Vergoeding',
|
||||
'field_type' => 'radio',
|
||||
'options' => [
|
||||
['label' => 'Pro Deo', 'description' => 'Geen vergoeding'],
|
||||
['label' => 'Entreeticket', 'description' => 'Gratis ticket'],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.normalized_options.0.label', 'Pro Deo')
|
||||
->assertJsonPath('data.normalized_options.0.description', 'Geen vergoeding')
|
||||
->assertJsonPath('data.normalized_options.1.label', 'Entreeticket')
|
||||
->assertJsonPath('data.normalized_options.1.description', 'Gratis ticket');
|
||||
}
|
||||
|
||||
public function test_options_backwards_compatible_with_string_array(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Shirtmaat',
|
||||
'field_type' => 'select',
|
||||
'options' => ['XS', 'S', 'M', 'L'],
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.normalized_options.0.label', 'XS')
|
||||
->assertJsonPath('data.normalized_options.0.description', null)
|
||||
->assertJsonPath('data.normalized_options.3.label', 'L');
|
||||
}
|
||||
|
||||
public function test_normalized_options_converts_strings_to_objects(): void
|
||||
{
|
||||
$field = RegistrationFormField::factory()->selectField()->create([
|
||||
'event_id' => $this->event->id,
|
||||
]);
|
||||
|
||||
$this->assertNotNull($field->normalized_options);
|
||||
$this->assertIsArray($field->normalized_options);
|
||||
|
||||
// Each option should be an array with label and description keys
|
||||
foreach ($field->normalized_options as $option) {
|
||||
$this->assertArrayHasKey('label', $option);
|
||||
$this->assertArrayHasKey('description', $option);
|
||||
$this->assertIsString($option['label']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_index_returns_display_width_and_normalized_options(): void
|
||||
{
|
||||
RegistrationFormField::factory()->radioField()->create([
|
||||
'event_id' => $this->event->id,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields");
|
||||
|
||||
$response->assertOk();
|
||||
$field = $response->json('data.0');
|
||||
$this->assertArrayHasKey('display_width', $field);
|
||||
$this->assertArrayHasKey('normalized_options', $field);
|
||||
$this->assertEquals('full', $field['display_width']);
|
||||
$this->assertNotNull($field['normalized_options']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user