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:
307
apps/app/src/pages/events/[id]/crowd-lists/index.vue
Normal file
307
apps/app/src/pages/events/[id]/crowd-lists/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user