refactor: organisation settings — vertical sidebar layout with grouped sections
Replace horizontal tabs with VList-based vertical sidebar following the Vuexy ecommerce settings pattern. Consolidate Tags, Crowd Types, Members, and Registration Fields pages into the settings page as sidebar tabs. Add SettingsGeneral panel with org details form and danger zone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
apps/app/components.d.ts
vendored
8
apps/app/components.d.ts
vendored
@@ -91,6 +91,14 @@ declare module 'vue' {
|
||||
ScrollToTop: typeof import('./src/@core/components/ScrollToTop.vue')['default']
|
||||
SectionsShiftsPanel: typeof import('./src/components/sections/SectionsShiftsPanel.vue')['default']
|
||||
SecurityTab: typeof import('./src/components/account-settings/SecurityTab.vue')['default']
|
||||
SettingsCrowdTypes: typeof import('./src/components/organisation/settings/SettingsCrowdTypes.vue')['default']
|
||||
SettingsEmailBranding: typeof import('./src/components/organisation/settings/SettingsEmailBranding.vue')['default']
|
||||
SettingsEmailLog: typeof import('./src/components/organisation/settings/SettingsEmailLog.vue')['default']
|
||||
SettingsEmailTemplates: typeof import('./src/components/organisation/settings/SettingsEmailTemplates.vue')['default']
|
||||
SettingsGeneral: typeof import('./src/components/organisation/settings/SettingsGeneral.vue')['default']
|
||||
SettingsMembers: typeof import('./src/components/organisation/settings/SettingsMembers.vue')['default']
|
||||
SettingsRegistrationFields: typeof import('./src/components/organisation/settings/SettingsRegistrationFields.vue')['default']
|
||||
SettingsTags: typeof import('./src/components/organisation/settings/SettingsTags.vue')['default']
|
||||
ShareProjectDialog: typeof import('./src/components/dialogs/ShareProjectDialog.vue')['default']
|
||||
ShiftDetailPanel: typeof import('./src/components/shifts/ShiftDetailPanel.vue')['default']
|
||||
Shortcuts: typeof import('./src/@core/components/Shortcuts.vue')['default']
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import CrowdTypesManager from '@/components/organisations/CrowdTypesManager.vue'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CrowdTypesManager :org-id="orgId" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import EmailBrandingTab from '@/components/organisation/EmailBrandingTab.vue'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmailBrandingTab :org-id="orgId" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import EmailLogTab from '@/components/organisation/EmailLogTab.vue'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmailLogTab :org-id="orgId" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import EmailTemplatesTab from '@/components/organisation/EmailTemplatesTab.vue'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmailTemplatesTab :org-id="orgId" />
|
||||
</template>
|
||||
@@ -0,0 +1,321 @@
|
||||
<script setup lang="ts">
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useMyOrganisation, useUpdateOrganisation } from '@/composables/api/useOrganisations'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { requiredValidator } from '@core/utils/validators'
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/auth'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isOrgAdmin = computed(() => {
|
||||
const role = authStore.currentOrganisation?.role
|
||||
return role === 'org_admin' || authStore.isSuperAdmin
|
||||
})
|
||||
|
||||
const { data: organisation, isLoading, isError, refetch } = useMyOrganisation()
|
||||
const { mutate: updateOrganisation, isPending } = useUpdateOrganisation()
|
||||
|
||||
const name = ref('')
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const refVForm = ref<VForm>()
|
||||
const showSuccess = ref(false)
|
||||
const showCopied = ref(false)
|
||||
const isDeleteDialogOpen = ref(false)
|
||||
const showDeleteNotice = ref(false)
|
||||
|
||||
watch(() => organisation.value, (org) => {
|
||||
if (org) {
|
||||
name.value = org.name
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function onSubmit() {
|
||||
refVForm.value?.validate().then(({ valid }) => {
|
||||
if (!valid || !organisation.value) return
|
||||
|
||||
errors.value = {}
|
||||
|
||||
updateOrganisation(
|
||||
{ id: organisation.value.id, name: name.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccess.value = true
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
const data = (err as AxiosError<ApiErrorResponse>).response?.data
|
||||
if (data?.errors) {
|
||||
errors.value = { name: data.errors.name?.[0] ?? '' }
|
||||
}
|
||||
else if (data?.message) {
|
||||
errors.value = { name: data.message }
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function copySlug() {
|
||||
if (organisation.value?.slug) {
|
||||
navigator.clipboard.writeText(organisation.value.slug)
|
||||
showCopied.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="isLoading"
|
||||
type="card"
|
||||
/>
|
||||
|
||||
<!-- Error -->
|
||||
<VAlert
|
||||
v-else-if="isError"
|
||||
type="error"
|
||||
class="mb-4"
|
||||
>
|
||||
Kon organisatie niet laden.
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="refetch()"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<template v-else-if="organisation">
|
||||
<!-- Organisation details -->
|
||||
<VCard
|
||||
title="Organisatiegegevens"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="name"
|
||||
label="Organisatienaam"
|
||||
:rules="[requiredValidator]"
|
||||
:error-messages="errors.name"
|
||||
:readonly="!isOrgAdmin"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
:model-value="organisation.slug"
|
||||
label="Slug"
|
||||
readonly
|
||||
>
|
||||
<template #append-inner>
|
||||
<VBtn
|
||||
icon="tabler-copy"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
@click="copySlug"
|
||||
/>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
label="Contactpersoon"
|
||||
disabled
|
||||
>
|
||||
<template #append-inner>
|
||||
<VTooltip text="Binnenkort beschikbaar">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<VIcon
|
||||
v-bind="tooltipProps"
|
||||
icon="tabler-info-circle"
|
||||
size="18"
|
||||
color="disabled"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
label="Contact e-mail"
|
||||
disabled
|
||||
>
|
||||
<template #append-inner>
|
||||
<VTooltip text="Binnenkort beschikbaar">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<VIcon
|
||||
v-bind="tooltipProps"
|
||||
icon="tabler-info-circle"
|
||||
size="18"
|
||||
color="disabled"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
label="Telefoon"
|
||||
disabled
|
||||
>
|
||||
<template #append-inner>
|
||||
<VTooltip text="Binnenkort beschikbaar">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<VIcon
|
||||
v-bind="tooltipProps"
|
||||
icon="tabler-info-circle"
|
||||
size="18"
|
||||
color="disabled"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
label="Website"
|
||||
disabled
|
||||
>
|
||||
<template #append-inner>
|
||||
<VTooltip text="Binnenkort beschikbaar">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<VIcon
|
||||
v-bind="tooltipProps"
|
||||
icon="tabler-info-circle"
|
||||
size="18"
|
||||
color="disabled"
|
||||
/>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div
|
||||
v-if="isOrgAdmin"
|
||||
class="d-flex justify-end mt-4"
|
||||
>
|
||||
<VBtn
|
||||
type="submit"
|
||||
:loading="isPending"
|
||||
>
|
||||
Wijzigingen opslaan
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Danger zone -->
|
||||
<VCard
|
||||
v-if="isOrgAdmin"
|
||||
class="border-error"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<h6 class="text-h6 text-error">
|
||||
Organisatie verwijderen
|
||||
</h6>
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
Verwijder deze organisatie en alle bijbehorende gegevens permanent. Deze actie kan niet ongedaan worden gemaakt.
|
||||
</p>
|
||||
</div>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
class="ms-4"
|
||||
@click="isDeleteDialogOpen = true"
|
||||
>
|
||||
Verwijderen
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<VDialog
|
||||
v-model="isDeleteDialogOpen"
|
||||
max-width="440"
|
||||
>
|
||||
<VCard title="Organisatie verwijderen">
|
||||
<VCardText class="text-body-1">
|
||||
Het verwijderen van een organisatie is momenteel niet mogelijk via de applicatie.
|
||||
Neem contact op met de platform beheerder.
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isDeleteDialogOpen = false"
|
||||
>
|
||||
Sluiten
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
@click="isDeleteDialogOpen = false; showDeleteNotice = true"
|
||||
>
|
||||
Begrepen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Snackbars -->
|
||||
<VSnackbar
|
||||
v-model="showSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Organisatie bijgewerkt
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showCopied"
|
||||
color="info"
|
||||
:timeout="2000"
|
||||
>
|
||||
Slug gekopieerd
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showDeleteNotice"
|
||||
color="info"
|
||||
:timeout="4000"
|
||||
>
|
||||
Neem contact op met de platform beheerder
|
||||
</VSnackbar>
|
||||
</template>
|
||||
@@ -0,0 +1,484 @@
|
||||
<script setup lang="ts">
|
||||
import { useMemberList, useRemoveMember, useRevokeInvitation } from '@/composables/api/useMembers'
|
||||
import { useAdminChangeEmail } from '@/composables/api/useAccount'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import InviteMemberDialog from '@/components/members/InviteMemberDialog.vue'
|
||||
import EditMemberRoleDialog from '@/components/members/EditMemberRoleDialog.vue'
|
||||
import type { Member } from '@/types/member'
|
||||
|
||||
const props = defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const orgIdRef = computed(() => props.orgId)
|
||||
const orgName = computed(() => authStore.currentOrganisation?.name ?? '')
|
||||
|
||||
const isOrgAdmin = computed(() => {
|
||||
const role = authStore.currentOrganisation?.role
|
||||
return role === 'org_admin' || authStore.isSuperAdmin
|
||||
})
|
||||
|
||||
const { data: memberData, isLoading, isError, refetch } = useMemberList(orgIdRef)
|
||||
|
||||
const members = computed(() => memberData.value?.data ?? [])
|
||||
const pendingInvitations = computed(() => memberData.value?.meta?.pending_invitations ?? [])
|
||||
|
||||
// Invite dialog
|
||||
const isInviteDialogOpen = ref(false)
|
||||
|
||||
// Edit role dialog
|
||||
const isEditRoleDialogOpen = ref(false)
|
||||
const selectedMember = ref<Member | null>(null)
|
||||
|
||||
// Remove member
|
||||
const isRemoveDialogOpen = ref(false)
|
||||
const memberToRemove = ref<Member | null>(null)
|
||||
const { mutate: removeMember, isPending: isRemoving } = useRemoveMember(orgIdRef)
|
||||
|
||||
// Revoke invitation
|
||||
const isRevokeDialogOpen = ref(false)
|
||||
const invitationToRevoke = ref<{ id: string; email: string } | null>(null)
|
||||
const { mutate: revokeInvitation, isPending: isRevoking } = useRevokeInvitation(orgIdRef)
|
||||
|
||||
// Change email
|
||||
const isEmailChangeDialogOpen = ref(false)
|
||||
const memberToChangeEmail = ref<Member | null>(null)
|
||||
const newMemberEmail = ref('')
|
||||
const adminEmailErrors = ref<Record<string, string>>({})
|
||||
const showEmailChangeSuccess = ref(false)
|
||||
const { mutate: adminChangeEmail, isPending: isChangingMemberEmail } = useAdminChangeEmail(orgIdRef)
|
||||
|
||||
const showRemoveSuccess = ref(false)
|
||||
const showRevokeSuccess = ref(false)
|
||||
|
||||
const roleColorMap: Record<string, string> = {
|
||||
org_admin: 'purple',
|
||||
org_member: 'info',
|
||||
event_manager: 'cyan',
|
||||
staff_coordinator: 'orange',
|
||||
volunteer_coordinator: 'success',
|
||||
}
|
||||
|
||||
const roleLabelMap: Record<string, string> = {
|
||||
org_admin: 'Organisatie Beheerder',
|
||||
org_member: 'Organisatie Lid',
|
||||
event_manager: 'Evenement Manager',
|
||||
staff_coordinator: 'Staf Coördinator',
|
||||
volunteer_coordinator: 'Vrijwilliger Coördinator',
|
||||
}
|
||||
|
||||
const memberSearch = ref('')
|
||||
|
||||
const memberHeaders = computed(() => {
|
||||
const headers: Array<{ title: string; key: string; sortable?: boolean; align?: 'start' | 'end' }> = [
|
||||
{ title: 'Naam', key: 'full_name', sortable: true },
|
||||
{ title: 'E-mailadres', key: 'email', sortable: true },
|
||||
{ title: 'Rol', key: 'role', sortable: true },
|
||||
]
|
||||
if (isOrgAdmin.value) {
|
||||
headers.push({ title: 'Acties', key: 'actions', sortable: false, align: 'end' })
|
||||
}
|
||||
return headers
|
||||
})
|
||||
|
||||
const memberSortBy = [{ key: 'full_name', order: 'asc' as const }]
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(p => p[0])
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
const date = d.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
const time = d.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' })
|
||||
return `${date} om ${time} uur`
|
||||
}
|
||||
|
||||
function openEditRole(member: Member) {
|
||||
selectedMember.value = member
|
||||
isEditRoleDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openRemoveDialog(member: Member) {
|
||||
memberToRemove.value = member
|
||||
isRemoveDialogOpen.value = true
|
||||
}
|
||||
|
||||
function confirmRemoveMember() {
|
||||
if (!memberToRemove.value) return
|
||||
|
||||
removeMember(memberToRemove.value.id, {
|
||||
onSuccess: () => {
|
||||
isRemoveDialogOpen.value = false
|
||||
memberToRemove.value = null
|
||||
showRemoveSuccess.value = true
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openRevokeDialog(invitation: { id: string; email: string }) {
|
||||
invitationToRevoke.value = invitation
|
||||
isRevokeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openEmailChangeDialog(member: Member) {
|
||||
memberToChangeEmail.value = member
|
||||
newMemberEmail.value = ''
|
||||
adminEmailErrors.value = {}
|
||||
isEmailChangeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function confirmEmailChange() {
|
||||
if (!memberToChangeEmail.value) return
|
||||
adminEmailErrors.value = {}
|
||||
|
||||
adminChangeEmail(
|
||||
{ userId: memberToChangeEmail.value.id, newEmail: newMemberEmail.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
isEmailChangeDialogOpen.value = false
|
||||
memberToChangeEmail.value = null
|
||||
newMemberEmail.value = ''
|
||||
showEmailChangeSuccess.value = true
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const ax = err as { response?: { data?: { errors?: Record<string, string[]> } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
for (const [key, messages] of Object.entries(ax.response.data.errors)) {
|
||||
adminEmailErrors.value[key] = messages[0]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function confirmRevokeInvitation() {
|
||||
if (!invitationToRevoke.value) return
|
||||
|
||||
revokeInvitation(invitationToRevoke.value.id, {
|
||||
onSuccess: () => {
|
||||
isRevokeDialogOpen.value = false
|
||||
invitationToRevoke.value = null
|
||||
showRevokeSuccess.value = true
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="isLoading"
|
||||
type="card"
|
||||
/>
|
||||
|
||||
<!-- Error -->
|
||||
<VAlert
|
||||
v-else-if="isError"
|
||||
type="error"
|
||||
class="mb-4"
|
||||
>
|
||||
Kon leden niet laden.
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="refetch()"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<template v-else>
|
||||
<!-- Members table -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="memberSearch"
|
||||
placeholder="Zoek op naam of e-mail..."
|
||||
prepend-inner-icon="tabler-search"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="isOrgAdmin"
|
||||
cols="12"
|
||||
md="6"
|
||||
class="d-flex justify-end align-center"
|
||||
>
|
||||
<VBtn
|
||||
prepend-icon="tabler-user-plus"
|
||||
@click="isInviteDialogOpen = true"
|
||||
>
|
||||
Lid uitnodigen
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDataTable
|
||||
:headers="memberHeaders"
|
||||
:items="members"
|
||||
:search="memberSearch"
|
||||
:sort-by="memberSortBy"
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template #item.full_name="{ item }">
|
||||
<div class="d-flex align-center gap-x-3">
|
||||
<VAvatar
|
||||
v-if="item.avatar"
|
||||
size="34"
|
||||
:image="item.avatar"
|
||||
/>
|
||||
<VAvatar
|
||||
v-else
|
||||
size="34"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<span class="text-sm">{{ getInitials(item.full_name) }}</span>
|
||||
</VAvatar>
|
||||
<span>{{ item.full_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.role="{ item }">
|
||||
<VChip
|
||||
:color="roleColorMap[item.role] ?? 'default'"
|
||||
size="small"
|
||||
>
|
||||
{{ roleLabelMap[item.role] ?? item.role }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-x-1 justify-end">
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="openEditRole(item)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-mail-forward"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="openEmailChangeDialog(item)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="openRemoveDialog(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center pa-4 text-disabled">
|
||||
<VIcon
|
||||
icon="tabler-users-minus"
|
||||
size="48"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p>Geen leden gevonden</p>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- Pending invitations -->
|
||||
<VCard
|
||||
v-if="isOrgAdmin && pendingInvitations.length > 0"
|
||||
>
|
||||
<VCardTitle>Openstaande uitnodigingen</VCardTitle>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="invitation in pendingInvitations"
|
||||
:key="invitation.id"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
size="34"
|
||||
>
|
||||
<VIcon icon="tabler-mail" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>
|
||||
{{ invitation.email }}
|
||||
<VChip
|
||||
:color="roleColorMap[invitation.role] ?? 'default'"
|
||||
size="x-small"
|
||||
class="ms-2"
|
||||
>
|
||||
{{ roleLabelMap[invitation.role] ?? invitation.role }}
|
||||
</VChip>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
Verloopt op {{ formatDate(invitation.expires_at) }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="openRevokeDialog(invitation)"
|
||||
>
|
||||
Intrekken
|
||||
</VBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<InviteMemberDialog
|
||||
v-if="isOrgAdmin"
|
||||
v-model="isInviteDialogOpen"
|
||||
:org-id="orgId"
|
||||
/>
|
||||
|
||||
<EditMemberRoleDialog
|
||||
v-if="selectedMember"
|
||||
v-model="isEditRoleDialogOpen"
|
||||
:org-id="orgId"
|
||||
:member="selectedMember"
|
||||
/>
|
||||
|
||||
<!-- Remove member confirmation -->
|
||||
<VDialog
|
||||
v-model="isRemoveDialogOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<VCard title="Lid verwijderen">
|
||||
<VCardText>
|
||||
Weet je zeker dat je <strong>{{ memberToRemove?.full_name }}</strong> wilt verwijderen uit de organisatie?
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isRemoveDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
:loading="isRemoving"
|
||||
@click="confirmRemoveMember"
|
||||
>
|
||||
Verwijderen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Revoke invitation confirmation -->
|
||||
<VDialog
|
||||
v-model="isRevokeDialogOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<VCard title="Uitnodiging intrekken">
|
||||
<VCardText>
|
||||
Weet je zeker dat je de uitnodiging voor <strong>{{ invitationToRevoke?.email }}</strong> wilt intrekken?
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isRevokeDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
:loading="isRevoking"
|
||||
@click="confirmRevokeInvitation"
|
||||
>
|
||||
Intrekken
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Change email dialog -->
|
||||
<VDialog
|
||||
v-model="isEmailChangeDialogOpen"
|
||||
max-width="420"
|
||||
>
|
||||
<VCard title="E-mailadres wijzigen">
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Wijzig het e-mailadres van <strong>{{ memberToChangeEmail?.full_name }}</strong>.
|
||||
Er wordt een verificatiemail verstuurd naar het nieuwe adres.
|
||||
</p>
|
||||
<AppTextField
|
||||
v-model="newMemberEmail"
|
||||
label="Nieuw e-mailadres"
|
||||
type="email"
|
||||
:error-messages="adminEmailErrors.new_email"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="isEmailChangeDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="isChangingMemberEmail"
|
||||
:disabled="!newMemberEmail"
|
||||
@click="confirmEmailChange"
|
||||
>
|
||||
Verificatiemail versturen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Success snackbars -->
|
||||
<VSnackbar
|
||||
v-model="showRemoveSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Lid verwijderd
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showRevokeSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Uitnodiging ingetrokken
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showEmailChangeSuccess"
|
||||
color="success"
|
||||
:timeout="4000"
|
||||
>
|
||||
Verificatiemail verstuurd
|
||||
</VSnackbar>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import RegistrationFieldTemplatesTab from '@/components/organisation/RegistrationFieldTemplatesTab.vue'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RegistrationFieldTemplatesTab :org-id="orgId" />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PersonTagsTab from '@/components/organisation/PersonTagsTab.vue'
|
||||
|
||||
defineProps<{
|
||||
orgId: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PersonTagsTab :org-id="orgId" />
|
||||
</template>
|
||||
@@ -17,26 +17,11 @@ export const orgNavItems = [
|
||||
to: { name: 'organisation' },
|
||||
icon: { icon: 'tabler-building' },
|
||||
},
|
||||
{
|
||||
title: 'Leden',
|
||||
to: { name: 'organisation-members' },
|
||||
icon: { icon: 'tabler-users' },
|
||||
},
|
||||
{
|
||||
title: 'Bedrijven',
|
||||
to: { name: 'organisation-companies' },
|
||||
icon: { icon: 'tabler-building' },
|
||||
},
|
||||
{
|
||||
title: 'Tags & Vaardigheden',
|
||||
to: { name: 'organisation-tags' },
|
||||
icon: { icon: 'tabler-tag' },
|
||||
},
|
||||
{
|
||||
title: 'Crowd types',
|
||||
to: { name: 'organisation-crowd-types' },
|
||||
icon: { icon: 'tabler-users-group' },
|
||||
},
|
||||
{
|
||||
title: 'Instellingen',
|
||||
to: { name: 'organisation-settings' },
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useOrganisationStore } from '@/stores/useOrganisationStore'
|
||||
import CrowdTypesManager from '@/components/organisations/CrowdTypesManager.vue'
|
||||
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
const orgId = computed(() => orgStore.activeOrganisationId ?? '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CrowdTypesManager :org-id="orgId" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,494 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useMemberList, useRemoveMember, useRevokeInvitation } from '@/composables/api/useMembers'
|
||||
import { useAdminChangeEmail } from '@/composables/api/useAccount'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { useOrganisationStore } from '@/stores/useOrganisationStore'
|
||||
import InviteMemberDialog from '@/components/members/InviteMemberDialog.vue'
|
||||
import EditMemberRoleDialog from '@/components/members/EditMemberRoleDialog.vue'
|
||||
import type { Member } from '@/types/member'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
const orgId = computed(() => orgStore.activeOrganisationId ?? '')
|
||||
const orgName = computed(() => authStore.currentOrganisation?.name ?? '')
|
||||
|
||||
const isOrgAdmin = computed(() => {
|
||||
const role = authStore.currentOrganisation?.role
|
||||
return role === 'org_admin' || authStore.isSuperAdmin
|
||||
})
|
||||
|
||||
const { data: memberData, isLoading, isError, refetch } = useMemberList(orgId)
|
||||
|
||||
const members = computed(() => memberData.value?.data ?? [])
|
||||
const pendingInvitations = computed(() => memberData.value?.meta?.pending_invitations ?? [])
|
||||
|
||||
// Invite dialog
|
||||
const isInviteDialogOpen = ref(false)
|
||||
|
||||
// Edit role dialog
|
||||
const isEditRoleDialogOpen = ref(false)
|
||||
const selectedMember = ref<Member | null>(null)
|
||||
|
||||
// Remove member
|
||||
const isRemoveDialogOpen = ref(false)
|
||||
const memberToRemove = ref<Member | null>(null)
|
||||
const { mutate: removeMember, isPending: isRemoving } = useRemoveMember(orgId)
|
||||
|
||||
// Revoke invitation
|
||||
const isRevokeDialogOpen = ref(false)
|
||||
const invitationToRevoke = ref<{ id: string; email: string } | null>(null)
|
||||
const { mutate: revokeInvitation, isPending: isRevoking } = useRevokeInvitation(orgId)
|
||||
|
||||
// Change email
|
||||
const isEmailChangeDialogOpen = ref(false)
|
||||
const memberToChangeEmail = ref<Member | null>(null)
|
||||
const newMemberEmail = ref('')
|
||||
const adminEmailErrors = ref<Record<string, string>>({})
|
||||
const showEmailChangeSuccess = ref(false)
|
||||
const { mutate: adminChangeEmail, isPending: isChangingMemberEmail } = useAdminChangeEmail(orgId)
|
||||
|
||||
const showRemoveSuccess = ref(false)
|
||||
const showRevokeSuccess = ref(false)
|
||||
|
||||
const roleColorMap: Record<string, string> = {
|
||||
org_admin: 'purple',
|
||||
org_member: 'info',
|
||||
event_manager: 'cyan',
|
||||
staff_coordinator: 'orange',
|
||||
volunteer_coordinator: 'success',
|
||||
}
|
||||
|
||||
const roleLabelMap: Record<string, string> = {
|
||||
org_admin: 'Organisatie Beheerder',
|
||||
org_member: 'Organisatie Lid',
|
||||
event_manager: 'Evenement Manager',
|
||||
staff_coordinator: 'Staf Coördinator',
|
||||
volunteer_coordinator: 'Vrijwilliger Coördinator',
|
||||
}
|
||||
|
||||
const memberSearch = ref('')
|
||||
|
||||
const memberHeaders = computed(() => {
|
||||
const headers: Array<{ title: string; key: string; sortable?: boolean; align?: 'start' | 'end' }> = [
|
||||
{ title: 'Naam', key: 'full_name', sortable: true },
|
||||
{ title: 'E-mailadres', key: 'email', sortable: true },
|
||||
{ title: 'Rol', key: 'role', sortable: true },
|
||||
]
|
||||
if (isOrgAdmin.value) {
|
||||
headers.push({ title: 'Acties', key: 'actions', sortable: false, align: 'end' })
|
||||
}
|
||||
return headers
|
||||
})
|
||||
|
||||
const memberSortBy = [{ key: 'full_name', order: 'asc' as const }]
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(p => p[0])
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
const date = d.toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
const time = d.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' })
|
||||
return `${date} om ${time} uur`
|
||||
}
|
||||
|
||||
function openEditRole(member: Member) {
|
||||
selectedMember.value = member
|
||||
isEditRoleDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openRemoveDialog(member: Member) {
|
||||
memberToRemove.value = member
|
||||
isRemoveDialogOpen.value = true
|
||||
}
|
||||
|
||||
function confirmRemoveMember() {
|
||||
if (!memberToRemove.value) return
|
||||
|
||||
removeMember(memberToRemove.value.id, {
|
||||
onSuccess: () => {
|
||||
isRemoveDialogOpen.value = false
|
||||
memberToRemove.value = null
|
||||
showRemoveSuccess.value = true
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openRevokeDialog(invitation: { id: string; email: string }) {
|
||||
invitationToRevoke.value = invitation
|
||||
isRevokeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openEmailChangeDialog(member: Member) {
|
||||
memberToChangeEmail.value = member
|
||||
newMemberEmail.value = ''
|
||||
adminEmailErrors.value = {}
|
||||
isEmailChangeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function confirmEmailChange() {
|
||||
if (!memberToChangeEmail.value) return
|
||||
adminEmailErrors.value = {}
|
||||
|
||||
adminChangeEmail(
|
||||
{ userId: memberToChangeEmail.value.id, newEmail: newMemberEmail.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
isEmailChangeDialogOpen.value = false
|
||||
memberToChangeEmail.value = null
|
||||
newMemberEmail.value = ''
|
||||
showEmailChangeSuccess.value = true
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const ax = err as { response?: { data?: { errors?: Record<string, string[]> } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
for (const [key, messages] of Object.entries(ax.response.data.errors)) {
|
||||
adminEmailErrors.value[key] = messages[0]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function confirmRevokeInvitation() {
|
||||
if (!invitationToRevoke.value) return
|
||||
|
||||
revokeInvitation(invitationToRevoke.value.id, {
|
||||
onSuccess: () => {
|
||||
isRevokeDialogOpen.value = false
|
||||
invitationToRevoke.value = null
|
||||
showRevokeSuccess.value = true
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="isLoading"
|
||||
type="card"
|
||||
/>
|
||||
|
||||
<!-- Error -->
|
||||
<VAlert
|
||||
v-else-if="isError"
|
||||
type="error"
|
||||
class="mb-4"
|
||||
>
|
||||
Kon leden niet laden.
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="refetch()"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<template v-else>
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-h4">
|
||||
Leden
|
||||
</h4>
|
||||
<p class="text-body-1 text-disabled mb-0">
|
||||
{{ orgName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Members table -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="memberSearch"
|
||||
placeholder="Zoek op naam of e-mail..."
|
||||
prepend-inner-icon="tabler-search"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="isOrgAdmin"
|
||||
cols="12"
|
||||
md="6"
|
||||
class="d-flex justify-end align-center"
|
||||
>
|
||||
<VBtn
|
||||
prepend-icon="tabler-user-plus"
|
||||
@click="isInviteDialogOpen = true"
|
||||
>
|
||||
Lid uitnodigen
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDataTable
|
||||
:headers="memberHeaders"
|
||||
:items="members"
|
||||
:search="memberSearch"
|
||||
:sort-by="memberSortBy"
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template #item.full_name="{ item }">
|
||||
<div class="d-flex align-center gap-x-3">
|
||||
<VAvatar
|
||||
v-if="item.avatar"
|
||||
size="34"
|
||||
:image="item.avatar"
|
||||
/>
|
||||
<VAvatar
|
||||
v-else
|
||||
size="34"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<span class="text-sm">{{ getInitials(item.full_name) }}</span>
|
||||
</VAvatar>
|
||||
<span>{{ item.full_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.role="{ item }">
|
||||
<VChip
|
||||
:color="roleColorMap[item.role] ?? 'default'"
|
||||
size="small"
|
||||
>
|
||||
{{ roleLabelMap[item.role] ?? item.role }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-x-1 justify-end">
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="openEditRole(item)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-mail-forward"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="openEmailChangeDialog(item)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="openRemoveDialog(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center pa-4 text-disabled">
|
||||
<VIcon
|
||||
icon="tabler-users-minus"
|
||||
size="48"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p>Geen leden gevonden</p>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- Pending invitations -->
|
||||
<VCard
|
||||
v-if="isOrgAdmin && pendingInvitations.length > 0"
|
||||
>
|
||||
<VCardTitle>Openstaande uitnodigingen</VCardTitle>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="invitation in pendingInvitations"
|
||||
:key="invitation.id"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
size="34"
|
||||
>
|
||||
<VIcon icon="tabler-mail" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>
|
||||
{{ invitation.email }}
|
||||
<VChip
|
||||
:color="roleColorMap[invitation.role] ?? 'default'"
|
||||
size="x-small"
|
||||
class="ms-2"
|
||||
>
|
||||
{{ roleLabelMap[invitation.role] ?? invitation.role }}
|
||||
</VChip>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
Verloopt op {{ formatDate(invitation.expires_at) }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="openRevokeDialog(invitation)"
|
||||
>
|
||||
Intrekken
|
||||
</VBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<InviteMemberDialog
|
||||
v-if="isOrgAdmin"
|
||||
v-model="isInviteDialogOpen"
|
||||
:org-id="orgId"
|
||||
/>
|
||||
|
||||
<EditMemberRoleDialog
|
||||
v-if="selectedMember"
|
||||
v-model="isEditRoleDialogOpen"
|
||||
:org-id="orgId"
|
||||
:member="selectedMember"
|
||||
/>
|
||||
|
||||
<!-- Remove member confirmation -->
|
||||
<VDialog
|
||||
v-model="isRemoveDialogOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<VCard title="Lid verwijderen">
|
||||
<VCardText>
|
||||
Weet je zeker dat je <strong>{{ memberToRemove?.full_name }}</strong> wilt verwijderen uit de organisatie?
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isRemoveDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
:loading="isRemoving"
|
||||
@click="confirmRemoveMember"
|
||||
>
|
||||
Verwijderen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Revoke invitation confirmation -->
|
||||
<VDialog
|
||||
v-model="isRevokeDialogOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<VCard title="Uitnodiging intrekken">
|
||||
<VCardText>
|
||||
Weet je zeker dat je de uitnodiging voor <strong>{{ invitationToRevoke?.email }}</strong> wilt intrekken?
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isRevokeDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
:loading="isRevoking"
|
||||
@click="confirmRevokeInvitation"
|
||||
>
|
||||
Intrekken
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Change email dialog -->
|
||||
<VDialog
|
||||
v-model="isEmailChangeDialogOpen"
|
||||
max-width="420"
|
||||
>
|
||||
<VCard title="E-mailadres wijzigen">
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Wijzig het e-mailadres van <strong>{{ memberToChangeEmail?.full_name }}</strong>.
|
||||
Er wordt een verificatiemail verstuurd naar het nieuwe adres.
|
||||
</p>
|
||||
<AppTextField
|
||||
v-model="newMemberEmail"
|
||||
label="Nieuw e-mailadres"
|
||||
type="email"
|
||||
:error-messages="adminEmailErrors.new_email"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="isEmailChangeDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="isChangingMemberEmail"
|
||||
:disabled="!newMemberEmail"
|
||||
@click="confirmEmailChange"
|
||||
>
|
||||
Verificatiemail versturen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Success snackbars -->
|
||||
<VSnackbar
|
||||
v-model="showRemoveSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Lid verwijderd
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showRevokeSuccess"
|
||||
color="success"
|
||||
:timeout="3000"
|
||||
>
|
||||
Uitnodiging ingetrokken
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showEmailChangeSuccess"
|
||||
color="success"
|
||||
:timeout="4000"
|
||||
>
|
||||
Verificatiemail verstuurd
|
||||
</VSnackbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useOrganisationStore } from '@/stores/useOrganisationStore'
|
||||
import RegistrationFieldTemplatesTab from '@/components/organisation/RegistrationFieldTemplatesTab.vue'
|
||||
import EmailBrandingTab from '@/components/organisation/EmailBrandingTab.vue'
|
||||
import EmailTemplatesTab from '@/components/organisation/EmailTemplatesTab.vue'
|
||||
import EmailLogTab from '@/components/organisation/EmailLogTab.vue'
|
||||
import SettingsGeneral from '@/components/organisation/settings/SettingsGeneral.vue'
|
||||
import SettingsMembers from '@/components/organisation/settings/SettingsMembers.vue'
|
||||
import SettingsCrowdTypes from '@/components/organisation/settings/SettingsCrowdTypes.vue'
|
||||
import SettingsTags from '@/components/organisation/settings/SettingsTags.vue'
|
||||
import SettingsRegistrationFields from '@/components/organisation/settings/SettingsRegistrationFields.vue'
|
||||
import SettingsEmailBranding from '@/components/organisation/settings/SettingsEmailBranding.vue'
|
||||
import SettingsEmailTemplates from '@/components/organisation/settings/SettingsEmailTemplates.vue'
|
||||
import SettingsEmailLog from '@/components/organisation/settings/SettingsEmailLog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -11,67 +15,128 @@ const orgStore = useOrganisationStore()
|
||||
|
||||
const orgId = computed(() => orgStore.activeOrganisationId ?? '')
|
||||
|
||||
const tabs = [
|
||||
{ value: 'templates', label: 'Registratieveld-templates', icon: 'tabler-forms' },
|
||||
{ value: 'email-branding', label: 'E-mail opmaak', icon: 'tabler-mail-cog' },
|
||||
{ value: 'email-templates', label: 'E-mail templates', icon: 'tabler-template' },
|
||||
{ value: 'email-log', label: 'E-mail log', icon: 'tabler-mail-search' },
|
||||
interface SettingsItem {
|
||||
icon: string
|
||||
title: string
|
||||
key: string
|
||||
}
|
||||
|
||||
interface SettingsGroup {
|
||||
label: string
|
||||
items: SettingsItem[]
|
||||
}
|
||||
|
||||
const settingsGroups: SettingsGroup[] = [
|
||||
{
|
||||
label: 'Organisatie',
|
||||
items: [
|
||||
{ icon: 'tabler-building', title: 'Algemeen', key: 'general' },
|
||||
{ icon: 'tabler-users-group', title: 'Leden', key: 'members' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Configuratie',
|
||||
items: [
|
||||
{ icon: 'tabler-category', title: 'Crowd Types', key: 'crowd-types' },
|
||||
{ icon: 'tabler-tags', title: 'Tags & Vaardigheden', key: 'tags' },
|
||||
{ icon: 'tabler-forms', title: 'Registratievelden', key: 'registration-fields' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'E-mail',
|
||||
items: [
|
||||
{ icon: 'tabler-palette', title: 'Opmaak & Branding', key: 'email-branding' },
|
||||
{ icon: 'tabler-template', title: 'Templates', key: 'email-templates' },
|
||||
{ icon: 'tabler-mail-search', title: 'Verzendlog', key: 'email-log' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const activeTab = computed({
|
||||
const allKeys = settingsGroups.flatMap(g => g.items.map(i => i.key))
|
||||
|
||||
const activeKey = computed({
|
||||
get: () => {
|
||||
const tab = route.query.tab as string
|
||||
return tabs.some(t => t.value === tab) ? tab : 'templates'
|
||||
return allKeys.includes(tab) ? tab : 'general'
|
||||
},
|
||||
set: (value: string) => {
|
||||
router.replace({ query: { ...route.query, tab: value } })
|
||||
set: (key: string) => {
|
||||
router.replace({ query: { tab: key } })
|
||||
},
|
||||
})
|
||||
|
||||
const componentMap: Record<string, Component> = {
|
||||
'general': SettingsGeneral,
|
||||
'members': SettingsMembers,
|
||||
'crowd-types': SettingsCrowdTypes,
|
||||
'tags': SettingsTags,
|
||||
'registration-fields': SettingsRegistrationFields,
|
||||
'email-branding': SettingsEmailBranding,
|
||||
'email-templates': SettingsEmailTemplates,
|
||||
'email-log': SettingsEmailLog,
|
||||
}
|
||||
|
||||
const activeComponent = computed(() => componentMap[activeKey.value] ?? SettingsGeneral)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex align-center mb-6">
|
||||
<div>
|
||||
<h4 class="text-h4">
|
||||
Instellingen
|
||||
</h4>
|
||||
<p class="text-body-1 text-disabled mb-0">
|
||||
Organisatie-instellingen en configuratie
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="text-h4 mb-1">
|
||||
Instellingen
|
||||
</h4>
|
||||
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||
Beheer de configuratie van je organisatie
|
||||
</p>
|
||||
|
||||
<VTabs
|
||||
v-model="activeTab"
|
||||
class="mb-6"
|
||||
>
|
||||
<VTab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:value="tab.value"
|
||||
:prepend-icon="tab.icon"
|
||||
<VRow>
|
||||
<!-- Sidebar -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VCard>
|
||||
<VList
|
||||
density="compact"
|
||||
nav
|
||||
class="py-0"
|
||||
>
|
||||
<template
|
||||
v-for="(group, gi) in settingsGroups"
|
||||
:key="gi"
|
||||
>
|
||||
<div
|
||||
class="text-caption text-uppercase text-medium-emphasis font-weight-medium px-4 mt-4 mb-1"
|
||||
:class="{ 'mt-2': gi === 0 }"
|
||||
>
|
||||
{{ group.label }}
|
||||
</div>
|
||||
<VListItem
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.title"
|
||||
:active="activeKey === item.key"
|
||||
:value="item.key"
|
||||
rounded="pill"
|
||||
@click="activeKey = item.key"
|
||||
/>
|
||||
</template>
|
||||
<div class="mb-2" />
|
||||
</VList>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VWindow
|
||||
v-model="activeTab"
|
||||
class="disable-tab-transition"
|
||||
>
|
||||
<VWindowItem value="templates">
|
||||
<RegistrationFieldTemplatesTab :org-id="orgId" />
|
||||
</VWindowItem>
|
||||
<VWindowItem value="email-branding">
|
||||
<EmailBrandingTab :org-id="orgId" />
|
||||
</VWindowItem>
|
||||
<VWindowItem value="email-templates">
|
||||
<EmailTemplatesTab :org-id="orgId" />
|
||||
</VWindowItem>
|
||||
<VWindowItem value="email-log">
|
||||
<EmailLogTab :org-id="orgId" />
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
<!-- Content -->
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
lg="9"
|
||||
>
|
||||
<component
|
||||
:is="activeComponent"
|
||||
:org-id="orgId"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useOrganisationStore } from '@/stores/useOrganisationStore'
|
||||
import PersonTagsTab from '@/components/organisation/PersonTagsTab.vue'
|
||||
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
const orgId = computed(() => orgStore.activeOrganisationId ?? '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PersonTagsTab :org-id="orgId" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,6 +10,7 @@ export interface Organisation {
|
||||
email_sender_name: string | null
|
||||
email_footer_text: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface OrganisationMember {
|
||||
|
||||
3
apps/app/typed-router.d.ts
vendored
3
apps/app/typed-router.d.ts
vendored
@@ -38,10 +38,7 @@ declare module 'vue-router/auto-routes' {
|
||||
'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
|
||||
'organisation': RouteRecordInfo<'organisation', '/organisation', Record<never, never>, Record<never, never>>,
|
||||
'organisation-companies': RouteRecordInfo<'organisation-companies', '/organisation/companies', Record<never, never>, Record<never, never>>,
|
||||
'organisation-crowd-types': RouteRecordInfo<'organisation-crowd-types', '/organisation/crowd-types', Record<never, never>, Record<never, never>>,
|
||||
'organisation-members': RouteRecordInfo<'organisation-members', '/organisation/members', Record<never, never>, Record<never, never>>,
|
||||
'organisation-settings': RouteRecordInfo<'organisation-settings', '/organisation/settings', Record<never, never>, Record<never, never>>,
|
||||
'organisation-tags': RouteRecordInfo<'organisation-tags', '/organisation/tags', Record<never, never>, Record<never, never>>,
|
||||
'platform': RouteRecordInfo<'platform', '/platform', Record<never, never>, Record<never, never>>,
|
||||
'platform-activity-log': RouteRecordInfo<'platform-activity-log', '/platform/activity-log', Record<never, never>, Record<never, never>>,
|
||||
'platform-organisations': RouteRecordInfo<'platform-organisations', '/platform/organisations', Record<never, never>, Record<never, never>>,
|
||||
|
||||
Reference in New Issue
Block a user