feat: festival/event model frontend + topbar activeren
- Events lijst: card grid met festival/serie chips - Festival detail: programmaonderdelen grid - CreateSubEventDialog voor sub-events binnen festival - EventTabsNav: breadcrumb terug naar festival - Sessie A: festival-bewuste EventResource + children endpoint - Topbar: zoekbalk, theme switcher, shortcuts, notificaties - Schema v1.7 + BACKLOG.md toegevoegd - 121 tests groen
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import EventTabsNav from '@/components/events/EventTabsNav.vue'
|
||||
import CreateSubEventDialog from '@/components/events/CreateSubEventDialog.vue'
|
||||
import { useEventDetail, useEventChildren } from '@/composables/api/useEvents'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import type { EventStatus, EventItem } from '@/types/event'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
@@ -9,9 +13,52 @@ definePage({
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const orgId = computed(() => authStore.currentOrganisation?.id ?? '')
|
||||
const eventId = computed(() => String((route.params as { id: string }).id))
|
||||
|
||||
const { data: event } = useEventDetail(orgId, eventId)
|
||||
|
||||
const isFestival = computed(() => event.value?.is_festival ?? false)
|
||||
const isFlatEvent = computed(() => event.value?.is_flat_event ?? false)
|
||||
|
||||
// Children query — only enabled for festivals
|
||||
const { data: children, isLoading: childrenLoading } = useEventChildren(orgId, eventId)
|
||||
|
||||
const isCreateSubEventOpen = ref(false)
|
||||
|
||||
const statusColor: Record<EventStatus, string> = {
|
||||
draft: 'default',
|
||||
published: 'info',
|
||||
registration_open: 'cyan',
|
||||
buildup: 'warning',
|
||||
showday: 'success',
|
||||
teardown: 'warning',
|
||||
closed: 'error',
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('nl-NL', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
function formatDate(iso: string) {
|
||||
return dateFormatter.format(new Date(iso))
|
||||
}
|
||||
|
||||
const subEventLabel = computed(() =>
|
||||
event.value?.sub_event_label ?? 'Programmaonderdeel',
|
||||
)
|
||||
|
||||
const subEventLabelPlural = computed(() =>
|
||||
event.value?.sub_event_label
|
||||
? `${event.value.sub_event_label}en`
|
||||
: 'Programmaonderdelen',
|
||||
)
|
||||
|
||||
// --- Flat event tiles (existing behaviour) ---
|
||||
const tiles = [
|
||||
{ title: 'Personen', value: 0, icon: 'tabler-users', color: 'success', route: 'events-id-persons', enabled: true },
|
||||
{ title: 'Secties & Shifts', value: 0, icon: 'tabler-layout-grid', color: 'primary', route: null, enabled: false },
|
||||
@@ -23,10 +70,163 @@ function onTileClick(tile: typeof tiles[number]) {
|
||||
if (tile.enabled && tile.route)
|
||||
router.push({ name: tile.route as 'events-id-persons', params: { id: eventId.value } })
|
||||
}
|
||||
|
||||
function navigateToChild(child: EventItem) {
|
||||
router.push({ name: 'events-id', params: { id: child.id } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EventTabsNav>
|
||||
<!-- Festival / Series view -->
|
||||
<template v-if="isFestival && event">
|
||||
<EventTabsNav :hide-tabs="true">
|
||||
<!-- Sub-events subtitle bar -->
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<div class="text-body-1 text-medium-emphasis">
|
||||
{{ children?.length ?? event.children_count ?? 0 }} {{ subEventLabelPlural.toLowerCase() }}
|
||||
</div>
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
size="small"
|
||||
@click="isCreateSubEventOpen = true"
|
||||
>
|
||||
{{ subEventLabel }} toevoegen
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- Children loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="childrenLoading"
|
||||
type="card@3"
|
||||
/>
|
||||
|
||||
<!-- Children grid -->
|
||||
<VRow v-else-if="children?.length">
|
||||
<VCol
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard
|
||||
class="cursor-pointer"
|
||||
hover
|
||||
@click="navigateToChild(child)"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-start mb-1">
|
||||
<h6 class="text-h6">
|
||||
{{ child.name }}
|
||||
</h6>
|
||||
<VChip
|
||||
:color="statusColor[child.status]"
|
||||
size="small"
|
||||
>
|
||||
{{ child.status }}
|
||||
</VChip>
|
||||
</div>
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
{{ formatDate(child.start_date) }}
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- Children empty -->
|
||||
<VCard
|
||||
v-else
|
||||
class="text-center pa-6"
|
||||
>
|
||||
<p class="text-body-1 text-disabled mb-0">
|
||||
Nog geen {{ subEventLabelPlural.toLowerCase() }}. Voeg je eerste {{ subEventLabel.toLowerCase() }} toe.
|
||||
</p>
|
||||
</VCard>
|
||||
|
||||
<!-- Bottom info cards -->
|
||||
<VRow class="mt-6">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center gap-x-3">
|
||||
<VAvatar
|
||||
color="success"
|
||||
variant="tonal"
|
||||
size="44"
|
||||
rounded
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-users"
|
||||
size="28"
|
||||
/>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<p class="text-body-1 mb-0">
|
||||
Vrijwilligers
|
||||
</p>
|
||||
<h4 class="text-h4">
|
||||
0
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:to="{ name: 'events-id-persons', params: { id: eventId } }"
|
||||
>
|
||||
Beheren
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<div class="d-flex align-center gap-x-3">
|
||||
<VAvatar
|
||||
color="info"
|
||||
variant="tonal"
|
||||
size="44"
|
||||
rounded
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-chart-bar"
|
||||
size="28"
|
||||
/>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<p class="text-body-1 mb-0">
|
||||
Capaciteitsoverzicht
|
||||
</p>
|
||||
<p class="text-body-2 text-disabled mb-0">
|
||||
Volledige capaciteitsplanning zichtbaar zodra {{ subEventLabelPlural.toLowerCase() }} zijn aangemaakt
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<CreateSubEventDialog
|
||||
v-if="event"
|
||||
v-model="isCreateSubEventOpen"
|
||||
:parent-event="event"
|
||||
:org-id="orgId"
|
||||
/>
|
||||
</EventTabsNav>
|
||||
</template>
|
||||
|
||||
<!-- Flat event / Sub-event view (existing behaviour) -->
|
||||
<EventTabsNav v-else>
|
||||
<VRow class="mb-6">
|
||||
<VCol
|
||||
v-for="tile in tiles"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useEventList } from '@/composables/api/useEvents'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import CreateEventDialog from '@/components/events/CreateEventDialog.vue'
|
||||
import type { EventStatus, EventType } from '@/types/event'
|
||||
import type { EventStatus, EventItem } from '@/types/event'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
@@ -13,13 +13,10 @@ const { data: events, isLoading, isError, refetch } = useEventList(orgId)
|
||||
|
||||
const isCreateDialogOpen = ref(false)
|
||||
|
||||
const headers = [
|
||||
{ title: 'Naam', key: 'name' },
|
||||
{ title: 'Status', key: 'status' },
|
||||
{ title: 'Startdatum', key: 'start_date' },
|
||||
{ title: 'Einddatum', key: 'end_date' },
|
||||
{ title: 'Acties', key: 'actions', sortable: false, align: 'end' as const },
|
||||
]
|
||||
// Filter: only top-level events (no sub-events)
|
||||
const topLevelEvents = computed(() =>
|
||||
events.value?.filter(e => !e.is_sub_event) ?? [],
|
||||
)
|
||||
|
||||
const statusColor: Record<EventStatus, string> = {
|
||||
draft: 'default',
|
||||
@@ -31,6 +28,11 @@ const statusColor: Record<EventStatus, string> = {
|
||||
closed: 'error',
|
||||
}
|
||||
|
||||
const eventTypeColor: Record<string, string> = {
|
||||
festival: 'purple',
|
||||
series: 'info',
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('nl-NL', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
@@ -41,8 +43,8 @@ function formatDate(iso: string) {
|
||||
return dateFormatter.format(new Date(iso))
|
||||
}
|
||||
|
||||
function navigateToDetail(_event: Event, row: { item: EventType }) {
|
||||
router.push({ name: 'events-id', params: { id: row.item.id } })
|
||||
function navigateToEvent(event: EventItem) {
|
||||
router.push({ name: 'events-id', params: { id: event.id } })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -74,14 +76,14 @@ function navigateToDetail(_event: Event, row: { item: EventType }) {
|
||||
prepend-icon="tabler-plus"
|
||||
@click="isCreateDialogOpen = true"
|
||||
>
|
||||
Nieuw evenement
|
||||
Nieuw aanmaken
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="isLoading"
|
||||
type="table"
|
||||
type="card@4"
|
||||
/>
|
||||
|
||||
<!-- Error -->
|
||||
@@ -103,7 +105,7 @@ function navigateToDetail(_event: Event, row: { item: EventType }) {
|
||||
|
||||
<!-- Empty -->
|
||||
<VCard
|
||||
v-else-if="!events?.length"
|
||||
v-else-if="!topLevelEvents.length"
|
||||
class="text-center pa-8"
|
||||
>
|
||||
<VIcon
|
||||
@@ -112,47 +114,72 @@ function navigateToDetail(_event: Event, row: { item: EventType }) {
|
||||
class="mb-4 text-disabled"
|
||||
/>
|
||||
<p class="text-body-1 text-disabled">
|
||||
Nog geen evenementen
|
||||
Nog geen evenementen. Maak je eerste evenement aan.
|
||||
</p>
|
||||
</VCard>
|
||||
|
||||
<!-- Data table -->
|
||||
<VCard v-else>
|
||||
<VDataTable
|
||||
:headers="headers"
|
||||
:items="events"
|
||||
item-value="id"
|
||||
hover
|
||||
@click:row="navigateToDetail"
|
||||
<!-- Event cards grid -->
|
||||
<VRow v-else>
|
||||
<VCol
|
||||
v-for="event in topLevelEvents"
|
||||
:key="event.id"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="statusColor[item.status]"
|
||||
size="small"
|
||||
>
|
||||
{{ item.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
<VCard
|
||||
class="cursor-pointer"
|
||||
hover
|
||||
@click="navigateToEvent(event)"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-start mb-2">
|
||||
<h5 class="text-h5">
|
||||
{{ event.name }}
|
||||
</h5>
|
||||
<div class="d-flex gap-x-2">
|
||||
<VChip
|
||||
v-if="event.event_type === 'festival'"
|
||||
color="purple"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
Festival
|
||||
</VChip>
|
||||
<VChip
|
||||
v-else-if="event.event_type === 'series'"
|
||||
color="info"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
Serie
|
||||
</VChip>
|
||||
<VChip
|
||||
:color="statusColor[event.status]"
|
||||
size="small"
|
||||
>
|
||||
{{ event.status }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #item.start_date="{ item }">
|
||||
{{ formatDate(item.start_date) }}
|
||||
</template>
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
{{ formatDate(event.start_date) }} – {{ formatDate(event.end_date) }}
|
||||
</p>
|
||||
|
||||
<template #item.end_date="{ item }">
|
||||
{{ formatDate(item.end_date) }}
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<VBtn
|
||||
icon="tabler-eye"
|
||||
variant="text"
|
||||
size="small"
|
||||
:to="{ name: 'events-id', params: { id: item.id } }"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
<p
|
||||
v-if="event.children_count && event.children_count > 0"
|
||||
class="text-body-2 text-medium-emphasis d-flex align-center gap-x-1 mt-1 mb-0"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-layout-grid"
|
||||
size="16"
|
||||
/>
|
||||
{{ event.children_count }} {{ event.sub_event_label?.toLowerCase() ?? 'programmaonderdelen' }}
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<CreateEventDialog
|
||||
v-model="isCreateDialogOpen"
|
||||
|
||||
Reference in New Issue
Block a user