feat: fix time slot hierarchy — seeder, API include_children, frontend dropdown, navigation
Restructure the festival hierarchy end-to-end: Seeder: Remove duplicate festival-level VOLUNTEER time slots, keep only CREW operational slots. Rename sub-events to "Dag 1/2/3 — ..." pattern. Change Nachtsecurity to Security (cross_event). EHBO/Security shifts now use sub-event time slots via cross_event exception. Add flat event "Braderie Dorpstown 2026". API: Add ?include_children=true to TimeSlotController for festivals, returning all sub-event time slots with source and event_name fields. Update StoreShiftRequest and UpdateShiftRequest to accept child time slots for cross_event sections. Frontend: Create useTimeSlotDropdown composable with 4-scenario dropdown logic. Replace AppSelect with VAutocomplete in CreateShiftDialog with grouped items, dimmed festival slots, and info tooltips. Add InfoTooltip reusable component. Show festival context labels on cross_event sections in sub-event section lists. Add read-only festival time slots on sub-event time-slots page. Add cross_event context banner with "Bekijk alle diensten" link. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ const eventId = computed(() => String((route.params as { id: string }).id))
|
||||
<SectionsShiftsPanel
|
||||
:event-id="eventId"
|
||||
:is-sub-event="event?.is_sub_event ?? false"
|
||||
:parent-event="event?.parent ?? null"
|
||||
/>
|
||||
</template>
|
||||
</EventTabsNav>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeSlotList, useDeleteTimeSlot } from '@/composables/api/useTimeSlots'
|
||||
import { useEventChildren } from '@/composables/api/useEvents'
|
||||
import { useEventChildren, useEventDetail } from '@/composables/api/useEvents'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import EventTabsNav from '@/components/events/EventTabsNav.vue'
|
||||
import CreateTimeSlotDialog from '@/components/sections/CreateTimeSlotDialog.vue'
|
||||
import InfoTooltip from '@/components/common/InfoTooltip.vue'
|
||||
import { PersonType } from '@/types/timeSlot'
|
||||
import type { TimeSlot, PersonType as PersonTypeValue } from '@/types/timeSlot'
|
||||
import type { EventItem } from '@/types/event'
|
||||
@@ -117,6 +118,29 @@ function getTimeSlotContext(ts: TimeSlot): { label: string; color: string } | nu
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Festival time slots (read-only, for sub-events) ---
|
||||
const { data: eventDetail } = useEventDetail(orgId, eventId)
|
||||
|
||||
const parentEventId = computed(() => eventDetail.value?.parent_event_id ?? '')
|
||||
const isSubEvent = computed(() => eventDetail.value?.is_sub_event ?? false)
|
||||
const parentEvent = computed(() => eventDetail.value?.parent ?? null)
|
||||
|
||||
const { data: festivalTimeSlots } = useTimeSlotList(orgId, parentEventId, {
|
||||
includeParent: computed(() => false),
|
||||
includeChildren: computed(() => false),
|
||||
})
|
||||
|
||||
const festivalTimeSlotsGrouped = computed(() => {
|
||||
if (!festivalTimeSlots.value?.length) return new Map<string, TimeSlot[]>()
|
||||
const groups = new Map<string, TimeSlot[]>()
|
||||
for (const slot of festivalTimeSlots.value) {
|
||||
const existing = groups.get(slot.date) || []
|
||||
existing.push(slot)
|
||||
groups.set(slot.date, existing)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
// --- Fill rate ---
|
||||
function fillRatePercent(ts: TimeSlot): number {
|
||||
if (ts.total_slots === 0) return 0
|
||||
@@ -411,6 +435,79 @@ function onDeleteExecute() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Festival time slots (read-only, sub-events only) -->
|
||||
<template v-if="isSubEvent && parentEvent && festivalTimeSlots?.length">
|
||||
<VDivider
|
||||
class="my-6"
|
||||
style="border-style: dashed;"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center gap-2 mb-3">
|
||||
<VIcon
|
||||
icon="tabler-lock"
|
||||
size="14"
|
||||
color="medium-emphasis"
|
||||
/>
|
||||
<span class="text-caption text-medium-emphasis">
|
||||
{{ parentEvent.name }} — alleen-lezen
|
||||
</span>
|
||||
<InfoTooltip>
|
||||
<p>
|
||||
Tijdsloten van <strong>{{ parentEvent.name }}</strong> gelden
|
||||
festival-breed en worden daar beheerd.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="[date, slots] in festivalTimeSlotsGrouped"
|
||||
:key="`festival-${date}`"
|
||||
class="mb-4 opacity-55"
|
||||
>
|
||||
<div class="d-flex align-center gap-x-2 mb-2">
|
||||
<h6 class="text-subtitle-1">
|
||||
{{ formatGroupDate(date) }}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<VCard variant="outlined">
|
||||
<VList density="compact">
|
||||
<VListItem
|
||||
v-for="ts in slots"
|
||||
:key="ts.id"
|
||||
>
|
||||
<div class="d-flex align-center gap-x-4 py-2 flex-wrap">
|
||||
<div style="min-inline-size: 200px;">
|
||||
<span class="text-body-1 font-weight-medium">
|
||||
{{ ts.name }}
|
||||
</span>
|
||||
<div class="text-body-2 text-disabled">
|
||||
{{ ts.start_time }} — {{ ts.end_time }}
|
||||
<template v-if="ts.duration_hours">
|
||||
· {{ formatDuration(ts) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<VChip
|
||||
size="small"
|
||||
:color="personTypeColor[ts.person_type]"
|
||||
>
|
||||
{{ personTypeLabel[ts.person_type] }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<RouterLink
|
||||
:to="{ name: 'events-id-time-slots', params: { id: parentEvent.id } }"
|
||||
class="text-caption text-info mt-2 d-inline-block"
|
||||
>
|
||||
Beheer tijdsloten van {{ parentEvent.name }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<!-- Create/Edit dialog -->
|
||||
<CreateTimeSlotDialog
|
||||
v-model="isCreateDialogOpen"
|
||||
|
||||
Reference in New Issue
Block a user