diff --git a/apps/portal/src/components/registration/DynamicRegistrationFields.vue b/apps/portal/src/components/registration/DynamicRegistrationFields.vue new file mode 100644 index 00000000..f0209c8b --- /dev/null +++ b/apps/portal/src/components/registration/DynamicRegistrationFields.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/apps/portal/src/pages/register/[eventSlug].vue b/apps/portal/src/pages/register/[eventSlug].vue index 3b7a444c..93a8f2de 100644 --- a/apps/portal/src/pages/register/[eventSlug].vue +++ b/apps/portal/src/pages/register/[eventSlug].vue @@ -2,14 +2,15 @@ import { useForm } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' import { useDisplay } from 'vuetify' +import DynamicRegistrationFields from '@/components/registration/DynamicRegistrationFields.vue' import { useAuthStore } from '@/stores/useAuthStore' import { useRegistrationData, useSubmitRegistration } from '@/composables/api/useVolunteerRegistration' import { fullRegistrationSchema } from '@/schemas/registrationSchema' import type { + FieldValuePayload, + RegistrationFieldType, SectionOption, - SectionPreference, TimeSlotOption, - VolunteerAvailability, VolunteerRegistrationForm, } from '@/types/registration' @@ -21,6 +22,8 @@ definePage({ }, }) +type StepKind = 'personal' | 'dynamic' | 'sections' | 'availability' + const route = useRoute('volunteer-register') const router = useRouter() const authStore = useAuthStore() @@ -32,9 +35,38 @@ const { mutateAsync: submitRegistration, isPending: isSubmitting } = useSubmitRe const currentStep = ref(0) const submitError = ref(null) +const fieldValues = ref>({}) +const fieldErrors = ref>({}) -// VeeValidate form -const { errors, defineField, validateField, setFieldValue } = useForm({ +const stepMeta: Record = { + personal: { title: 'Over jou', subtitle: 'Vul je persoonlijke gegevens in' }, + dynamic: { title: 'Extra informatie', subtitle: 'Antwoord op de vragen van de organisatie' }, + sections: { title: 'Voorkeurssecties', subtitle: 'Waar wil je het liefst werken?' }, + availability: { title: 'Beschikbaarheid', subtitle: 'Wanneer kun je helpen?' }, +} + +const stepKinds = computed(() => { + if (!registrationData.value) return ['personal', 'dynamic'] + const kinds: StepKind[] = ['personal', 'dynamic'] + if (registrationData.value.event.registration_show_section_preferences) + kinds.push('sections') + if (registrationData.value.event.registration_show_availability) + kinds.push('availability') + + return kinds +}) + +const steps = computed(() => stepKinds.value.map(k => stepMeta[k])) + +watch(stepKinds, kinds => { + if (currentStep.value >= kinds.length) + currentStep.value = Math.max(0, kinds.length - 1) +}) + +const currentKind = computed(() => stepKinds.value[currentStep.value] ?? 'personal') + +// VeeValidate — fixed personal fields only +const { errors, defineField, validateField, setFieldValue, setFieldError } = useForm({ validationSchema: toTypedSchema(fullRegistrationSchema), initialValues: { first_name: '', @@ -42,12 +74,6 @@ const { errors, defineField, validateField, setFieldValue } = useForm({ email: '', date_of_birth: '', phone: '', - tshirt_size: '', - first_aid: false, - allergies: '', - driving_licence: false, - motivation: '', - motivation_other: '', }, }) @@ -56,14 +82,7 @@ const [lastName] = defineField('last_name') const [dateOfBirth] = defineField('date_of_birth') const [email] = defineField('email') const [phone] = defineField('phone') -const [tshirtSize] = defineField('tshirt_size') -const [firstAid] = defineField('first_aid') -const [allergies] = defineField('allergies') -const [drivingLicence] = defineField('driving_licence') -const [motivation] = defineField('motivation') -const [motivationOther] = defineField('motivation_other') -// Pre-fill authenticated user data watch(() => authStore.user, user => { if (user) { setFieldValue('first_name', user.first_name) @@ -72,38 +91,50 @@ watch(() => authStore.user, user => { } }, { immediate: true }) -// Step 3: Section preferences (by name, not ID) -const selectedSections = ref([]) +const registrationFieldsList = computed(() => registrationData.value?.registration_fields ?? []) -// Step 4: Availability +const groupedRegistrationFields = computed(() => { + const fields = registrationFieldsList.value + const groups: { section: string | null; fields: typeof fields }[] = [] + 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': + case 'checkbox': + case 'tag_picker': + return [] + case 'boolean': + return false + case 'number': + return null + default: + return '' + } +} + +watch(registrationFieldsList, fields => { + for (const f of fields) { + if (!(f.slug in fieldValues.value)) + fieldValues.value[f.slug] = defaultFieldValue(f.field_type) + } +}, { immediate: true }) + +// Section preferences (by festival_section_id, max 5, order = priority) +const selectedSectionIds = ref([]) + +// Availability (time slots) const selectedTimeSlotIds = ref([]) -const timeSlotPreferences = ref>({}) -// Steps definition -const steps = [ - { title: 'Over jou', subtitle: 'Vul je persoonlijke gegevens in' }, - { title: 'Meer over jou', subtitle: 'Praktische informatie' }, - { title: 'Motivatie', subtitle: 'Waarom wil je meehelpen?' }, - { title: 'Secties', subtitle: 'Waar wil je het liefst werken?' }, - { title: 'Beschikbaarheid', subtitle: 'Wanneer kun je helpen?' }, -] - -// Constants -const tshirtSizeItems = [ - { title: 'Geen voorkeur', value: '' }, - ...['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'].map(s => ({ title: s, value: s })), -] - -const motivationItems = [ - { title: 'Gratis festivalpas', value: 'Gratis festivalpas' }, - { title: 'Ervaring opdoen', value: 'Ervaring opdoen' }, - { title: 'Vrienden helpen', value: 'Vrienden helpen' }, - { title: 'CV opbouwen', value: 'CV opbouwen' }, - { title: 'Ik ben gevraagd', value: 'Ik ben gevraagd' }, - { title: 'Anders', value: 'Anders' }, -] - -// Section helpers const sectionsByCategory = computed(() => { if (!registrationData.value?.sections) return {} @@ -116,17 +147,16 @@ const sectionsByCategory = computed(() => { }, {} as Record) }) -function isSelected(sectionName: string) { - return selectedSections.value.includes(sectionName) +function isSelected(sectionId: string) { + return selectedSectionIds.value.includes(sectionId) } -function getSelectionPriority(sectionName: string) { - return selectedSections.value.indexOf(sectionName) + 1 +function getSelectionPriority(sectionId: string) { + return selectedSectionIds.value.indexOf(sectionId) + 1 } -const selectedCount = computed(() => selectedSections.value.length) +const selectedCount = computed(() => selectedSectionIds.value.length) -// Computed const timeSlotsByDate = computed(() => { if (!registrationData.value?.time_slots) return [] const groups = new Map() @@ -158,27 +188,97 @@ const formattedDates = computed(() => { ) }) -// Step field mapping for validation (0-based) -type FormField = 'first_name' | 'last_name' | 'date_of_birth' | 'email' | 'phone' | 'tshirt_size' | 'first_aid' | 'allergies' | 'driving_licence' | 'motivation' | 'motivation_other' +const personalFields = ['first_name', 'last_name', 'date_of_birth', 'email', 'phone'] as const -const stepFields: Record = { - 0: ['first_name', 'last_name', 'date_of_birth', 'email', 'phone'], - 1: ['tshirt_size', 'first_aid', 'allergies', 'driving_licence'], - 2: ['motivation', 'motivation_other'], +async function validateCurrentStep(): Promise { + const kind = currentKind.value + if (kind === 'personal') { + const results = await Promise.all(personalFields.map(f => validateField(f))) + + return results.every(r => r.valid) + } + if (kind === 'dynamic') + return validateDynamicFields() + + return true } -// Navigation -async function validateCurrentStep(): Promise { - const fields = stepFields[currentStep.value] - if (!fields) return true - const results = await Promise.all(fields.map(f => validateField(f))) +function isMultiValueType(type: RegistrationFieldType): boolean { + return type === 'multiselect' || type === 'checkbox' || type === 'tag_picker' +} - return results.every(r => r.valid) +function valueEmpty(value: unknown, type: RegistrationFieldType): boolean { + if (value === undefined || value === null) return true + if (value === '') return true + if (type === 'number' && (typeof value !== 'number' || Number.isNaN(value))) return true + if (isMultiValueType(type)) { + if (!Array.isArray(value)) return true + + return value.length === 0 + } + + return false +} + +function validateDynamicFields(): boolean { + fieldErrors.value = {} + let firstSlug: string | null = null + + for (const field of registrationFieldsList.value) { + if (!field.is_required) + continue + + const value = fieldValues.value[field.slug] + + if (valueEmpty(value, field.field_type)) { + fieldErrors.value[field.slug] = 'Dit veld is verplicht.' + firstSlug ??= field.slug + continue + } + + if (field.field_type === 'select' || field.field_type === 'radio') { + const opts = field.options ?? [] + if (typeof value === 'string' && !opts.includes(value)) { + fieldErrors.value[field.slug] = 'Maak een geldige keuze.' + firstSlug ??= field.slug + } + } + + if (field.field_type === 'multiselect' || field.field_type === 'checkbox') { + const opts = field.options ?? [] + const arr = Array.isArray(value) ? value : [] + if (arr.some(x => !opts.includes(String(x)))) { + fieldErrors.value[field.slug] = 'Ongeldige optie geselecteerd.' + firstSlug ??= field.slug + } + } + + if (field.field_type === 'tag_picker') { + const tags = field.available_tags ?? [] + const ids = new Set(tags.map(t => t.id)) + const arr = Array.isArray(value) ? value : [] + if (arr.some(id => !ids.has(String(id)))) { + fieldErrors.value[field.slug] = 'Ongeldige tag geselecteerd.' + firstSlug ??= field.slug + } + } + } + + if (firstSlug) { + nextTick(() => { + document.getElementById(`dyn-field-${firstSlug}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }) + + return false + } + + return true } async function nextStep() { if (await validateCurrentStep()) { - if (currentStep.value < 4) currentStep.value++ + if (currentStep.value < steps.value.length - 1) + currentStep.value++ } } @@ -187,41 +287,47 @@ function prevStep() { } function goToStep(index: number) { - if (index < currentStep.value) { + if (index < currentStep.value) currentStep.value = index - } } -// Section toggle -function toggleSection(sectionName: string) { - const idx = selectedSections.value.indexOf(sectionName) +function toggleSection(sectionId: string) { + const idx = selectedSectionIds.value.indexOf(sectionId) if (idx !== -1) { - selectedSections.value.splice(idx, 1) + selectedSectionIds.value.splice(idx, 1) } - else if (selectedSections.value.length < 5) { - selectedSections.value.push(sectionName) + else if (selectedSectionIds.value.length < 5) { + selectedSectionIds.value.push(sectionId) } } -// Time slot toggle +function moveSection(sectionId: string, direction: -1 | 1) { + const i = selectedSectionIds.value.indexOf(sectionId) + const j = i + direction + if (i < 0 || j < 0 || j >= selectedSectionIds.value.length) + return + const next = [...selectedSectionIds.value] + const a = next[i]! + const b = next[j]! + next[i] = b + next[j] = a + selectedSectionIds.value = next +} + function toggleTimeSlot(slotId: string) { const idx = selectedTimeSlotIds.value.indexOf(slotId) - if (idx >= 0) { + if (idx >= 0) selectedTimeSlotIds.value.splice(idx, 1) - delete timeSlotPreferences.value[slotId] - } - else { + else selectedTimeSlotIds.value.push(slotId) - timeSlotPreferences.value[slotId] = 3 - } } -// Helpers function formatDate(dateStr: string): string { return new Date(`${dateStr}T00:00:00`).toLocaleDateString('nl-NL', { weekday: 'long', day: 'numeric', month: 'long', + year: 'numeric', }) } @@ -240,49 +346,95 @@ function formatTimeRange(start: string, end: string): string { return `${start.slice(0, 5)} – ${end.slice(0, 5)}` } -// Submit +function buildFieldValues(): Record | undefined { + const out: Record = {} + + for (const f of registrationFieldsList.value) { + const raw = fieldValues.value[f.slug] + + if (f.field_type === 'boolean') { + if (typeof raw === 'boolean') + out[f.slug] = raw + continue + } + + if (raw === undefined || raw === null) continue + if (raw === '') continue + if (Array.isArray(raw) && raw.length === 0) continue + out[f.slug] = raw as FieldValuePayload + } + + if (Object.keys(out).length === 0) + return undefined + + return out +} + +function isPersonalField(key: string): key is typeof personalFields[number] { + return (personalFields as readonly string[]).includes(key) +} + +function applyServerValidationErrors(serverErrors: Record) { + fieldErrors.value = {} + for (const [key, msgs] of Object.entries(serverErrors)) { + const m = /^field_values\.(.+)$/.exec(key) + if (m?.[1]) { + fieldErrors.value[m[1]] = msgs[0] ?? 'Ongeldige waarde.' + + continue + } + + if (isPersonalField(key)) + setFieldError(key, msgs[0] ?? 'Ongeldige waarde.') + } +} + async function onSubmit() { submitError.value = null + fieldErrors.value = {} - // Validate steps 0-2 - for (let step = 0; step <= 2; step++) { - const fields = stepFields[step] - if (!fields) continue - const results = await Promise.all(fields.map(f => validateField(f))) - - if (!results.every(r => r.valid)) { - currentStep.value = step + for (const f of personalFields) { + const r = await validateField(f) + if (!r.valid) { + currentStep.value = stepKinds.value.indexOf('personal') return } } + if (!validateDynamicFields()) { + currentStep.value = stepKinds.value.indexOf('dynamic') + + return + } + if (!registrationData.value) return - const sectionPreferences: SectionPreference[] = selectedSections.value.map((sectionName, index) => ({ - section_name: sectionName, - priority: index + 1, - })) - - const availabilities: VolunteerAvailability[] = selectedTimeSlotIds.value.map(id => ({ - time_slot_id: id, - preference_level: timeSlotPreferences.value[id] ?? 3, - })) - const payload: VolunteerRegistrationForm = { first_name: firstName.value ?? '', last_name: lastName.value ?? '', - date_of_birth: dateOfBirth.value ?? '', email: email.value ?? '', - phone: phone.value ?? '', - tshirt_size: tshirtSize.value ?? '', - first_aid: firstAid.value ?? false, - allergies: allergies.value ?? '', - driving_licence: drivingLicence.value ?? false, - motivation: motivation.value ?? '', - motivation_other: motivationOther.value ?? '', - section_preferences: sectionPreferences, - availabilities, + phone: (phone.value && String(phone.value).trim() !== '') ? String(phone.value) : undefined, + date_of_birth: (dateOfBirth.value && String(dateOfBirth.value).trim() !== '') + ? String(dateOfBirth.value) + : undefined, + } + + const fv = buildFieldValues() + if (fv) payload.field_values = fv + + if (registrationData.value.event.registration_show_section_preferences && selectedSectionIds.value.length > 0) { + payload.section_preferences = selectedSectionIds.value.map((id, index) => ({ + festival_section_id: id, + priority: index + 1, + })) + } + + if (registrationData.value.event.registration_show_availability && selectedTimeSlotIds.value.length > 0) { + payload.availabilities = selectedTimeSlotIds.value.map(id => ({ + time_slot_id: id, + preference_level: 3, + })) } try { @@ -301,20 +453,35 @@ async function onSubmit() { }) } catch (error: unknown) { - const axiosError = error as { response?: { status?: number; data?: { errors?: Record } } } + const axiosError = error as { response?: { status?: number; data?: { errors?: Record; message?: string } } } if (axiosError.response?.status === 422) { const serverErrors = axiosError.response.data?.errors - if (serverErrors) { - for (const field of Object.keys(serverErrors)) { - for (const [step, fields] of Object.entries(stepFields)) { - if (fields.includes(field as FormField)) { - currentStep.value = Number(step) + applyServerValidationErrors(serverErrors) - return - } - } + const keys = Object.keys(serverErrors) + if (keys.some(k => isPersonalField(k))) { + currentStep.value = stepKinds.value.indexOf('personal') + + return + } + if (keys.some(k => k.startsWith('field_values.'))) { + currentStep.value = stepKinds.value.indexOf('dynamic') + + return + } + if (keys.some(k => k.startsWith('section_preferences'))) { + const idx = stepKinds.value.indexOf('sections') + if (idx !== -1) currentStep.value = idx + + return + } + if (keys.some(k => k.startsWith('availabilities'))) { + const idx = stepKinds.value.indexOf('availability') + if (idx !== -1) currentStep.value = idx + + return } } submitError.value = 'Er zijn validatiefouten gevonden. Controleer je invoer.' @@ -330,11 +497,12 @@ async function onSubmit() {
@@ -353,11 +521,12 @@ async function onSubmit() {
@@ -383,7 +552,10 @@ async function onSubmit() {
-
+
-

+

{{ registrationData.event.name }} -

-

+ +

{{ formattedDates }}

@@ -415,90 +589,90 @@ async function onSubmit() {
-

+

{{ registrationData.event.name }} -

-

+ +

{{ formattedDates }}

- - - -

- {{ registrationData.event.registration_welcome_text }} -

-
- - - - Je bent ingelogd als {{ authStore.user?.full_name }}. Je gegevens zijn automatisch ingevuld. - -
Al een account? Log in om je gegevens automatisch in te vullen.
- + +
+ + + + Je bent ingelogd als {{ authStore.user?.full_name }}. Je gegevens zijn automatisch ingevuld. + + {{ submitError }} - - -
+
-
- tabler-check - + icon="tabler-check" + /> {{ index + 1 }}
-
{{ step.title }}
{{ step.subtitle }}
-
+
- @@ -592,38 +745,39 @@ async function onSubmit() { v-if="index < currentStep" start size="14" - > - tabler-check - + icon="tabler-check" + /> {{ index + 1 }}. {{ step.title }}
- - - +
-
- {{ steps[currentStep].title }} -
+

+ Deel {{ currentStep + 1 }} van {{ steps.length }} +

+

+ {{ steps[currentStep]?.title }} +

- {{ steps[currentStep].subtitle }} + {{ steps[currentStep]?.subtitle }}

- -
+ +
-
+
-
+

Contactgegevens -

+

Vul deze gegevens zorgvuldig in: we gebruiken ze om alle informatie over het evenement naar je te versturen. Je e-mailadres is ook je gebruikersnaam voor Crewli, het systeem waar je straks alle informatie terugvindt. @@ -752,190 +906,110 @@ async function onSubmit() {

- -
- - - - - - - -
- - - -
-
- - - - - - -
+ +
+
- -
- - - - - + +
+

+ We proberen hier zoveel mogelijk rekening mee te houden, + maar de uiteindelijke indeling wordt bepaald door de organisatie. +

- - - - - - - -
- - -
- -
+
Volgende @@ -1067,7 +1137,9 @@ async function onSubmit() { @@ -1083,8 +1155,7 @@ async function onSubmit() { - -
+
Powered by Crewli
@@ -1095,14 +1166,79 @@ async function onSubmit() { .registration-container { position: relative; z-index: 1; - max-inline-size: 1000px; + max-inline-size: 1040px; +} + +.registration-header-fallback { + background: rgb(var(--v-theme-surface)); + border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important; +} + +.registration-welcome-html :deep(p) { + margin-block-end: 0.5rem; +} + +.registration-welcome-html :deep(p:last-child) { + margin-block-end: 0; +} + +.step-indicator { + inline-size: 48px; + block-size: 48px; + border-radius: 10px; + font-size: 16px; + font-weight: 700; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.step-indicator--active { + background-color: rgb(var(--v-theme-primary)); + color: rgb(var(--v-theme-on-primary)); +} + +.step-indicator--done { + background-color: rgba(var(--v-theme-primary), 0.12); + color: rgb(var(--v-theme-primary)); +} + +.step-indicator--todo { + background-color: rgba(var(--v-theme-on-surface), 0.06); + color: rgba(var(--v-theme-on-surface), 0.35); +} + +.section-category-heading { + padding-block-end: 8px; + border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.registration-section-grid { + align-items: stretch; +} + +.section-pref-card { + border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important; + transition: border-color 0.15s ease, background-color 0.15s ease; +} + +.section-pref-card--selected { + border: 2px solid rgb(var(--v-theme-primary)) !important; + background-color: rgba(var(--v-theme-primary), 0.06); +} + +.priority-pill { + position: absolute; + inset-block-start: 10px; + inset-inline-end: 10px; + z-index: 1; + font-size: 11px; + font-weight: 600; + padding-block: 4px; + padding-inline: 10px; + border-radius: 999px; + background-color: rgba(var(--v-theme-on-surface), 0.75); + color: rgb(var(--v-theme-surface)); } -/* - Native date inputs have a large intrinsic min-width; without min-width: 0 the - grid cell grows and the prepend-inner icon no longer sits inside the outline - next to the value (Vuetify v-field grid: prepend-inner | field). -*/ .volunteer-reg-dob-field :deep(.v-field__field) { min-inline-size: 0; } @@ -1113,10 +1249,6 @@ async function onSubmit() { min-inline-size: 0; } -/* - Vuetify’s .v-list uses overflow: auto, which shows scrollbars on this step when - list rows are slightly wider than the card (e.g. checkbox + text + rating). -*/ .registration-availability-list { overflow: visible !important; } diff --git a/apps/portal/src/schemas/registrationSchema.ts b/apps/portal/src/schemas/registrationSchema.ts index 34f30601..3f228c49 100644 --- a/apps/portal/src/schemas/registrationSchema.ts +++ b/apps/portal/src/schemas/registrationSchema.ts @@ -1,6 +1,7 @@ import { z } from 'zod' -export const step1Schema = z.object({ +/** Fixed fields for step "Over jou" (portal volunteer registration). */ +export const personalStepSchema = z.object({ first_name: z.string().min(1, 'Voornaam is verplicht').max(255), last_name: z.string().min(1, 'Achternaam is verplicht').max(255), email: z.string().min(1, 'E-mailadres is verplicht').email('Ongeldig e-mailadres').max(255), @@ -8,18 +9,4 @@ export const step1Schema = z.object({ phone: z.string().max(50).optional().or(z.literal('')), }) -export const step2Schema = z.object({ - tshirt_size: z.enum(['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']).optional().or(z.literal('')), - first_aid: z.boolean().default(false), - allergies: z.string().max(500).optional().or(z.literal('')), - driving_licence: z.boolean().default(false), -}) - -export const step3Schema = z.object({ - motivation: z.string().max(1000).optional().or(z.literal('')), - motivation_other: z.string().max(500).optional().or(z.literal('')), -}) - -export const fullRegistrationSchema = step1Schema - .merge(step2Schema) - .merge(step3Schema) +export const fullRegistrationSchema = personalStepSchema diff --git a/apps/portal/src/types/registration.ts b/apps/portal/src/types/registration.ts index 9a7b1a14..f4d52da9 100644 --- a/apps/portal/src/types/registration.ts +++ b/apps/portal/src/types/registration.ts @@ -1,3 +1,33 @@ +export type RegistrationFieldType = + | 'text' + | 'textarea' + | 'select' + | 'multiselect' + | 'checkbox' + | 'radio' + | 'boolean' + | 'number' + | 'tag_picker' + +export interface RegistrationTagOption { + id: string + name: string + category: string | null +} + +export interface RegistrationField { + id: string + label: string + slug: string + field_type: RegistrationFieldType + options: string[] | null + tag_category: string | null + is_required: boolean + section: string | null + help_text: string | null + available_tags?: RegistrationTagOption[] +} + export interface EventRegistrationData { event: { id: string @@ -8,9 +38,12 @@ export interface EventRegistrationData { registration_banner_url: string | null registration_welcome_text: string | null registration_logo_url: string | null + registration_show_section_preferences: boolean + registration_show_availability: boolean } sections: SectionOption[] time_slots: TimeSlotOption[] + registration_fields: RegistrationField[] } export interface SectionOption { @@ -30,33 +63,25 @@ export interface TimeSlotOption { duration_hours: number } -export interface SectionPreference { - section_name: string +export interface SectionPreferencePayload { + festival_section_id: string priority: number } -export interface VolunteerAvailability { +export interface VolunteerAvailabilityPayload { time_slot_id: string - preference_level: number + preference_level?: number } +export type FieldValuePayload = string | number | boolean | string[] | null + export interface VolunteerRegistrationForm { - // Step 1 first_name: string last_name: string - date_of_birth: string + date_of_birth?: string email: string - phone: string - // Step 2 - tshirt_size: string - first_aid: boolean - allergies: string - driving_licence: boolean - // Step 3 - motivation: string - motivation_other: string - // Step 4 - section_preferences: SectionPreference[] - // Step 5 - availabilities: VolunteerAvailability[] + phone?: string + field_values?: Record + section_preferences?: SectionPreferencePayload[] + availabilities?: VolunteerAvailabilityPayload[] }