feat: festival/series model with sub-events, cross-event sections, tab navigation, SectionsShiftsPanel extraction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 11:15:19 +02:00
parent 11b9f1d399
commit 10bd55b8ae
40 changed files with 3087 additions and 1080 deletions

View File

@@ -1,16 +1,11 @@
<script setup lang="ts">
import { useEventDetail } from '@/composables/api/useEvents'
import { useEventDetail, useEventChildren } from '@/composables/api/useEvents'
import { dutchPlural } from '@/lib/dutch-plural'
import { useAuthStore } from '@/stores/useAuthStore'
import { useOrganisationStore } from '@/stores/useOrganisationStore'
import EditEventDialog from '@/components/events/EditEventDialog.vue'
import type { EventStatus } from '@/types/event'
const props = withDefaults(defineProps<{
hideTabs?: boolean
}>(), {
hideTabs: false,
})
const route = useRoute()
const authStore = useAuthStore()
const orgStore = useOrganisationStore()
@@ -20,6 +15,9 @@ const eventId = computed(() => String((route.params as { id: string }).id))
const { data: event, isLoading, isError, refetch } = useEventDetail(orgId, eventId)
// Children count for programmaonderdelen badge — only for festivals
const { data: children } = useEventChildren(orgId, eventId)
// Set active event in store
watch(eventId, (id) => {
if (id) orgStore.setActiveEvent(id)
@@ -52,7 +50,7 @@ function formatDate(iso: string) {
return dateFormatter.format(new Date(iso))
}
const tabs = [
const baseTabs = [
{ label: 'Overzicht', icon: 'tabler-layout-dashboard', route: 'events-id' },
{ label: 'Personen', icon: 'tabler-users', route: 'events-id-persons' },
{ label: 'Secties & Shifts', icon: 'tabler-layout-grid', route: 'events-id-sections' },
@@ -61,9 +59,38 @@ const tabs = [
{ label: 'Instellingen', icon: 'tabler-settings', route: 'events-id-settings' },
]
const programmaonderdelenLabel = computed(() => {
const label = event.value?.sub_event_label
? dutchPlural(event.value.sub_event_label)
: 'Programmaonderdelen'
const count = children.value?.length ?? event.value?.children_count ?? 0
return `${label} (${count})`
})
const tabs = computed(() => {
if (!event.value?.is_festival) return baseTabs
// Festival tab order: Overzicht | Programmaonderdelen | Secties & Shifts | Personen | Artiesten | Briefings | Instellingen
const festivalTab = {
label: programmaonderdelenLabel.value,
icon: 'tabler-calendar-event',
route: 'events-id-programmaonderdelen',
}
return [
baseTabs[0], // Overzicht
festivalTab,
baseTabs[2], // Secties & Shifts
baseTabs[1], // Personen
baseTabs[3], // Artiesten
baseTabs[4], // Briefings
baseTabs[5], // Instellingen
]
})
const activeTab = computed(() => {
const name = route.name as string
return tabs.find(t => name === t.route || name?.startsWith(`${t.route}-`))?.route ?? 'events-id'
return tabs.value.find(t => name === t.route || name?.startsWith(`${t.route}-`))?.route ?? 'events-id'
})
const backRoute = computed(() => {
@@ -100,22 +127,6 @@ const backRoute = computed(() => {
</VAlert>
<template v-else-if="event">
<!-- Sub-event breadcrumb -->
<div
v-if="event.is_sub_event && event.parent && event.parent_event_id"
class="text-caption text-medium-emphasis mb-1"
>
<VIcon size="12">
tabler-arrow-left
</VIcon>
<RouterLink
:to="{ name: 'events-id', params: { id: event.parent_event_id } }"
class="text-medium-emphasis"
>
{{ event.parent.name }}
</RouterLink>
</div>
<!-- Header -->
<div class="d-flex justify-space-between align-center mb-6">
<div class="d-flex align-center gap-x-3">
@@ -125,6 +136,15 @@ const backRoute = computed(() => {
:to="backRoute"
/>
<h4 class="text-h4">
<template v-if="event.is_sub_event && event.parent && event.parent_event_id">
<RouterLink
:to="{ name: 'events-id', params: { id: event.parent_event_id } }"
class="text-medium-emphasis text-decoration-none"
>
{{ event.parent.name }}
</RouterLink>
<span class="text-medium-emphasis mx-1">&raquo;</span>
</template>
{{ event.name }}
</h4>
<VChip
@@ -153,9 +173,8 @@ const backRoute = computed(() => {
</VBtn>
</div>
<!-- Horizontal tabs (hidden for festival containers) -->
<!-- Horizontal tabs -->
<VTabs
v-if="!hideTabs"
:model-value="activeTab"
class="mb-6"
>