refactor: identical VDataTable for members on both organisation pages
Both org pages now use the same VDataTable with: - Search field (name/email filter) - Sortable columns (Naam, E-mail, Rol) with default sort on name - Pagination (10 per page) - Avatar with initials, role chips with color mapping - Consistent empty state with icon Platform page: replaced VTable with VDataTable, added role chips (replacing inline AppSelect), role editing via menu on edit button. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,18 +95,22 @@ const roleLabelMap: Record<string, string> = {
|
|||||||
volunteer_coordinator: 'Vrijwilliger Coördinator',
|
volunteer_coordinator: 'Vrijwilliger Coördinator',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const memberSearch = ref('')
|
||||||
|
|
||||||
const memberHeaders = computed(() => {
|
const memberHeaders = computed(() => {
|
||||||
const headers: Array<{ title: string; key: string; sortable?: boolean }> = [
|
const headers: Array<{ title: string; key: string; sortable?: boolean; align?: 'start' | 'end' }> = [
|
||||||
{ title: 'Naam', key: 'full_name' },
|
{ title: 'Naam', key: 'full_name', sortable: true },
|
||||||
{ title: 'E-mailadres', key: 'email' },
|
{ title: 'E-mailadres', key: 'email', sortable: true },
|
||||||
{ title: 'Rol', key: 'role' },
|
{ title: 'Rol', key: 'role', sortable: true },
|
||||||
]
|
]
|
||||||
if (isOrgAdmin.value) {
|
if (isOrgAdmin.value) {
|
||||||
headers.push({ title: 'Acties', key: 'actions', sortable: false })
|
headers.push({ title: 'Acties', key: 'actions', sortable: false, align: 'end' })
|
||||||
}
|
}
|
||||||
return headers
|
return headers
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const memberSortBy = [{ key: 'full_name', order: 'asc' as const }]
|
||||||
|
|
||||||
function getInitials(name: string): string {
|
function getInitials(name: string): string {
|
||||||
return name
|
return name
|
||||||
.split(' ')
|
.split(' ')
|
||||||
@@ -328,32 +332,43 @@ function confirmRevokeInvitation() {
|
|||||||
</VAlert>
|
</VAlert>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Invite button -->
|
|
||||||
<div
|
|
||||||
v-if="isOrgAdmin"
|
|
||||||
class="d-flex justify-end mb-4"
|
|
||||||
>
|
|
||||||
<VBtn
|
|
||||||
prepend-icon="tabler-user-plus"
|
|
||||||
@click="isInviteDialogOpen = true"
|
|
||||||
>
|
|
||||||
Lid uitnodigen
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Members table -->
|
|
||||||
<VCard class="mb-6">
|
<VCard class="mb-6">
|
||||||
<VCardText v-if="members.length === 0">
|
<!-- Search + Invite -->
|
||||||
<p class="text-body-1 text-disabled mb-0">
|
<VCardText>
|
||||||
Nog geen leden
|
<VRow>
|
||||||
</p>
|
<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"
|
||||||
|
>
|
||||||
|
<VBtn
|
||||||
|
prepend-icon="tabler-user-plus"
|
||||||
|
@click="isInviteDialogOpen = true"
|
||||||
|
>
|
||||||
|
Lid uitnodigen
|
||||||
|
</VBtn>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<VDataTable
|
<VDataTable
|
||||||
v-else
|
|
||||||
:headers="memberHeaders"
|
:headers="memberHeaders"
|
||||||
:items="members"
|
:items="members"
|
||||||
:items-per-page="-1"
|
:search="memberSearch"
|
||||||
hide-default-footer
|
:sort-by="memberSortBy"
|
||||||
|
:items-per-page="10"
|
||||||
>
|
>
|
||||||
<template #item.full_name="{ item }">
|
<template #item.full_name="{ item }">
|
||||||
<div class="d-flex align-center gap-x-3">
|
<div class="d-flex align-center gap-x-3">
|
||||||
@@ -384,7 +399,7 @@ function confirmRevokeInvitation() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #item.actions="{ item }">
|
<template #item.actions="{ item }">
|
||||||
<div class="d-flex gap-x-1">
|
<div class="d-flex gap-x-1 justify-end">
|
||||||
<VBtn
|
<VBtn
|
||||||
icon="tabler-edit"
|
icon="tabler-edit"
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -406,6 +421,17 @@ function confirmRevokeInvitation() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
</VDataTable>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,46 @@ const roleOptions = [
|
|||||||
{ title: 'Lid', value: 'org_member' },
|
{ title: 'Lid', value: 'org_member' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 },
|
||||||
|
{ 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()
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Tabs ──────────────────────────────────────────────────
|
// ─── Tabs ──────────────────────────────────────────────────
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ value: 'algemeen', label: 'Algemeen', icon: 'tabler-building' },
|
{ value: 'algemeen', label: 'Algemeen', icon: 'tabler-building' },
|
||||||
@@ -424,94 +464,131 @@ function formatDate(iso: string): string {
|
|||||||
|
|
||||||
<!-- Leden tab -->
|
<!-- Leden tab -->
|
||||||
<VWindowItem value="leden">
|
<VWindowItem value="leden">
|
||||||
<div class="d-flex justify-end mb-4">
|
|
||||||
<VBtn
|
|
||||||
prepend-icon="tabler-user-plus"
|
|
||||||
@click="openInviteDialog"
|
|
||||||
>
|
|
||||||
Lid uitnodigen
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VCard>
|
<VCard>
|
||||||
<VTable v-if="members.length > 0">
|
<!-- Search + Invite -->
|
||||||
<thead>
|
<VCardText>
|
||||||
<tr>
|
<VRow>
|
||||||
<th>Naam</th>
|
<VCol
|
||||||
<th>Email</th>
|
cols="12"
|
||||||
<th>Rol</th>
|
md="6"
|
||||||
<th class="text-end">
|
|
||||||
Acties
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="member in members"
|
|
||||||
:key="member.id"
|
|
||||||
>
|
>
|
||||||
<td>
|
<AppTextField
|
||||||
<div class="d-flex align-center gap-x-3 py-2">
|
v-model="memberSearch"
|
||||||
<VAvatar
|
placeholder="Zoek op naam of e-mail..."
|
||||||
size="34"
|
prepend-inner-icon="tabler-search"
|
||||||
:color="member.avatar ? undefined : 'primary'"
|
clearable
|
||||||
variant="tonal"
|
/>
|
||||||
>
|
</VCol>
|
||||||
<VImg
|
<VCol
|
||||||
v-if="member.avatar"
|
cols="12"
|
||||||
:src="member.avatar"
|
md="6"
|
||||||
/>
|
class="d-flex justify-end"
|
||||||
<span v-else>{{ member.first_name.charAt(0) }}{{ member.last_name.charAt(0) }}</span>
|
>
|
||||||
</VAvatar>
|
<VBtn
|
||||||
<span>{{ member.full_name }}</span>
|
prepend-icon="tabler-user-plus"
|
||||||
</div>
|
@click="openInviteDialog"
|
||||||
</td>
|
>
|
||||||
<td>{{ member.email }}</td>
|
Lid uitnodigen
|
||||||
<td>
|
</VBtn>
|
||||||
<AppSelect
|
</VCol>
|
||||||
:model-value="member.role"
|
</VRow>
|
||||||
:items="roleOptions"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
style="max-inline-size: 160px;"
|
|
||||||
@update:model-value="(val: string) => onRoleChange(member.id, val)"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<VTooltip
|
|
||||||
v-if="member.id === authStore.user?.id"
|
|
||||||
location="top"
|
|
||||||
>
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<span v-bind="props">
|
|
||||||
<IconBtn
|
|
||||||
disabled
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<VIcon icon="tabler-trash" />
|
|
||||||
</IconBtn>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
Je kunt jezelf niet verwijderen
|
|
||||||
</VTooltip>
|
|
||||||
<IconBtn
|
|
||||||
v-else
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
@click="openRemoveDialog(member)"
|
|
||||||
>
|
|
||||||
<VIcon icon="tabler-trash" />
|
|
||||||
</IconBtn>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</VTable>
|
|
||||||
<VCardText
|
|
||||||
v-else
|
|
||||||
class="text-center text-disabled"
|
|
||||||
>
|
|
||||||
Geen leden gevonden.
|
|
||||||
</VCardText>
|
</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">
|
||||||
|
<VMenu location="bottom end">
|
||||||
|
<template #activator="{ props: menuProps }">
|
||||||
|
<VBtn
|
||||||
|
icon="tabler-edit"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
v-bind="menuProps"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<VList density="compact">
|
||||||
|
<VListItem
|
||||||
|
v-for="opt in roleOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:active="item.role === opt.value"
|
||||||
|
@click="onRoleChange(item.id, opt.value)"
|
||||||
|
>
|
||||||
|
<VListItemTitle>{{ opt.title }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
<VTooltip
|
||||||
|
v-if="item.id === authStore.user?.id"
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<span v-bind="props">
|
||||||
|
<VBtn
|
||||||
|
icon="tabler-trash"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
Je kunt jezelf niet verwijderen
|
||||||
|
</VTooltip>
|
||||||
|
<VBtn
|
||||||
|
v-else
|
||||||
|
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>
|
</VCard>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user