feat: crowd lists frontend with list view, create/edit dialog and person management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:14:17 +02:00
parent 9b7aa92e84
commit 331f662c67
7 changed files with 1203 additions and 5 deletions

View File

@@ -0,0 +1,307 @@
<script setup lang="ts">
import { useCrowdLists, useDeleteCrowdList } from '@/composables/api/useCrowdLists'
import { useCrowdTypeList } from '@/composables/api/useCrowdTypes'
import { useCompanies } from '@/composables/api/useCompanies'
import { useAuthStore } from '@/stores/useAuthStore'
import EventTabsNav from '@/components/events/EventTabsNav.vue'
import CrowdListFormDialog from '@/components/crowd-lists/CrowdListFormDialog.vue'
import CrowdListDetailPanel from '@/components/crowd-lists/CrowdListDetailPanel.vue'
import { CrowdListType } from '@/types/crowdList'
import type { CrowdList } from '@/types/crowdList'
definePage({
meta: {
navActiveLink: 'events',
},
})
const route = useRoute()
const authStore = useAuthStore()
const orgId = computed(() => authStore.currentOrganisation?.id ?? '')
const eventId = computed(() => String((route.params as { id: string }).id))
const { data: crowdLists, isLoading, isError, refetch } = useCrowdLists(eventId)
const { data: crowdTypes } = useCrowdTypeList(orgId)
const { data: companies } = useCompanies(orgId)
const { mutate: deleteCrowdList, isPending: isDeleting } = useDeleteCrowdList(eventId)
// Lookup maps for resolving IDs to names
const crowdTypeMap = computed(() => {
const map = new Map<string, string>()
crowdTypes.value?.forEach(ct => map.set(ct.id, ct.name))
return map
})
const companyMap = computed(() => {
const map = new Map<string, string>()
companies.value?.forEach(c => map.set(c.id, c.name))
return map
})
// Table headers
const headers = [
{ title: 'Naam', key: 'name' },
{ title: 'Type', key: 'type', width: 100 },
{ title: 'Crowd Type', key: 'crowd_type_id' },
{ title: 'Personen', key: 'persons_count', width: 120 },
{ title: 'Auto-approve', key: 'auto_approve', width: 130 },
{ title: 'Bedrijf', key: 'recipient_company_id' },
{ title: 'Acties', key: 'actions', sortable: false, align: 'end' as const, width: 100 },
]
// Dialogs
const isFormDialogOpen = ref(false)
const editingCrowdList = ref<CrowdList | null>(null)
const isDetailPanelOpen = ref(false)
const selectedCrowdList = ref<CrowdList | null>(null)
// Delete confirmation
const isDeleteDialogOpen = ref(false)
const deletingCrowdList = ref<CrowdList | null>(null)
const showSuccess = ref(false)
const successMessage = ref('')
function onCreateNew() {
editingCrowdList.value = null
isFormDialogOpen.value = true
}
function onRowClick(_event: Event, row: { item: CrowdList }) {
selectedCrowdList.value = row.item
isDetailPanelOpen.value = true
}
function onEdit(crowdList: CrowdList) {
editingCrowdList.value = crowdList
isFormDialogOpen.value = true
}
function onDeleteConfirm(crowdList: CrowdList) {
deletingCrowdList.value = crowdList
isDeleteDialogOpen.value = true
}
function onDeleteExecute() {
if (!deletingCrowdList.value) return
const name = deletingCrowdList.value.name
deleteCrowdList(deletingCrowdList.value.id, {
onSuccess: () => {
isDeleteDialogOpen.value = false
deletingCrowdList.value = null
successMessage.value = `${name} verwijderd`
showSuccess.value = true
},
})
}
function onEditFromPanel(crowdList: CrowdList) {
isDetailPanelOpen.value = false
onEdit(crowdList)
}
function formatPersonsCount(item: CrowdList): string {
if (item.max_persons) {
return `${item.persons_count} / ${item.max_persons}`
}
return String(item.persons_count)
}
</script>
<template>
<EventTabsNav>
<!-- Action bar -->
<div class="d-flex justify-space-between align-center mb-4">
<h5 class="text-h5">
Publiekslijsten
</h5>
<VBtn
prepend-icon="tabler-plus"
@click="onCreateNew"
>
Nieuwe lijst
</VBtn>
</div>
<!-- Loading -->
<VSkeletonLoader
v-if="isLoading"
type="table"
/>
<!-- Error -->
<VAlert
v-else-if="isError"
type="error"
class="mb-4"
>
Kon publiekslijsten niet laden.
<template #append>
<VBtn
variant="text"
@click="refetch()"
>
Opnieuw proberen
</VBtn>
</template>
</VAlert>
<!-- Empty -->
<VCard
v-else-if="!crowdLists?.length"
class="text-center pa-8"
>
<VIcon
icon="tabler-list"
size="48"
class="mb-4 text-disabled"
/>
<p class="text-body-1 text-disabled mb-4">
Nog geen publiekslijsten voor dit evenement.
Maak je eerste lijst aan om deelnemers te organiseren.
</p>
<VBtn
prepend-icon="tabler-plus"
@click="onCreateNew"
>
Eerste lijst aanmaken
</VBtn>
</VCard>
<!-- Data table -->
<VCard v-else>
<VDataTable
:headers="headers"
:items="crowdLists"
item-value="id"
hover
@click:row="onRowClick"
>
<template #item.name="{ item }">
<div class="d-flex align-center gap-x-2">
<span>{{ item.name }}</span>
<VChip
v-if="item.is_full"
color="error"
size="x-small"
>
Vol
</VChip>
</div>
</template>
<template #item.type="{ item }">
<VChip
:color="item.type === CrowdListType.INTERNAL ? 'primary' : 'info'"
size="small"
>
{{ item.type === CrowdListType.INTERNAL ? 'Intern' : 'Extern' }}
</VChip>
</template>
<template #item.crowd_type_id="{ item }">
{{ crowdTypeMap.get(item.crowd_type_id) ?? '-' }}
</template>
<template #item.persons_count="{ item }">
{{ formatPersonsCount(item) }}
</template>
<template #item.auto_approve="{ item }">
<VIcon
:icon="item.auto_approve ? 'tabler-circle-check' : 'tabler-circle-x'"
:color="item.auto_approve ? 'success' : 'default'"
size="20"
/>
</template>
<template #item.recipient_company_id="{ item }">
<template v-if="item.recipient_company_id">
{{ companyMap.get(item.recipient_company_id) ?? '-' }}
</template>
<span
v-else
class="text-disabled"
>-</span>
</template>
<template #item.actions="{ item }">
<div class="d-flex justify-end gap-x-1">
<VBtn
icon="tabler-edit"
variant="text"
size="small"
title="Bewerken"
@click.stop="onEdit(item)"
/>
<VBtn
icon="tabler-trash"
variant="text"
size="small"
color="error"
title="Verwijderen"
@click.stop="onDeleteConfirm(item)"
/>
</div>
</template>
</VDataTable>
</VCard>
<!-- Create / Edit dialog -->
<CrowdListFormDialog
v-model="isFormDialogOpen"
:event-id="eventId"
:org-id="orgId"
:crowd-list="editingCrowdList"
/>
<!-- Detail panel -->
<CrowdListDetailPanel
v-model="isDetailPanelOpen"
:event-id="eventId"
:org-id="orgId"
:crowd-list="selectedCrowdList"
@edit="onEditFromPanel"
/>
<!-- Delete confirmation -->
<VDialog
v-model="isDeleteDialogOpen"
max-width="400"
>
<VCard title="Publiekslijst verwijderen">
<VCardText>
Weet je zeker dat je <strong>{{ deletingCrowdList?.name }}</strong> wilt verwijderen?
Alle personen worden van de lijst verwijderd.
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="text"
@click="isDeleteDialogOpen = false"
>
Annuleren
</VBtn>
<VBtn
color="error"
:loading="isDeleting"
@click="onDeleteExecute"
>
Verwijderen
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- Success snackbar -->
<VSnackbar
v-model="showSuccess"
color="success"
:timeout="3000"
>
{{ successMessage }}
</VSnackbar>
</EventTabsNav>
</template>