From 1c0ac488b0e8169394b45004278d3d790cf0e307 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sun, 12 Apr 2026 23:02:07 +0200 Subject: [PATCH] feat(app): organisation settings page with tags & registration field templates Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/organisation/PersonTagsTab.vue | 477 +++++++++++++++ .../RegistrationFieldTemplatesTab.vue | 565 ++++++++++++++++++ apps/app/src/composables/api/usePersonTags.ts | 91 +++ .../api/useRegistrationFieldTemplates.ts | 74 +++ apps/app/src/navigation/vertical/index.ts | 5 + apps/app/src/pages/organisation/settings.vue | 67 +++ apps/app/src/types/person-tag.ts | 21 + .../src/types/registration-field-template.ts | 68 +++ 8 files changed, 1368 insertions(+) create mode 100644 apps/app/src/components/organisation/PersonTagsTab.vue create mode 100644 apps/app/src/components/organisation/RegistrationFieldTemplatesTab.vue create mode 100644 apps/app/src/composables/api/usePersonTags.ts create mode 100644 apps/app/src/composables/api/useRegistrationFieldTemplates.ts create mode 100644 apps/app/src/pages/organisation/settings.vue create mode 100644 apps/app/src/types/person-tag.ts create mode 100644 apps/app/src/types/registration-field-template.ts diff --git a/apps/app/src/components/organisation/PersonTagsTab.vue b/apps/app/src/components/organisation/PersonTagsTab.vue new file mode 100644 index 00000000..cc01afcc --- /dev/null +++ b/apps/app/src/components/organisation/PersonTagsTab.vue @@ -0,0 +1,477 @@ + + + diff --git a/apps/app/src/components/organisation/RegistrationFieldTemplatesTab.vue b/apps/app/src/components/organisation/RegistrationFieldTemplatesTab.vue new file mode 100644 index 00000000..d991da18 --- /dev/null +++ b/apps/app/src/components/organisation/RegistrationFieldTemplatesTab.vue @@ -0,0 +1,565 @@ + + + diff --git a/apps/app/src/composables/api/usePersonTags.ts b/apps/app/src/composables/api/usePersonTags.ts new file mode 100644 index 00000000..b2811d51 --- /dev/null +++ b/apps/app/src/composables/api/usePersonTags.ts @@ -0,0 +1,91 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' +import type { Ref } from 'vue' +import { apiClient } from '@/lib/axios' +import type { PersonTag, PersonTagCreateDTO, PersonTagUpdateDTO } from '@/types/person-tag' + +interface ApiResponse { + success: boolean + data: T + message?: string +} + +export function usePersonTags(orgId: Ref) { + return useQuery({ + queryKey: ['person-tags', orgId], + queryFn: async () => { + const { data } = await apiClient.get<{ data: PersonTag[] }>( + `/organisations/${orgId.value}/person-tags`, + ) + + return data.data + }, + enabled: () => !!orgId.value, + staleTime: Infinity, + }) +} + +export function usePersonTagCategories(orgId: Ref) { + return useQuery({ + queryKey: ['person-tag-categories', orgId], + queryFn: async () => { + const { data } = await apiClient.get<{ data: string[] }>( + `/organisations/${orgId.value}/person-tag-categories`, + ) + + return data.data + }, + enabled: () => !!orgId.value, + staleTime: Infinity, + }) +} + +export function useCreatePersonTag(orgId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: PersonTagCreateDTO) => { + const { data } = await apiClient.post>( + `/organisations/${orgId.value}/person-tags`, + payload, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['person-tags', orgId] }) + queryClient.invalidateQueries({ queryKey: ['person-tag-categories', orgId] }) + }, + }) +} + +export function useUpdatePersonTag(orgId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ id, ...payload }: PersonTagUpdateDTO & { id: string }) => { + const { data } = await apiClient.put>( + `/organisations/${orgId.value}/person-tags/${id}`, + payload, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['person-tags', orgId] }) + queryClient.invalidateQueries({ queryKey: ['person-tag-categories', orgId] }) + }, + }) +} + +export function useDeletePersonTag(orgId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (id: string) => { + await apiClient.delete(`/organisations/${orgId.value}/person-tags/${id}`) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['person-tags', orgId] }) + }, + }) +} diff --git a/apps/app/src/composables/api/useRegistrationFieldTemplates.ts b/apps/app/src/composables/api/useRegistrationFieldTemplates.ts new file mode 100644 index 00000000..feeaa97a --- /dev/null +++ b/apps/app/src/composables/api/useRegistrationFieldTemplates.ts @@ -0,0 +1,74 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' +import type { Ref } from 'vue' +import { apiClient } from '@/lib/axios' +import type { RegistrationFieldTemplate, RegistrationFieldTemplateCreateDTO, RegistrationFieldTemplateUpdateDTO } from '@/types/registration-field-template' + +interface ApiResponse { + success: boolean + data: T + message?: string +} + +export function useRegistrationFieldTemplates(orgId: Ref) { + return useQuery({ + queryKey: ['registration-field-templates', orgId], + queryFn: async () => { + const { data } = await apiClient.get<{ data: RegistrationFieldTemplate[] }>( + `/organisations/${orgId.value}/registration-field-templates`, + ) + + return data.data + }, + enabled: () => !!orgId.value, + staleTime: Infinity, + }) +} + +export function useCreateRegistrationFieldTemplate(orgId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: RegistrationFieldTemplateCreateDTO) => { + const { data } = await apiClient.post>( + `/organisations/${orgId.value}/registration-field-templates`, + payload, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-field-templates', orgId] }) + }, + }) +} + +export function useUpdateRegistrationFieldTemplate(orgId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ id, ...payload }: RegistrationFieldTemplateUpdateDTO & { id: string }) => { + const { data } = await apiClient.put>( + `/organisations/${orgId.value}/registration-field-templates/${id}`, + payload, + ) + + return data.data + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-field-templates', orgId] }) + }, + }) +} + +export function useDeleteRegistrationFieldTemplate(orgId: Ref) { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (id: string) => { + await apiClient.delete(`/organisations/${orgId.value}/registration-field-templates/${id}`) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['registration-field-templates', orgId] }) + }, + }) +} diff --git a/apps/app/src/navigation/vertical/index.ts b/apps/app/src/navigation/vertical/index.ts index 46f423bf..77fd4eae 100644 --- a/apps/app/src/navigation/vertical/index.ts +++ b/apps/app/src/navigation/vertical/index.ts @@ -29,4 +29,9 @@ export default [ to: { name: 'organisation-companies' }, icon: { icon: 'tabler-building' }, }, + { + title: 'Instellingen', + to: { name: 'organisation-settings' }, + icon: { icon: 'tabler-settings' }, + }, ] diff --git a/apps/app/src/pages/organisation/settings.vue b/apps/app/src/pages/organisation/settings.vue new file mode 100644 index 00000000..cd3e33d0 --- /dev/null +++ b/apps/app/src/pages/organisation/settings.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/app/src/types/person-tag.ts b/apps/app/src/types/person-tag.ts new file mode 100644 index 00000000..9713a5f1 --- /dev/null +++ b/apps/app/src/types/person-tag.ts @@ -0,0 +1,21 @@ +export interface PersonTag { + id: string + name: string + category: string | null + icon: string | null + color: string | null + is_active: boolean + sort_order: number +} + +export interface PersonTagCreateDTO { + name: string + category?: string | null + icon?: string | null + color?: string | null + sort_order?: number +} + +export interface PersonTagUpdateDTO extends Partial { + is_active?: boolean +} diff --git a/apps/app/src/types/registration-field-template.ts b/apps/app/src/types/registration-field-template.ts new file mode 100644 index 00000000..a2edccb7 --- /dev/null +++ b/apps/app/src/types/registration-field-template.ts @@ -0,0 +1,68 @@ +export const RegistrationFieldType = { + TEXT: 'text', + TEXTAREA: 'textarea', + SELECT: 'select', + MULTISELECT: 'multiselect', + CHECKBOX: 'checkbox', + RADIO: 'radio', + BOOLEAN: 'boolean', + NUMBER: 'number', + TAG_PICKER: 'tag_picker', +} as const + +export type RegistrationFieldType = typeof RegistrationFieldType[keyof typeof RegistrationFieldType] + +export const FIELD_TYPE_LABELS: Record = { + text: 'Tekstveld', + textarea: 'Tekstvak', + select: 'Enkele keuze (dropdown)', + multiselect: 'Meervoudige keuze', + checkbox: 'Checkbox', + radio: 'Keuzerondjes', + boolean: 'Ja/Nee', + number: 'Getal', + tag_picker: 'Tag-kiezer', +} + +export const FIELD_TYPES_WITH_OPTIONS: RegistrationFieldType[] = [ + 'select', + 'multiselect', + 'radio', + 'checkbox', +] + +export interface RegistrationFieldTemplate { + id: string + label: string + slug: string + field_type: RegistrationFieldType + options: string[] | null + tag_category: string | null + is_required: boolean + is_filterable: boolean + is_portal_visible: boolean + is_admin_only: boolean + section: string | null + help_text: string | null + sort_order: number + is_system: boolean + is_active: boolean +} + +export interface RegistrationFieldTemplateCreateDTO { + label: string + field_type: RegistrationFieldType + options?: string[] | null + tag_category?: string | null + is_required?: boolean + is_filterable?: boolean + is_portal_visible?: boolean + is_admin_only?: boolean + section?: string | null + help_text?: string | null + sort_order?: number +} + +export interface RegistrationFieldTemplateUpdateDTO extends Partial> { + is_active?: boolean +}