feat: registration section preferences with show_in_registration filtering and deduplication
Add show_in_registration and registration_description columns to festival_sections. Registration form now shows deduplicated sections by name (across sub-events), filtered by show_in_registration=true, grouped by category with card-based UI. Section preferences use section_name instead of section_id. Add GET/PUT registration-settings endpoints for festival-level bulk management. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,8 @@ const form = ref({
|
||||
type: 'standard' as SectionType,
|
||||
crew_auto_accepts: false,
|
||||
responder_self_checkin: true,
|
||||
show_in_registration: false,
|
||||
registration_description: null as string | null,
|
||||
})
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
@@ -56,6 +58,8 @@ function resetForm() {
|
||||
type: 'standard',
|
||||
crew_auto_accepts: false,
|
||||
responder_self_checkin: true,
|
||||
show_in_registration: false,
|
||||
registration_description: null,
|
||||
}
|
||||
errors.value = {}
|
||||
refVForm.value?.resetValidation()
|
||||
@@ -76,6 +80,8 @@ function onSubmit() {
|
||||
sort_order: props.nextSortOrder,
|
||||
crew_auto_accepts: form.value.crew_auto_accepts,
|
||||
responder_self_checkin: form.value.responder_self_checkin,
|
||||
show_in_registration: form.value.show_in_registration,
|
||||
registration_description: form.value.registration_description || null,
|
||||
},
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
@@ -188,6 +194,29 @@ function onSubmit() {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="form.show_in_registration"
|
||||
label="Toon in vrijwilligersregistratie"
|
||||
hint="Dit werkgebied verschijnt in het aanmeldformulier voor vrijwilligers"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="form.show_in_registration"
|
||||
cols="12"
|
||||
>
|
||||
<VTextarea
|
||||
v-model="form.registration_description"
|
||||
label="Beschrijving voor vrijwilligers"
|
||||
:counter="500"
|
||||
rows="2"
|
||||
auto-grow
|
||||
hint="Korte uitleg zodat vrijwilligers weten wat dit werkgebied inhoudt"
|
||||
persistent-hint
|
||||
:error-messages="errors.registration_description"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -28,6 +28,8 @@ const form = ref({
|
||||
type: 'standard' as SectionType,
|
||||
crew_auto_accepts: false,
|
||||
responder_self_checkin: true,
|
||||
show_in_registration: false,
|
||||
registration_description: null as string | null,
|
||||
})
|
||||
|
||||
const errors = ref<Record<string, string>>({})
|
||||
@@ -52,6 +54,8 @@ watch(
|
||||
type: section.type,
|
||||
crew_auto_accepts: section.crew_auto_accepts,
|
||||
responder_self_checkin: section.responder_self_checkin,
|
||||
show_in_registration: section.show_in_registration,
|
||||
registration_description: section.registration_description,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -79,6 +83,8 @@ function onSubmit() {
|
||||
icon: form.value.icon || null,
|
||||
crew_auto_accepts: form.value.crew_auto_accepts,
|
||||
responder_self_checkin: form.value.responder_self_checkin,
|
||||
show_in_registration: form.value.show_in_registration,
|
||||
registration_description: form.value.registration_description || null,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -180,6 +186,29 @@ function onSubmit() {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="form.show_in_registration"
|
||||
label="Toon in vrijwilligersregistratie"
|
||||
hint="Dit werkgebied verschijnt in het aanmeldformulier voor vrijwilligers"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="form.show_in_registration"
|
||||
cols="12"
|
||||
>
|
||||
<VTextarea
|
||||
v-model="form.registration_description"
|
||||
label="Beschrijving voor vrijwilligers"
|
||||
:counter="500"
|
||||
rows="2"
|
||||
auto-grow
|
||||
hint="Korte uitleg zodat vrijwilligers weten wat dit werkgebied inhoudt"
|
||||
persistent-hint
|
||||
:error-messages="errors.registration_description"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface FestivalSection {
|
||||
crew_need: number | null
|
||||
crew_auto_accepts: boolean
|
||||
responder_self_checkin: boolean
|
||||
show_in_registration: boolean
|
||||
registration_description: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -39,6 +41,8 @@ export interface Shift {
|
||||
status: ShiftStatus
|
||||
filled_slots: number
|
||||
fill_rate: number
|
||||
is_overbooked: boolean
|
||||
overbooking_count: number
|
||||
effective_start_time: string
|
||||
effective_end_time: string
|
||||
time_slot: TimeSlot | null
|
||||
@@ -61,6 +65,8 @@ export interface CreateSectionPayload {
|
||||
sort_order?: number
|
||||
crew_auto_accepts?: boolean
|
||||
responder_self_checkin?: boolean
|
||||
show_in_registration?: boolean
|
||||
registration_description?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateSectionPayload extends Partial<CreateSectionPayload> {}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { useRegistrationData, useSubmitRegistration } from '@/composables/api/useVolunteerRegistration'
|
||||
import { fullRegistrationSchema } from '@/schemas/registrationSchema'
|
||||
import type {
|
||||
SectionOption,
|
||||
SectionPreference,
|
||||
TimeSlotOption,
|
||||
VolunteerAvailability,
|
||||
@@ -68,8 +69,8 @@ watch(() => authStore.user, (user) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Step 4: Section preferences
|
||||
const selectedSectionIds = ref<string[]>([])
|
||||
// Step 4: Section preferences (by name, not ID)
|
||||
const selectedSections = ref<string[]>([])
|
||||
|
||||
// Step 5: Availability
|
||||
const selectedTimeSlotIds = ref<string[]>([])
|
||||
@@ -91,6 +92,27 @@ const motivationItems = [
|
||||
|
||||
const stepTitles = ['Over jou', 'Meer over jou', 'Motivatie', 'Secties', 'Beschikbaarheid']
|
||||
|
||||
// Section helpers
|
||||
const sectionsByCategory = computed(() => {
|
||||
if (!registrationData.value?.sections) return {}
|
||||
return registrationData.value.sections.reduce((groups, section) => {
|
||||
const cat = section.category || 'Overig'
|
||||
if (!groups[cat]) groups[cat] = []
|
||||
groups[cat].push(section)
|
||||
return groups
|
||||
}, {} as Record<string, SectionOption[]>)
|
||||
})
|
||||
|
||||
function isSelected(name: string) {
|
||||
return selectedSections.value.includes(name)
|
||||
}
|
||||
|
||||
function getSelectionPriority(name: string) {
|
||||
return selectedSections.value.indexOf(name) + 1
|
||||
}
|
||||
|
||||
const selectedCount = computed(() => selectedSections.value.length)
|
||||
|
||||
// Computed
|
||||
const timeSlotsByDate = computed(() => {
|
||||
if (!registrationData.value?.time_slots) return []
|
||||
@@ -140,13 +162,13 @@ function prevStep() {
|
||||
}
|
||||
|
||||
// Section toggle
|
||||
function toggleSection(sectionId: string) {
|
||||
const idx = selectedSectionIds.value.indexOf(sectionId)
|
||||
if (idx >= 0) {
|
||||
selectedSectionIds.value.splice(idx, 1)
|
||||
function toggleSection(name: string) {
|
||||
const idx = selectedSections.value.indexOf(name)
|
||||
if (idx !== -1) {
|
||||
selectedSections.value.splice(idx, 1)
|
||||
}
|
||||
else if (selectedSectionIds.value.length < 5) {
|
||||
selectedSectionIds.value.push(sectionId)
|
||||
else if (selectedSections.value.length < 5) {
|
||||
selectedSections.value.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +216,8 @@ async function onSubmit() {
|
||||
|
||||
if (!registrationData.value) return
|
||||
|
||||
const sectionPreferences: SectionPreference[] = selectedSectionIds.value.map((id, index) => ({
|
||||
section_id: id,
|
||||
const sectionPreferences: SectionPreference[] = selectedSections.value.map((name, index) => ({
|
||||
section_name: name,
|
||||
priority: index + 1,
|
||||
}))
|
||||
|
||||
@@ -490,69 +512,87 @@ async function onSubmit() {
|
||||
<!-- Step 4: Voorkeurssecties -->
|
||||
<VWindowItem :value="4">
|
||||
<div class="pa-4 pa-sm-6">
|
||||
<p
|
||||
v-if="registrationData.sections.length === 0"
|
||||
class="text-body-1 text-medium-emphasis"
|
||||
>
|
||||
Er zijn geen secties beschikbaar voor dit evenement.
|
||||
<h3 class="text-h6 mb-1">
|
||||
Bij welke onderdelen wil je het liefst helpen?
|
||||
</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Selecteer maximaal 5 onderdelen. Je eerste keuze heeft de hoogste prioriteit.
|
||||
</p>
|
||||
<template v-else>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Selecteer maximaal 5 secties waar je graag wilt werken.
|
||||
De volgorde van selectie bepaalt je voorkeur.
|
||||
</p>
|
||||
<VList lines="two">
|
||||
<VListItem
|
||||
v-for="section in registrationData.sections"
|
||||
:key="section.id"
|
||||
:disabled="!selectedSectionIds.includes(section.id) && selectedSectionIds.length >= 5"
|
||||
class="section-item"
|
||||
@click="toggleSection(section.id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VCheckboxBtn
|
||||
:model-value="selectedSectionIds.includes(section.id)"
|
||||
:disabled="!selectedSectionIds.includes(section.id) && selectedSectionIds.length >= 5"
|
||||
@click.stop="toggleSection(section.id)"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle class="d-flex align-center ga-2">
|
||||
<VIcon
|
||||
v-if="section.icon"
|
||||
:icon="section.icon"
|
||||
size="18"
|
||||
/>
|
||||
{{ section.name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle v-if="section.category">
|
||||
<VChip
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ section.category }}
|
||||
</VChip>
|
||||
</VListItemSubtitle>
|
||||
<template
|
||||
v-if="selectedSectionIds.includes(section.id)"
|
||||
#append
|
||||
>
|
||||
<VChip
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
Voorkeur {{ selectedSectionIds.indexOf(section.id) + 1 }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
<p
|
||||
v-if="selectedSectionIds.length > 0"
|
||||
class="text-body-2 text-medium-emphasis mt-4"
|
||||
>
|
||||
{{ selectedSectionIds.length }} van 5 secties geselecteerd
|
||||
</p>
|
||||
<template
|
||||
v-for="(sections, category) in sectionsByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="text-subtitle-2 text-medium-emphasis mt-4 mb-2">
|
||||
{{ category }}
|
||||
</div>
|
||||
<VRow dense>
|
||||
<VCol
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
>
|
||||
<VCard
|
||||
:variant="isSelected(section.name) ? 'flat' : 'outlined'"
|
||||
:color="isSelected(section.name) ? 'primary' : undefined"
|
||||
class="cursor-pointer"
|
||||
:disabled="!isSelected(section.name) && selectedCount >= 5"
|
||||
@click="toggleSection(section.name)"
|
||||
>
|
||||
<VCardText class="d-flex align-center ga-3 pa-3">
|
||||
<VCheckboxBtn
|
||||
:model-value="isSelected(section.name)"
|
||||
readonly
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
<VIcon
|
||||
v-if="section.icon"
|
||||
size="20"
|
||||
>
|
||||
{{ section.icon }}
|
||||
</VIcon>
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-body-1 font-weight-medium">
|
||||
{{ section.name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="section.registration_description"
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
>
|
||||
{{ section.registration_description }}
|
||||
</div>
|
||||
</div>
|
||||
<VChip
|
||||
v-if="isSelected(section.name)"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
>
|
||||
#{{ getSelectionPriority(section.name) }}
|
||||
</VChip>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<VAlert
|
||||
v-if="!Object.keys(sectionsByCategory).length"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
>
|
||||
Er zijn nog geen werkgebieden geconfigureerd voor dit evenement.
|
||||
</VAlert>
|
||||
|
||||
<p
|
||||
v-if="selectedCount > 0"
|
||||
class="text-body-2 text-medium-emphasis mt-4"
|
||||
>
|
||||
{{ selectedCount }} van 5 onderdelen geselecteerd
|
||||
</p>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface SectionOption {
|
||||
name: string
|
||||
category: string | null
|
||||
icon: string | null
|
||||
registration_description: string | null
|
||||
}
|
||||
|
||||
export interface TimeSlotOption {
|
||||
@@ -27,7 +28,7 @@ export interface TimeSlotOption {
|
||||
}
|
||||
|
||||
export interface SectionPreference {
|
||||
section_id: string
|
||||
section_name: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user