feat(app): dedicated Tijdsloten tab with grouped view and fill rates
Extract time slots from Secties & Shifts into a dedicated Tijdsloten tab. New tab groups time slots by date with Dutch date headers, person type filter pills, fill rate progress bars, and sections count. Includes duplicate, edit, and delete actions with confirmation dialog. - Create types/timeSlot.ts with enriched TimeSlot interface - Add Tijdsloten tab to EventTabsNav between Publiekslijsten and Secties - Create time-slots page with loading, error, and empty states - Remove time slots panel from SectionsShiftsPanel - Update CreateShiftDialog to navigate to time slots tab Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
apps/app/components.d.ts
vendored
3
apps/app/components.d.ts
vendored
@@ -10,6 +10,7 @@ declare module 'vue' {
|
||||
AddEditAddressDialog: typeof import('./src/components/dialogs/AddEditAddressDialog.vue')['default']
|
||||
AddEditPermissionDialog: typeof import('./src/components/dialogs/AddEditPermissionDialog.vue')['default']
|
||||
AddEditRoleDialog: typeof import('./src/components/dialogs/AddEditRoleDialog.vue')['default']
|
||||
AddPersonToCrowdListDialog: typeof import('./src/components/crowd-lists/AddPersonToCrowdListDialog.vue')['default']
|
||||
AppAutocomplete: typeof import('./src/@core/components/app-form-elements/AppAutocomplete.vue')['default']
|
||||
AppBarSearch: typeof import('./src/@core/components/AppBarSearch.vue')['default']
|
||||
AppCardActions: typeof import('./src/@core/components/cards/AppCardActions.vue')['default']
|
||||
@@ -37,6 +38,8 @@ declare module 'vue' {
|
||||
CreateShiftDialog: typeof import('./src/components/sections/CreateShiftDialog.vue')['default']
|
||||
CreateSubEventDialog: typeof import('./src/components/events/CreateSubEventDialog.vue')['default']
|
||||
CreateTimeSlotDialog: typeof import('./src/components/sections/CreateTimeSlotDialog.vue')['default']
|
||||
CrowdListDetailPanel: typeof import('./src/components/crowd-lists/CrowdListDetailPanel.vue')['default']
|
||||
CrowdListFormDialog: typeof import('./src/components/crowd-lists/CrowdListFormDialog.vue')['default']
|
||||
CrowdTypesManager: typeof import('./src/components/organisations/CrowdTypesManager.vue')['default']
|
||||
CustomCheckboxes: typeof import('./src/@core/components/app-form-elements/CustomCheckboxes.vue')['default']
|
||||
CustomCheckboxesWithIcon: typeof import('./src/@core/components/app-form-elements/CustomCheckboxesWithIcon.vue')['default']
|
||||
|
||||
@@ -54,6 +54,7 @@ const baseTabs = [
|
||||
{ label: 'Overzicht', icon: 'tabler-layout-dashboard', route: 'events-id' },
|
||||
{ label: 'Personen', icon: 'tabler-users', route: 'events-id-persons' },
|
||||
{ label: 'Publiekslijsten', icon: 'tabler-list', route: 'events-id-crowd-lists' },
|
||||
{ label: 'Tijdsloten', icon: 'tabler-clock', route: 'events-id-time-slots' },
|
||||
{ label: 'Secties & Shifts', icon: 'tabler-layout-grid', route: 'events-id-sections' },
|
||||
{ label: 'Artiesten', icon: 'tabler-music', route: 'events-id-artists' },
|
||||
{ label: 'Briefings', icon: 'tabler-mail', route: 'events-id-briefings' },
|
||||
@@ -71,7 +72,7 @@ const programmaonderdelenLabel = computed(() => {
|
||||
const tabs = computed(() => {
|
||||
if (!event.value?.is_festival) return baseTabs
|
||||
|
||||
// Festival tab order: Overzicht | Programmaonderdelen | Secties & Shifts | Personen | Publiekslijsten | Artiesten | Briefings | Instellingen
|
||||
// Festival tab order: Overzicht | Programmaonderdelen | Tijdsloten | Secties & Shifts | Personen | Publiekslijsten | Artiesten | Briefings | Instellingen
|
||||
const festivalTab = {
|
||||
label: programmaonderdelenLabel.value,
|
||||
icon: 'tabler-calendar-event',
|
||||
@@ -81,12 +82,13 @@ const tabs = computed(() => {
|
||||
return [
|
||||
baseTabs[0], // Overzicht
|
||||
festivalTab,
|
||||
baseTabs[3], // Secties & Shifts
|
||||
baseTabs[3], // Tijdsloten
|
||||
baseTabs[4], // Secties & Shifts
|
||||
baseTabs[1], // Personen
|
||||
baseTabs[2], // Publiekslijsten
|
||||
baseTabs[4], // Artiesten
|
||||
baseTabs[5], // Briefings
|
||||
baseTabs[6], // Instellingen
|
||||
baseTabs[5], // Artiesten
|
||||
baseTabs[6], // Briefings
|
||||
baseTabs[7], // Instellingen
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -14,12 +14,11 @@ const props = withDefaults(defineProps<{
|
||||
isSubEvent: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
openTimeSlots: []
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const eventIdRef = computed(() => props.eventId)
|
||||
const sectionIdRef = computed(() => props.sectionId)
|
||||
|
||||
@@ -209,7 +208,7 @@ function onSubmit() {
|
||||
variant="text"
|
||||
color="primary"
|
||||
prepend-icon="tabler-clock"
|
||||
@click="emit('openTimeSlots'); modelValue = false"
|
||||
@click="modelValue = false; router.push({ name: 'events-id-time-slots', params: { id: (route.params as { id: string }).id } })"
|
||||
>
|
||||
Time Slots beheren
|
||||
</VBtn>
|
||||
|
||||
@@ -2,126 +2,18 @@
|
||||
import draggable from 'vuedraggable'
|
||||
import { useSectionList, useDeleteSection, useReorderSections } from '@/composables/api/useSections'
|
||||
import { useShiftList, useDeleteShift } from '@/composables/api/useShifts'
|
||||
import { useTimeSlotList, useDeleteTimeSlot } from '@/composables/api/useTimeSlots'
|
||||
import { useSectionsUiStore } from '@/stores/useSectionsUiStore'
|
||||
import CreateSectionDialog from '@/components/sections/CreateSectionDialog.vue'
|
||||
import EditSectionDialog from '@/components/sections/EditSectionDialog.vue'
|
||||
import CreateTimeSlotDialog from '@/components/sections/CreateTimeSlotDialog.vue'
|
||||
import CreateShiftDialog from '@/components/sections/CreateShiftDialog.vue'
|
||||
import AssignShiftDialog from '@/components/sections/AssignShiftDialog.vue'
|
||||
import type { FestivalSection, Shift, ShiftStatus, TimeSlot, PersonType } from '@/types/section'
|
||||
import type { EventItem } from '@/types/event'
|
||||
import type { FestivalSection, Shift, ShiftStatus } from '@/types/section'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
eventId: string
|
||||
children?: EventItem[]
|
||||
isSubEvent?: boolean
|
||||
}>(), {
|
||||
children: () => [],
|
||||
isSubEvent: false,
|
||||
})
|
||||
}>()
|
||||
|
||||
const eventIdRef = computed(() => props.eventId)
|
||||
const isSubEventRef = computed(() => props.isSubEvent)
|
||||
|
||||
// --- Time Slots ---
|
||||
const { data: timeSlots, isLoading: timeSlotsLoading } = useTimeSlotList(eventIdRef, { includeParent: isSubEventRef })
|
||||
const { mutate: deleteTimeSlotMutation, isPending: isDeletingTimeSlot } = useDeleteTimeSlot(eventIdRef)
|
||||
|
||||
const personTypeLabel: Record<PersonType, string> = {
|
||||
VOLUNTEER: 'Vrijwilliger',
|
||||
CREW: 'Crew',
|
||||
PRESS: 'Pers',
|
||||
PHOTO: 'Fotograaf',
|
||||
PARTNER: 'Partner',
|
||||
}
|
||||
|
||||
const personTypeColor: Record<PersonType, string> = {
|
||||
VOLUNTEER: 'success',
|
||||
CREW: 'primary',
|
||||
PRESS: 'warning',
|
||||
PHOTO: 'info',
|
||||
PARTNER: 'secondary',
|
||||
}
|
||||
|
||||
// Group time slots by source when on a sub-event with parent time slots included
|
||||
const hasFestivalTimeSlots = computed(() =>
|
||||
timeSlots.value?.some(ts => ts.source === 'festival') ?? false,
|
||||
)
|
||||
|
||||
const subEventTimeSlots = computed(() =>
|
||||
timeSlots.value?.filter(ts => ts.source !== 'festival') ?? [],
|
||||
)
|
||||
|
||||
const festivalTimeSlots = computed(() =>
|
||||
timeSlots.value?.filter(ts => ts.source === 'festival') ?? [],
|
||||
)
|
||||
|
||||
const timeSlotsSummary = computed(() => {
|
||||
const slots = timeSlots.value
|
||||
if (!slots?.length) return ''
|
||||
|
||||
const count = slots.length
|
||||
const dates = slots.map(ts => ts.date).sort()
|
||||
const minDate = dates[0]
|
||||
const maxDate = dates[dates.length - 1]
|
||||
|
||||
const fmt = new Intl.DateTimeFormat('nl-NL', { day: 'numeric', month: 'short' })
|
||||
const fmtYear = new Intl.DateTimeFormat('nl-NL', { day: 'numeric', month: 'short', year: 'numeric' })
|
||||
|
||||
if (minDate === maxDate) {
|
||||
return `${count} time slot${count > 1 ? 's' : ''} \u00b7 ${fmtYear.format(new Date(minDate))}`
|
||||
}
|
||||
|
||||
return `${count} time slots \u00b7 ${fmt.format(new Date(minDate))} \u2013 ${fmtYear.format(new Date(maxDate))}`
|
||||
})
|
||||
|
||||
function formatTimeSlotDate(iso: string) {
|
||||
if (!iso) return ''
|
||||
return new Intl.DateTimeFormat('nl-NL', { weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso))
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
return time?.slice(0, 5) ?? ''
|
||||
}
|
||||
|
||||
// --- Time slot context labels (Opbouw / Afbraak / Transitie) ---
|
||||
const hasChildren = computed(() => props.children.length > 0)
|
||||
|
||||
const childDateRange = computed(() => {
|
||||
if (!hasChildren.value) return null
|
||||
|
||||
const starts = props.children.map(c => c.start_date).sort()
|
||||
const ends = props.children.map(c => c.end_date).sort()
|
||||
|
||||
return {
|
||||
firstStart: starts[0],
|
||||
lastEnd: ends[ends.length - 1],
|
||||
dates: props.children.map(c => ({ start: c.start_date, end: c.end_date })),
|
||||
}
|
||||
})
|
||||
|
||||
function getTimeSlotContext(ts: TimeSlot): { label: string; color: string } | null {
|
||||
if (!childDateRange.value) return null
|
||||
|
||||
const tsDate = ts.date
|
||||
const { firstStart, lastEnd, dates } = childDateRange.value
|
||||
|
||||
if (tsDate < firstStart) return { label: 'Opbouw', color: 'warning' }
|
||||
if (tsDate > lastEnd) return { label: 'Afbraak', color: 'error' }
|
||||
|
||||
// Check if this date falls between two sub-events (transition)
|
||||
const sorted = [...dates].sort((a, b) => a.start.localeCompare(b.start))
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
const currentEnd = sorted[i].end
|
||||
const nextStart = sorted[i + 1].start
|
||||
if (tsDate > currentEnd && tsDate < nextStart) {
|
||||
return { label: 'Transitie', color: 'info' }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Section list ---
|
||||
const { data: sectionsQuery, isLoading: sectionsLoading } = useSectionList(eventIdRef)
|
||||
@@ -199,18 +91,14 @@ const shiftsByTimeSlot = computed(() => {
|
||||
})
|
||||
|
||||
// --- Dialogs ---
|
||||
const sectionsUiStore = useSectionsUiStore()
|
||||
|
||||
const isCreateSectionOpen = ref(false)
|
||||
const isEditSectionOpen = ref(false)
|
||||
const isCreateTimeSlotOpen = ref(false)
|
||||
const isCreateShiftOpen = ref(false)
|
||||
const isAssignShiftOpen = ref(false)
|
||||
|
||||
const editingShift = ref<Shift | null>(null)
|
||||
const assigningShift = ref<Shift | null>(null)
|
||||
const editingTimeSlot = ref<TimeSlot | null>(null)
|
||||
const duplicatingTimeSlot = ref<TimeSlot | null>(null)
|
||||
|
||||
// Delete section
|
||||
const isDeleteSectionOpen = ref(false)
|
||||
@@ -234,43 +122,6 @@ function onDeleteSectionExecute() {
|
||||
})
|
||||
}
|
||||
|
||||
// Delete time slot
|
||||
const isDeleteTimeSlotOpen = ref(false)
|
||||
const deletingTimeSlotId = ref<string | null>(null)
|
||||
|
||||
function onDeleteTimeSlotConfirm(ts: TimeSlot) {
|
||||
deletingTimeSlotId.value = ts.id
|
||||
isDeleteTimeSlotOpen.value = true
|
||||
}
|
||||
|
||||
function onDeleteTimeSlotExecute() {
|
||||
if (!deletingTimeSlotId.value) return
|
||||
deleteTimeSlotMutation(deletingTimeSlotId.value, {
|
||||
onSuccess: () => {
|
||||
isDeleteTimeSlotOpen.value = false
|
||||
deletingTimeSlotId.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function onEditTimeSlot(ts: TimeSlot) {
|
||||
editingTimeSlot.value = ts
|
||||
duplicatingTimeSlot.value = null
|
||||
isCreateTimeSlotOpen.value = true
|
||||
}
|
||||
|
||||
function onDuplicateTimeSlot(ts: TimeSlot) {
|
||||
editingTimeSlot.value = null
|
||||
duplicatingTimeSlot.value = ts
|
||||
isCreateTimeSlotOpen.value = true
|
||||
}
|
||||
|
||||
function onAddTimeSlot() {
|
||||
editingTimeSlot.value = null
|
||||
duplicatingTimeSlot.value = null
|
||||
isCreateTimeSlotOpen.value = true
|
||||
}
|
||||
|
||||
// Delete shift
|
||||
const isDeleteShiftOpen = ref(false)
|
||||
const deletingShiftId = ref<string | null>(null)
|
||||
@@ -309,15 +160,6 @@ function onEditSection() {
|
||||
isEditSectionOpen.value = true
|
||||
}
|
||||
|
||||
watch(isCreateTimeSlotOpen, (open) => {
|
||||
if (!open) duplicatingTimeSlot.value = null
|
||||
})
|
||||
|
||||
function onOpenTimeSlotsFromShiftDialog() {
|
||||
isCreateShiftOpen.value = false
|
||||
sectionsUiStore.expandTimeSlots()
|
||||
}
|
||||
|
||||
// Status styling
|
||||
const statusColor: Record<ShiftStatus, string> = {
|
||||
draft: 'default',
|
||||
@@ -380,282 +222,6 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- TIME SLOTS PANEL (collapsible, event-scoped) -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<VCard class="mb-4">
|
||||
<!-- Header — clickable to toggle -->
|
||||
<VCardTitle
|
||||
class="d-flex align-center justify-space-between cursor-pointer"
|
||||
@click="sectionsUiStore.toggleTimeSlots()"
|
||||
>
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<span>Time Slots</span>
|
||||
|
||||
<!-- Contextual help tooltip -->
|
||||
<VTooltip
|
||||
location="bottom"
|
||||
max-width="300"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<VIcon
|
||||
v-bind="tooltipProps"
|
||||
icon="tabler-info-circle"
|
||||
size="18"
|
||||
class="text-disabled"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
Time Slots zijn de tijdblokken van je evenement (bijv. 'Opbouw dag 1', 'Afbraak'). Ze gelden voor het hele evenement. Alle secties delen dezelfde Time Slots.
|
||||
</VTooltip>
|
||||
|
||||
<!-- Collapsed summary -->
|
||||
<span
|
||||
v-if="!sectionsUiStore.timeSlotsExpanded && timeSlotsSummary"
|
||||
class="text-body-2 text-disabled"
|
||||
>
|
||||
· {{ timeSlotsSummary }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-x-1">
|
||||
<VBtn
|
||||
icon="tabler-plus"
|
||||
variant="text"
|
||||
size="small"
|
||||
title="Time Slot toevoegen"
|
||||
@click.stop="onAddTimeSlot"
|
||||
/>
|
||||
<VIcon
|
||||
:icon="sectionsUiStore.timeSlotsExpanded ? 'tabler-chevron-up' : 'tabler-chevron-down'"
|
||||
size="20"
|
||||
/>
|
||||
</div>
|
||||
</VCardTitle>
|
||||
|
||||
<!-- Expanded content -->
|
||||
<VExpandTransition>
|
||||
<div v-show="sectionsUiStore.timeSlotsExpanded">
|
||||
<VDivider />
|
||||
|
||||
<VCardText>
|
||||
<!-- Loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="timeSlotsLoading"
|
||||
type="chip@4"
|
||||
/>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-else-if="!timeSlots?.length"
|
||||
class="text-center py-4"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
size="40"
|
||||
class="mb-3 text-disabled"
|
||||
/>
|
||||
<p class="text-body-1 text-disabled mb-4">
|
||||
Nog geen time slots. Maak eerst time slots aan voordat je shifts kunt plannen.
|
||||
</p>
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
@click="onAddTimeSlot"
|
||||
>
|
||||
Time Slot aanmaken
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- Time Slot cards -->
|
||||
<template v-else>
|
||||
<!-- Grouped view when parent festival time slots are included -->
|
||||
<template v-if="hasFestivalTimeSlots">
|
||||
<!-- Sub-event's own time slots -->
|
||||
<div
|
||||
v-if="subEventTimeSlots.length"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-caption text-uppercase font-weight-medium text-disabled mb-2">
|
||||
{{ subEventTimeSlots[0]?.event_name ?? 'Programma' }}
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<VCard
|
||||
v-for="ts in subEventTimeSlots"
|
||||
:key="ts.id"
|
||||
variant="outlined"
|
||||
class="pa-3"
|
||||
style="min-inline-size: 180px; max-inline-size: 220px;"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between mb-1">
|
||||
<span class="text-body-1 font-weight-medium text-truncate">
|
||||
{{ ts.name }}
|
||||
</span>
|
||||
<div class="d-flex">
|
||||
<VBtn
|
||||
icon="tabler-copy"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
title="Dupliceren"
|
||||
@click="onDuplicateTimeSlot(ts)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
title="Bewerken"
|
||||
@click="onEditTimeSlot(ts)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="error"
|
||||
title="Verwijderen"
|
||||
@click="onDeleteTimeSlotConfirm(ts)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-body-2 text-disabled">
|
||||
{{ formatTimeSlotDate(ts.date) }} · {{ formatTime(ts.start_time) }}–{{ formatTime(ts.end_time) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-x-1 mt-1">
|
||||
<VChip
|
||||
size="x-small"
|
||||
:color="personTypeColor[ts.person_type]"
|
||||
>
|
||||
{{ personTypeLabel[ts.person_type] }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Festival-level time slots (read-only view) -->
|
||||
<div v-if="festivalTimeSlots.length">
|
||||
<div class="text-caption text-uppercase font-weight-medium text-disabled mb-2">
|
||||
{{ festivalTimeSlots[0]?.event_name ?? 'Festival' }}
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<VCard
|
||||
v-for="ts in festivalTimeSlots"
|
||||
:key="ts.id"
|
||||
variant="outlined"
|
||||
class="pa-3"
|
||||
style="min-inline-size: 180px; max-inline-size: 220px; opacity: 0.75;"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between mb-1">
|
||||
<span class="text-body-1 font-weight-medium text-truncate">
|
||||
{{ ts.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text-body-2 text-disabled">
|
||||
{{ formatTimeSlotDate(ts.date) }} · {{ formatTime(ts.start_time) }}–{{ formatTime(ts.end_time) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-x-1 mt-1">
|
||||
<VChip
|
||||
size="x-small"
|
||||
:color="personTypeColor[ts.person_type]"
|
||||
>
|
||||
{{ personTypeLabel[ts.person_type] }}
|
||||
</VChip>
|
||||
|
||||
<VChip
|
||||
v-if="getTimeSlotContext(ts)"
|
||||
size="x-small"
|
||||
:color="getTimeSlotContext(ts)!.color"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ getTimeSlotContext(ts)!.label }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Flat view (default, for non-sub-events) -->
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-wrap gap-3"
|
||||
>
|
||||
<VCard
|
||||
v-for="ts in timeSlots"
|
||||
:key="ts.id"
|
||||
variant="outlined"
|
||||
class="pa-3"
|
||||
style="min-inline-size: 180px; max-inline-size: 220px;"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between mb-1">
|
||||
<span class="text-body-1 font-weight-medium text-truncate">
|
||||
{{ ts.name }}
|
||||
</span>
|
||||
<div class="d-flex">
|
||||
<VBtn
|
||||
icon="tabler-copy"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
title="Dupliceren"
|
||||
@click="onDuplicateTimeSlot(ts)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
title="Bewerken"
|
||||
@click="onEditTimeSlot(ts)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="error"
|
||||
title="Verwijderen"
|
||||
@click="onDeleteTimeSlotConfirm(ts)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-body-2 text-disabled">
|
||||
{{ formatTimeSlotDate(ts.date) }} · {{ formatTime(ts.start_time) }}–{{ formatTime(ts.end_time) }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-x-1 mt-1">
|
||||
<VChip
|
||||
size="x-small"
|
||||
:color="personTypeColor[ts.person_type]"
|
||||
>
|
||||
{{ personTypeLabel[ts.person_type] }}
|
||||
</VChip>
|
||||
|
||||
<!-- Context label (Opbouw / Afbraak / Transitie) -->
|
||||
<VChip
|
||||
v-if="getTimeSlotContext(ts)"
|
||||
size="x-small"
|
||||
:color="getTimeSlotContext(ts)!.color"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ getTimeSlotContext(ts)!.label }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ts.shifts_count != null"
|
||||
class="text-caption text-disabled mt-1"
|
||||
>
|
||||
{{ ts.shifts_count }} shift{{ ts.shifts_count === 1 ? '' : 's' }}
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
</VCardText>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
</VCard>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- SECTIONS + SHIFTS (two-column layout) -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
@@ -988,13 +554,6 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
@updated="onSectionUpdated"
|
||||
/>
|
||||
|
||||
<CreateTimeSlotDialog
|
||||
v-model="isCreateTimeSlotOpen"
|
||||
:event-id="eventId"
|
||||
:time-slot="editingTimeSlot"
|
||||
:duplicate-from="duplicatingTimeSlot"
|
||||
/>
|
||||
|
||||
<CreateShiftDialog
|
||||
v-if="activeSection"
|
||||
v-model="isCreateShiftOpen"
|
||||
@@ -1002,7 +561,6 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
:section-id="activeSection.id"
|
||||
:shift="editingShift"
|
||||
:is-sub-event="isSubEvent"
|
||||
@open-time-slots="onOpenTimeSlotsFromShiftDialog"
|
||||
/>
|
||||
|
||||
<AssignShiftDialog
|
||||
@@ -1040,34 +598,6 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Delete time slot confirmation -->
|
||||
<VDialog
|
||||
v-model="isDeleteTimeSlotOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<VCard title="Time Slot verwijderen">
|
||||
<VCardText>
|
||||
Weet je zeker dat je dit time slot wilt verwijderen? Alle shifts die dit time slot gebruiken worden ook verwijderd.
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isDeleteTimeSlotOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
:loading="isDeletingTimeSlot"
|
||||
@click="onDeleteTimeSlotExecute"
|
||||
>
|
||||
Verwijderen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Delete shift confirmation -->
|
||||
<VDialog
|
||||
v-model="isDeleteShiftOpen"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { CreateTimeSlotPayload, TimeSlot, UpdateTimeSlotPayload } from '@/types/section'
|
||||
import type { CreateTimeSlotPayload, TimeSlot, UpdateTimeSlotPayload } from '@/types/timeSlot'
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import EventTabsNav from '@/components/events/EventTabsNav.vue'
|
||||
import SectionsShiftsPanel from '@/components/sections/SectionsShiftsPanel.vue'
|
||||
import { useEventChildren } from '@/composables/api/useEvents'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
@@ -11,13 +9,8 @@ definePage({
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const orgId = computed(() => authStore.currentOrganisation?.id ?? '')
|
||||
const eventId = computed(() => String((route.params as { id: string }).id))
|
||||
|
||||
// Load children for festivals — needed for time slot context chips (Opbouw/Afbraak/Transitie)
|
||||
const { data: children } = useEventChildren(orgId, eventId)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,7 +19,6 @@ const { data: children } = useEventChildren(orgId, eventId)
|
||||
<SectionsShiftsPanel
|
||||
:event-id="eventId"
|
||||
:is-sub-event="event?.is_sub_event ?? false"
|
||||
:children="children ?? []"
|
||||
/>
|
||||
</template>
|
||||
</EventTabsNav>
|
||||
|
||||
460
apps/app/src/pages/events/[id]/time-slots/index.vue
Normal file
460
apps/app/src/pages/events/[id]/time-slots/index.vue
Normal file
@@ -0,0 +1,460 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeSlotList, useDeleteTimeSlot } from '@/composables/api/useTimeSlots'
|
||||
import { useEventChildren } from '@/composables/api/useEvents'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import EventTabsNav from '@/components/events/EventTabsNav.vue'
|
||||
import CreateTimeSlotDialog from '@/components/sections/CreateTimeSlotDialog.vue'
|
||||
import { PersonType } from '@/types/timeSlot'
|
||||
import type { TimeSlot, PersonType as PersonTypeValue } from '@/types/timeSlot'
|
||||
import type { EventItem } from '@/types/event'
|
||||
|
||||
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: timeSlots, isLoading, isError, refetch } = useTimeSlotList(eventId)
|
||||
const { mutate: deleteTimeSlotMutation, isPending: isDeletingTimeSlot } = useDeleteTimeSlot(eventId)
|
||||
|
||||
// Load children for festivals — needed for time slot context chips
|
||||
const { data: children } = useEventChildren(orgId, eventId)
|
||||
|
||||
// --- Person type filter ---
|
||||
const activeFilter = ref<PersonTypeValue | null>(null)
|
||||
|
||||
const personTypeLabel: Record<PersonTypeValue, string> = {
|
||||
VOLUNTEER: 'Vrijwilliger',
|
||||
CREW: 'Crew',
|
||||
PRESS: 'Pers',
|
||||
PHOTO: 'Foto',
|
||||
PARTNER: 'Partner',
|
||||
}
|
||||
|
||||
const personTypeColor: Record<PersonTypeValue, string> = {
|
||||
VOLUNTEER: 'success',
|
||||
CREW: 'info',
|
||||
PRESS: 'warning',
|
||||
PHOTO: 'secondary',
|
||||
PARTNER: 'default',
|
||||
}
|
||||
|
||||
const personTypeFilters = [
|
||||
{ label: 'Alle types', value: null },
|
||||
...Object.entries(PersonType).map(([, value]) => ({
|
||||
label: personTypeLabel[value],
|
||||
value,
|
||||
})),
|
||||
]
|
||||
|
||||
const filteredSlots = computed(() => {
|
||||
if (!timeSlots.value) return []
|
||||
if (!activeFilter.value) return timeSlots.value
|
||||
return timeSlots.value.filter(s => s.person_type === activeFilter.value)
|
||||
})
|
||||
|
||||
// --- Date grouping ---
|
||||
const dutchDateFormatter = new Intl.DateTimeFormat('nl-NL', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
function formatGroupDate(iso: string): string {
|
||||
const formatted = dutchDateFormatter.format(new Date(iso))
|
||||
return formatted.charAt(0).toUpperCase() + formatted.slice(1)
|
||||
}
|
||||
|
||||
const groupedByDate = computed(() => {
|
||||
const groups = new Map<string, TimeSlot[]>()
|
||||
filteredSlots.value.forEach(slot => {
|
||||
const existing = groups.get(slot.date) || []
|
||||
existing.push(slot)
|
||||
groups.set(slot.date, existing)
|
||||
})
|
||||
return groups
|
||||
})
|
||||
|
||||
// --- Summary ---
|
||||
const summary = computed(() => {
|
||||
const slots = timeSlots.value
|
||||
if (!slots?.length) return ''
|
||||
const dates = new Set(slots.map(s => s.date))
|
||||
return `${slots.length} slots · ${dates.size} ${dates.size === 1 ? 'dag' : 'dagen'}`
|
||||
})
|
||||
|
||||
// --- Time slot context labels (Opbouw / Afbraak / Transitie) ---
|
||||
const childDateRange = computed(() => {
|
||||
if (!children.value?.length) return null
|
||||
const starts = children.value.map((c: EventItem) => c.start_date).sort()
|
||||
const ends = children.value.map((c: EventItem) => c.end_date).sort()
|
||||
return {
|
||||
firstStart: starts[0],
|
||||
lastEnd: ends[ends.length - 1],
|
||||
dates: children.value.map((c: EventItem) => ({ start: c.start_date, end: c.end_date })),
|
||||
}
|
||||
})
|
||||
|
||||
function getTimeSlotContext(ts: TimeSlot): { label: string; color: string } | null {
|
||||
if (!childDateRange.value) return null
|
||||
const tsDate = ts.date
|
||||
const { firstStart, lastEnd, dates } = childDateRange.value
|
||||
if (tsDate < firstStart) return { label: 'Opbouw', color: 'warning' }
|
||||
if (tsDate > lastEnd) return { label: 'Afbraak', color: 'error' }
|
||||
const sorted = [...dates].sort((a, b) => a.start.localeCompare(b.start))
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
if (tsDate > sorted[i].end && tsDate < sorted[i + 1].start) {
|
||||
return { label: 'Transitie', color: 'info' }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// --- Fill rate ---
|
||||
function fillRatePercent(ts: TimeSlot): number {
|
||||
if (ts.total_slots === 0) return 0
|
||||
return Math.round((ts.filled_slots / ts.total_slots) * 100)
|
||||
}
|
||||
|
||||
function fillRateColor(ts: TimeSlot): string {
|
||||
const pct = fillRatePercent(ts)
|
||||
if (pct >= 80) return 'success'
|
||||
if (pct >= 50) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
// --- Duration formatting ---
|
||||
function formatDuration(ts: TimeSlot): string {
|
||||
if (ts.duration_hours == null) return ''
|
||||
const h = Math.floor(ts.duration_hours)
|
||||
const m = Math.round((ts.duration_hours - h) * 60)
|
||||
if (m === 0) return `${h} uur`
|
||||
return `${h},${m < 10 ? '0' : ''}${m} uur`
|
||||
}
|
||||
|
||||
// --- CRUD dialog state ---
|
||||
const isCreateDialogOpen = ref(false)
|
||||
const editingTimeSlot = ref<TimeSlot | null>(null)
|
||||
const duplicatingTimeSlot = ref<TimeSlot | null>(null)
|
||||
|
||||
function onAddTimeSlot() {
|
||||
editingTimeSlot.value = null
|
||||
duplicatingTimeSlot.value = null
|
||||
isCreateDialogOpen.value = true
|
||||
}
|
||||
|
||||
function onEditTimeSlot(ts: TimeSlot) {
|
||||
editingTimeSlot.value = ts
|
||||
duplicatingTimeSlot.value = null
|
||||
isCreateDialogOpen.value = true
|
||||
}
|
||||
|
||||
function onDuplicateTimeSlot(ts: TimeSlot) {
|
||||
editingTimeSlot.value = null
|
||||
duplicatingTimeSlot.value = ts
|
||||
isCreateDialogOpen.value = true
|
||||
}
|
||||
|
||||
watch(isCreateDialogOpen, (open) => {
|
||||
if (!open) duplicatingTimeSlot.value = null
|
||||
})
|
||||
|
||||
// --- Delete confirmation ---
|
||||
const isDeleteDialogOpen = ref(false)
|
||||
const deletingTimeSlot = ref<TimeSlot | null>(null)
|
||||
|
||||
function onDeleteConfirm(ts: TimeSlot) {
|
||||
deletingTimeSlot.value = ts
|
||||
isDeleteDialogOpen.value = true
|
||||
}
|
||||
|
||||
function onDeleteExecute() {
|
||||
if (!deletingTimeSlot.value) return
|
||||
deleteTimeSlotMutation(deletingTimeSlot.value.id, {
|
||||
onSuccess: () => {
|
||||
isDeleteDialogOpen.value = false
|
||||
deletingTimeSlot.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EventTabsNav>
|
||||
<template #default="{ event }">
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-center justify-space-between flex-wrap gap-2 mb-4">
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<h5 class="text-h5">
|
||||
Tijdsloten
|
||||
</h5>
|
||||
<VChip
|
||||
v-if="summary"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ summary }}
|
||||
</VChip>
|
||||
</div>
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
@click="onAddTimeSlot"
|
||||
>
|
||||
Tijdslot aanmaken
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- Filter pills -->
|
||||
<div
|
||||
v-if="timeSlots?.length"
|
||||
class="d-flex flex-wrap gap-2 mb-4"
|
||||
>
|
||||
<VChip
|
||||
v-for="filter in personTypeFilters"
|
||||
:key="String(filter.value)"
|
||||
:color="activeFilter === filter.value ? 'primary' : 'default'"
|
||||
:variant="activeFilter === filter.value ? 'flat' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="activeFilter = filter.value"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<VSkeletonLoader
|
||||
v-if="isLoading"
|
||||
type="table"
|
||||
/>
|
||||
|
||||
<!-- Error -->
|
||||
<VAlert
|
||||
v-else-if="isError"
|
||||
type="error"
|
||||
class="mb-4"
|
||||
>
|
||||
Kon tijdsloten niet laden.
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="refetch()"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<!-- Empty -->
|
||||
<VCard
|
||||
v-else-if="!timeSlots?.length"
|
||||
class="text-center pa-12"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
size="64"
|
||||
class="mb-4 text-disabled"
|
||||
/>
|
||||
<h6 class="text-h6 mb-2">
|
||||
Nog geen tijdsloten aangemaakt
|
||||
</h6>
|
||||
<p class="text-body-1 text-disabled mb-6">
|
||||
Maak je eerste tijdslot aan om shifts te kunnen plannen.
|
||||
</p>
|
||||
<VBtn
|
||||
prepend-icon="tabler-plus"
|
||||
@click="onAddTimeSlot"
|
||||
>
|
||||
Tijdslot aanmaken
|
||||
</VBtn>
|
||||
</VCard>
|
||||
|
||||
<!-- Empty after filter -->
|
||||
<VCard
|
||||
v-else-if="!filteredSlots.length"
|
||||
class="text-center pa-8"
|
||||
>
|
||||
<p class="text-body-1 text-disabled mb-0">
|
||||
Geen tijdsloten voor dit type.
|
||||
</p>
|
||||
</VCard>
|
||||
|
||||
<!-- Grouped by date -->
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="[date, slots] in groupedByDate"
|
||||
:key="date"
|
||||
class="mb-6"
|
||||
>
|
||||
<!-- Day header -->
|
||||
<div class="d-flex align-center gap-x-2 mb-3">
|
||||
<h6 class="text-h6">
|
||||
{{ formatGroupDate(date) }}
|
||||
</h6>
|
||||
<VChip
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ slots.length }} {{ slots.length === 1 ? 'slot' : 'slots' }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<!-- Time slot rows -->
|
||||
<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">
|
||||
<!-- Name + time range -->
|
||||
<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>
|
||||
|
||||
<!-- Person type badge -->
|
||||
<VChip
|
||||
size="small"
|
||||
:color="personTypeColor[ts.person_type]"
|
||||
>
|
||||
{{ personTypeLabel[ts.person_type] }}
|
||||
</VChip>
|
||||
|
||||
<!-- Context label (Opbouw / Afbraak / Transitie) -->
|
||||
<VChip
|
||||
v-if="getTimeSlotContext(ts)"
|
||||
size="small"
|
||||
:color="getTimeSlotContext(ts)!.color"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ getTimeSlotContext(ts)!.label }}
|
||||
</VChip>
|
||||
|
||||
<!-- Fill rate -->
|
||||
<div
|
||||
class="d-flex align-center gap-x-2"
|
||||
style="min-inline-size: 200px; flex: 1;"
|
||||
>
|
||||
<template v-if="ts.shifts_count > 0">
|
||||
<VProgressLinear
|
||||
:model-value="fillRatePercent(ts)"
|
||||
:color="fillRateColor(ts)"
|
||||
height="8"
|
||||
rounded
|
||||
style="max-inline-size: 120px;"
|
||||
/>
|
||||
<span class="text-body-2 text-no-wrap">
|
||||
{{ ts.filled_slots }}/{{ ts.total_slots }} plekken
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="text-body-2 text-disabled font-italic"
|
||||
>
|
||||
Geen shifts aangemaakt
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Sections count -->
|
||||
<span
|
||||
v-if="ts.sections_count > 0"
|
||||
class="text-body-2 text-no-wrap"
|
||||
>
|
||||
{{ ts.sections_count }} {{ ts.sections_count === 1 ? 'sectie' : 'secties' }}
|
||||
</span>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex gap-x-1">
|
||||
<VBtn
|
||||
icon="tabler-copy"
|
||||
variant="text"
|
||||
size="small"
|
||||
title="Dupliceren"
|
||||
@click="onDuplicateTimeSlot(ts)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-edit"
|
||||
variant="text"
|
||||
size="small"
|
||||
title="Bewerken"
|
||||
@click="onEditTimeSlot(ts)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
title="Verwijderen"
|
||||
@click="onDeleteConfirm(ts)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Create/Edit dialog -->
|
||||
<CreateTimeSlotDialog
|
||||
v-model="isCreateDialogOpen"
|
||||
:event-id="eventId"
|
||||
:time-slot="editingTimeSlot"
|
||||
:duplicate-from="duplicatingTimeSlot"
|
||||
/>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<VDialog
|
||||
v-model="isDeleteDialogOpen"
|
||||
max-width="450"
|
||||
>
|
||||
<VCard title="Tijdslot verwijderen">
|
||||
<VCardText>
|
||||
<p>
|
||||
Weet je zeker dat je dit tijdslot wilt verwijderen?
|
||||
Alle shifts die aan dit tijdslot gekoppeld zijn worden ook verwijderd.
|
||||
</p>
|
||||
<p
|
||||
v-if="deletingTimeSlot"
|
||||
class="font-weight-medium mb-0"
|
||||
>
|
||||
{{ deletingTimeSlot.name }} — {{ formatGroupDate(deletingTimeSlot.date) }}
|
||||
</p>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="isDeleteDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
:loading="isDeletingTimeSlot"
|
||||
@click="onDeleteExecute"
|
||||
>
|
||||
Verwijderen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
</EventTabsNav>
|
||||
</template>
|
||||
@@ -1,3 +1,8 @@
|
||||
import type { TimeSlot, PersonType, TimeSlotSource } from '@/types/timeSlot'
|
||||
|
||||
export type { TimeSlot, PersonType, TimeSlotSource }
|
||||
export type { CreateTimeSlotPayload, UpdateTimeSlotPayload } from '@/types/timeSlot'
|
||||
|
||||
export type SectionType = 'standard' | 'cross_event'
|
||||
|
||||
export type ShiftStatus = 'draft' | 'open' | 'full' | 'in_progress' | 'completed' | 'cancelled'
|
||||
@@ -16,24 +21,6 @@ export interface FestivalSection {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type PersonType = 'CREW' | 'VOLUNTEER' | 'PRESS' | 'PHOTO' | 'PARTNER'
|
||||
|
||||
export type TimeSlotSource = 'sub_event' | 'festival'
|
||||
|
||||
export interface TimeSlot {
|
||||
id: string
|
||||
event_id: string
|
||||
name: string
|
||||
person_type: PersonType
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
duration_hours: number | null
|
||||
source?: TimeSlotSource | null
|
||||
event_name?: string | null
|
||||
shifts_count?: number
|
||||
}
|
||||
|
||||
export interface Shift {
|
||||
id: string
|
||||
festival_section_id: string
|
||||
@@ -78,16 +65,6 @@ export interface CreateSectionPayload {
|
||||
|
||||
export interface UpdateSectionPayload extends Partial<CreateSectionPayload> {}
|
||||
|
||||
export interface CreateTimeSlotPayload {
|
||||
name: string
|
||||
person_type: string
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
duration_hours?: number
|
||||
}
|
||||
|
||||
export interface UpdateTimeSlotPayload extends Partial<CreateTimeSlotPayload> {}
|
||||
|
||||
export interface CreateShiftPayload {
|
||||
time_slot_id: string
|
||||
|
||||
40
apps/app/src/types/timeSlot.ts
Normal file
40
apps/app/src/types/timeSlot.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export const PersonType = {
|
||||
CREW: 'CREW',
|
||||
VOLUNTEER: 'VOLUNTEER',
|
||||
PRESS: 'PRESS',
|
||||
PHOTO: 'PHOTO',
|
||||
PARTNER: 'PARTNER',
|
||||
} as const
|
||||
|
||||
export type PersonType = (typeof PersonType)[keyof typeof PersonType]
|
||||
|
||||
export type TimeSlotSource = 'sub_event' | 'festival'
|
||||
|
||||
export interface TimeSlot {
|
||||
id: string
|
||||
event_id: string
|
||||
name: string
|
||||
person_type: PersonType
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
duration_hours: number | null
|
||||
source?: TimeSlotSource | null
|
||||
event_name?: string | null
|
||||
shifts_count: number
|
||||
total_slots: number
|
||||
filled_slots: number
|
||||
sections_count: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface CreateTimeSlotPayload {
|
||||
name: string
|
||||
person_type: PersonType
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
duration_hours?: number
|
||||
}
|
||||
|
||||
export interface UpdateTimeSlotPayload extends Partial<CreateTimeSlotPayload> {}
|
||||
2
apps/app/typed-router.d.ts
vendored
2
apps/app/typed-router.d.ts
vendored
@@ -25,10 +25,12 @@ declare module 'vue-router/auto-routes' {
|
||||
'events-id': RouteRecordInfo<'events-id', '/events/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-artists': RouteRecordInfo<'events-id-artists', '/events/:id/artists', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-briefings': RouteRecordInfo<'events-id-briefings', '/events/:id/briefings', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-crowd-lists': RouteRecordInfo<'events-id-crowd-lists', '/events/:id/crowd-lists', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-persons': RouteRecordInfo<'events-id-persons', '/events/:id/persons', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-programmaonderdelen': RouteRecordInfo<'events-id-programmaonderdelen', '/events/:id/programmaonderdelen', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-sections': RouteRecordInfo<'events-id-sections', '/events/:id/sections', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-settings': RouteRecordInfo<'events-id-settings', '/events/:id/settings', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'events-id-time-slots': RouteRecordInfo<'events-id-time-slots', '/events/:id/time-slots', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
'invitations-token': RouteRecordInfo<'invitations-token', '/invitations/:token', { token: ParamValue<true> }, { token: ParamValue<false> }>,
|
||||
'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
|
||||
'organisation': RouteRecordInfo<'organisation', '/organisation', Record<never, never>, Record<never, never>>,
|
||||
|
||||
Reference in New Issue
Block a user