From a9dcee0fc7bcd37e0ffae8ea1713bbe012ede8dd Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sun, 12 Apr 2026 23:44:14 +0200 Subject: [PATCH] feat(app): registration fields management page in event settings Adds a new settings sub-page for managing dynamic registration form fields per event. Includes sortable field list, create/edit dialog, template picker, and import-from-event functionality. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../event/ImportFromEventDialog.vue | 120 +++++ .../event/RegistrationFieldCard.vue | 156 +++++++ .../event/RegistrationFieldFormDialog.vue | 304 ++++++++++++ .../components/event/TemplatePickerDialog.vue | 141 ++++++ .../api/useRegistrationFormFields.ts | 144 ++++++ .../src/pages/events/[id]/settings/index.vue | 27 ++ .../[id]/settings/registration-fields.vue | 441 ++++++++++++++++++ apps/app/src/types/event.ts | 4 + apps/app/src/types/registration-form-field.ts | 37 ++ apps/app/typed-router.d.ts | 2 + 10 files changed, 1376 insertions(+) create mode 100644 apps/app/src/components/event/ImportFromEventDialog.vue create mode 100644 apps/app/src/components/event/RegistrationFieldCard.vue create mode 100644 apps/app/src/components/event/RegistrationFieldFormDialog.vue create mode 100644 apps/app/src/components/event/TemplatePickerDialog.vue create mode 100644 apps/app/src/composables/api/useRegistrationFormFields.ts create mode 100644 apps/app/src/pages/events/[id]/settings/registration-fields.vue create mode 100644 apps/app/src/types/registration-form-field.ts diff --git a/apps/app/src/components/event/ImportFromEventDialog.vue b/apps/app/src/components/event/ImportFromEventDialog.vue new file mode 100644 index 00000000..eee7f276 --- /dev/null +++ b/apps/app/src/components/event/ImportFromEventDialog.vue @@ -0,0 +1,120 @@ + + + diff --git a/apps/app/src/components/event/RegistrationFieldCard.vue b/apps/app/src/components/event/RegistrationFieldCard.vue new file mode 100644 index 00000000..b7f53175 --- /dev/null +++ b/apps/app/src/components/event/RegistrationFieldCard.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/apps/app/src/components/event/RegistrationFieldFormDialog.vue b/apps/app/src/components/event/RegistrationFieldFormDialog.vue new file mode 100644 index 00000000..c22e29ac --- /dev/null +++ b/apps/app/src/components/event/RegistrationFieldFormDialog.vue @@ -0,0 +1,304 @@ + + + diff --git a/apps/app/src/components/event/TemplatePickerDialog.vue b/apps/app/src/components/event/TemplatePickerDialog.vue new file mode 100644 index 00000000..f9c6b6a1 --- /dev/null +++ b/apps/app/src/components/event/TemplatePickerDialog.vue @@ -0,0 +1,141 @@ + + + diff --git a/apps/app/src/composables/api/useRegistrationFormFields.ts b/apps/app/src/composables/api/useRegistrationFormFields.ts new file mode 100644 index 00000000..84adce45 --- /dev/null +++ b/apps/app/src/composables/api/useRegistrationFormFields.ts @@ -0,0 +1,144 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' +import type { Ref } from 'vue' +import { apiClient } from '@/lib/axios' +import type { + RegistrationFormField, + RegistrationFormFieldCreateDTO, + RegistrationFormFieldUpdateDTO, +} from '@/types/registration-form-field' + +interface ApiResponse { + success: boolean + data: T + message?: string +} + +export function useRegistrationFormFields(eventId: Ref) { + return useQuery({ + queryKey: ['registration-form-fields', eventId], + queryFn: async () => { + const { data } = await apiClient.get<{ data: RegistrationFormField[] }>( + `/events/${eventId.value}/registration-fields`, + ) + + return data.data + }, + enabled: () => !!eventId.value, + staleTime: Infinity, + }) +} + +export function useCreateRegistrationFormField(eventId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: RegistrationFormFieldCreateDTO) => { + const { data } = await apiClient.post>( + `/events/${eventId.value}/registration-fields`, + payload, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-form-fields', eventId] }) + }, + }) +} + +export function useUpdateRegistrationFormField(eventId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ id, ...payload }: RegistrationFormFieldUpdateDTO & { id: string }) => { + const { data } = await apiClient.put>( + `/events/${eventId.value}/registration-fields/${id}`, + payload, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-form-fields', eventId] }) + }, + }) +} + +export function useDeleteRegistrationFormField(eventId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (id: string) => { + await apiClient.delete(`/events/${eventId.value}/registration-fields/${id}`) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-form-fields', eventId] }) + }, + }) +} + +export function useReorderRegistrationFormFields(eventId: Ref) { + const queryClient = useQueryClient() + let previousFields: RegistrationFormField[] | undefined + + return useMutation({ + mutationFn: async (orderedIds: string[]) => { + await apiClient.post(`/events/${eventId.value}/registration-fields/reorder`, { + ids: orderedIds, + }) + }, + onMutate: async (orderedIds) => { + await queryClient.cancelQueries({ queryKey: ['registration-form-fields', eventId.value] }) + previousFields = queryClient.getQueryData(['registration-form-fields', eventId.value]) + + if (previousFields) { + const byId = new Map(previousFields.map(f => [f.id, f])) + const reordered = orderedIds + .map(id => byId.get(id)) + .filter((f): f is RegistrationFormField => !!f) + queryClient.setQueryData(['registration-form-fields', eventId.value], reordered) + } + }, + onError: () => { + if (previousFields) { + queryClient.setQueryData(['registration-form-fields', eventId.value], previousFields) + } + }, + }) +} + +export function useCreateFieldFromTemplate(eventId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (templateId: string) => { + const { data } = await apiClient.post>( + `/events/${eventId.value}/registration-fields/from-template`, + { template_id: templateId }, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-form-fields', eventId] }) + }, + }) +} + +export function useImportFieldsFromEvent(eventId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (sourceEventId: string) => { + const { data } = await apiClient.post>( + `/events/${eventId.value}/registration-fields/import-from-event`, + { source_event_id: sourceEventId }, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-form-fields', eventId] }) + }, + }) +} diff --git a/apps/app/src/pages/events/[id]/settings/index.vue b/apps/app/src/pages/events/[id]/settings/index.vue index dd0d091b..beabb864 100644 --- a/apps/app/src/pages/events/[id]/settings/index.vue +++ b/apps/app/src/pages/events/[id]/settings/index.vue @@ -17,6 +17,12 @@ const authStore = useAuthStore() const orgId = computed(() => authStore.currentOrganisation?.id ?? '') const eventId = computed(() => String((route.params as { id: string }).id)) +const settingsTab = computed(() => { + const name = route.name as string + if (name?.includes('registration-fields')) return 'registration-fields' + return 'branding' +}) + const { mutate: updateEvent, isPending: isUpdating } = useUpdateEvent(orgId, eventId) const { mutate: uploadImage, isPending: isUploading } = useUploadEventImage(orgId, eventId) @@ -56,6 +62,27 @@ function onClearImage(event: EventItem, type: 'banner' | 'logo') {