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',
|
||||
}
|
||||
|
||||
const memberSearch = ref('')
|
||||
|
||||
const memberHeaders = computed(() => {
|
||||
const headers: Array<{ title: string; key: string; sortable?: boolean }> = [
|
||||
{ title: 'Naam', key: 'full_name' },
|
||||
{ title: 'E-mailadres', key: 'email' },
|
||||
{ title: 'Rol', key: 'role' },
|
||||
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 })
|
||||
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(' ')
|
||||
@@ -328,32 +332,43 @@ function confirmRevokeInvitation() {
|
||||
</VAlert>
|
||||
|
||||
<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">
|
||||
<VCardText v-if="members.length === 0">
|
||||
<p class="text-body-1 text-disabled mb-0">
|
||||
Nog geen leden
|
||||
</p>
|
||||
<!-- Search + Invite -->
|
||||
<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"
|
||||
>
|
||||
<VBtn
|
||||
prepend-icon="tabler-user-plus"
|
||||
@click="isInviteDialogOpen = true"
|
||||
>
|
||||
Lid uitnodigen
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VDataTable
|
||||
v-else
|
||||
:headers="memberHeaders"
|
||||
:items="members"
|
||||
:items-per-page="-1"
|
||||
hide-default-footer
|
||||
:search="memberSearch"
|
||||
:sort-by="memberSortBy"
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template #item.full_name="{ item }">
|
||||
<div class="d-flex align-center gap-x-3">
|
||||
@@ -384,7 +399,7 @@ function confirmRevokeInvitation() {
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-x-1">
|
||||
<div class="d-flex gap-x-1 justify-end">
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
@@ -406,6 +421,17 @@ function confirmRevokeInvitation() {
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -48,6 +48,46 @@ const roleOptions = [
|
||||
{ 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 ──────────────────────────────────────────────────
|
||||
const tabs = [
|
||||
{ value: 'algemeen', label: 'Algemeen', icon: 'tabler-building' },
|
||||
@@ -424,94 +464,131 @@ function formatDate(iso: string): string {
|
||||
|
||||
<!-- Leden tab -->
|
||||
<VWindowItem value="leden">
|
||||
<div class="d-flex justify-end mb-4">
|
||||
<VBtn
|
||||
prepend-icon="tabler-user-plus"
|
||||
@click="openInviteDialog"
|
||||
>
|
||||
Lid uitnodigen
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VCard>
|
||||
<VTable v-if="members.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Naam</th>
|
||||
<th>Email</th>
|
||||
<th>Rol</th>
|
||||
<th class="text-end">
|
||||
Acties
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
<!-- Search + Invite -->
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<td>
|
||||
<div class="d-flex align-center gap-x-3 py-2">
|
||||
<VAvatar
|
||||
size="34"
|
||||
:color="member.avatar ? undefined : 'primary'"
|
||||
variant="tonal"
|
||||
>
|
||||
<VImg
|
||||
v-if="member.avatar"
|
||||
:src="member.avatar"
|
||||
/>
|
||||
<span v-else>{{ member.first_name.charAt(0) }}{{ member.last_name.charAt(0) }}</span>
|
||||
</VAvatar>
|
||||
<span>{{ member.full_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ member.email }}</td>
|
||||
<td>
|
||||
<AppSelect
|
||||
:model-value="member.role"
|
||||
: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.
|
||||
<AppTextField
|
||||
v-model="memberSearch"
|
||||
placeholder="Zoek op naam of e-mail..."
|
||||
prepend-inner-icon="tabler-search"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
class="d-flex justify-end"
|
||||
>
|
||||
<VBtn
|
||||
prepend-icon="tabler-user-plus"
|
||||
@click="openInviteDialog"
|
||||
>
|
||||
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">
|
||||
<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>
|
||||
</VWindowItem>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user