WS-3 session 1b-ii Task 4 (audit Buckets A, C, D — 26 items resolved
this commit; 24 indent items in useTimeSlotDropdown.ts remain — see
deviations).
Bucket A — Trivial fixes (12 items resolved):
- A.1: second-pass eslint --fix on App.vue resolved 4 multi-attribute
warnings. AppKpiCard / PortalLayout / PublicLayout
lines-around-comment items were attempted via blank-line addition,
but that introduced an equal number of vue/block-tag-newline
errors (the rules conflict at the SFC <script>-tag boundary). The
blank-line additions were reverted; net-zero, the 3 items remain
for a 1b-iii .eslintrc.cjs override decision.
- A.3: 6 unused-imports / unused-vars manual deletes:
* OrganisationSwitcher.vue: removed orphan toggleMenu() function
* CreateShiftDialog.vue: removed unused 'scenario' from destructure
* pages/events/[id]/time-slots/index.vue: removed unused 'event'
slot scope binding (template <#default="{ event }"> → <#default>)
* pages/organisation/companies.vue: removed unused authStore
declaration + import
* pages/platform/activity-log/index.vue: removed unused
search/searchDebounced pair
* PersonDetailPanel.vue:77: removed redundant single-statement
if-braces (curly autofix that the original pass didn't reach)
Bucket C — Style preference (8 items resolved):
- DismissFailureDialog.vue:43: collapsed two consecutive `if cond return false`
branches into `return !(cond)`
- FormFailureDetail.vue:44: replaced `void clipboard.writeText(...)` with
`clipboard.writeText(...).catch(() => {})` — fire-and-forget with
silent rejection (the no-void rule wants the void operator gone;
.catch() handles it semantically).
- AssignShiftDialog.vue:40-46: hasOverlapWarning collapsed from
always-false branching to `computed(() => false)` (the early-return
was dead code; backend enforces the constraint).
- SectionsShiftsPanel.vue:333 + registration-fields.vue:335: rewrote
`:delay-on-touch-only="true"` to attribute-shorthand `delay-on-touch-only`.
- AssignPersonDialog.vue:120-128: collapsed two `if outer { if inner ... }`
pairs into single `if (outer && inner)` form (sonarjs/no-collapsible-if).
- useImpersonationStore.ts:99-104: collapsed the same nested-if pattern
into `if (!data.data.active && state.value)`.
Bucket D — Vuetify utility class rename (5 items, 3 files):
- ml-1 → ms-1 (PersonDetailPanel:271, SectionsShiftsPanel:357,
AssignPersonDialog:496)
- pl-4 → ps-4 (AssignPersonDialog:457)
- ml-auto → ms-auto (AssignPersonDialog:471)
LTR/RTL-aware Vuetify utilities, matching the Vuexy reference idiom.
Tests + typecheck verified green.
Lint baseline: 62 → 36.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
767 lines
23 KiB
Vue
767 lines
23 KiB
Vue
<script setup lang="ts">
|
||
import { useDeleteSection, useReorderSections, useSectionList } from '@/composables/api/useSections'
|
||
import { useDeleteShift, useShiftList } from '@/composables/api/useShifts'
|
||
import { useEventDetail } from '@/composables/api/useEvents'
|
||
import { useAuthStore } from '@/stores/useAuthStore'
|
||
import CreateSectionDialog from '@/components/sections/CreateSectionDialog.vue'
|
||
import EditSectionDialog from '@/components/sections/EditSectionDialog.vue'
|
||
import CreateShiftDialog from '@/components/sections/CreateShiftDialog.vue'
|
||
import AssignPersonDialog from '@/components/shifts/AssignPersonDialog.vue'
|
||
import ShiftDetailPanel from '@/components/shifts/ShiftDetailPanel.vue'
|
||
import { useShiftDetailStore } from '@/stores/useShiftDetailStore'
|
||
import InfoTooltip from '@/components/common/InfoTooltip.vue'
|
||
import type { FestivalSection, Shift, ShiftStatus } from '@/types/section'
|
||
import type { EventItem } from '@/types/event'
|
||
|
||
const props = defineProps<{
|
||
eventId: string
|
||
isSubEvent?: boolean
|
||
parentEvent?: EventItem | null
|
||
}>()
|
||
|
||
const shiftDetailStore = useShiftDetailStore()
|
||
const authStore = useAuthStore()
|
||
|
||
const orgIdRef = computed(() => authStore.currentOrganisation?.id ?? '')
|
||
const eventIdRef = computed(() => props.eventId)
|
||
|
||
// Event detail for context banner
|
||
const { data: eventDetail } = useEventDetail(orgIdRef, eventIdRef)
|
||
|
||
const isSubEvent = computed(() => props.isSubEvent ?? false)
|
||
|
||
// --- Section list ---
|
||
const { data: sectionsQuery, isLoading: sectionsLoading } = useSectionList(orgIdRef, eventIdRef)
|
||
const { mutate: reorderSections } = useReorderSections(orgIdRef, eventIdRef)
|
||
|
||
// Local ref for draggable — synced from query but not overwritten during drag
|
||
const sections = ref<FestivalSection[]>([])
|
||
|
||
watch(sectionsQuery, newData => {
|
||
if (newData)
|
||
sections.value = [...newData]
|
||
}, { immediate: true })
|
||
|
||
function onDragEnd() {
|
||
reorderSections(sections.value.map(s => s.id))
|
||
}
|
||
|
||
const activeSectionId = ref<string | null>(null)
|
||
|
||
const activeSection = computed(() =>
|
||
sections.value?.find(s => s.id === activeSectionId.value) ?? null,
|
||
)
|
||
|
||
watch(sections, list => {
|
||
if (list?.length && !activeSectionId.value)
|
||
activeSectionId.value = list[0].id
|
||
}, { immediate: true })
|
||
|
||
// For cross_event sections, use the section's own event_id (parent festival)
|
||
const activeSectionEventId = computed(() => {
|
||
if (activeSection.value?.type === 'cross_event')
|
||
return activeSection.value.event_id
|
||
|
||
return props.eventId
|
||
})
|
||
|
||
const activeSectionEventIdRef = computed(() => activeSectionEventId.value)
|
||
|
||
const { mutate: deleteSection } = useDeleteSection(orgIdRef, activeSectionEventIdRef)
|
||
|
||
// --- Shifts for active section ---
|
||
|
||
const activeSectionIdRef = computed(() => activeSectionId.value ?? '')
|
||
|
||
const { data: shifts, isLoading: shiftsLoading } = useShiftList(orgIdRef, activeSectionEventIdRef, activeSectionIdRef)
|
||
const { mutate: deleteShiftMutation, isPending: isDeleting } = useDeleteShift(orgIdRef, activeSectionEventIdRef, activeSectionIdRef)
|
||
|
||
// Group shifts by time_slot_id
|
||
const shiftsByTimeSlot = computed(() => {
|
||
if (!shifts.value)
|
||
return []
|
||
|
||
const groups = new Map<string, { timeSlotName: string; date: string; startTime: string; endTime: string; totalSlots: number; filledSlots: number; shifts: Shift[] }>()
|
||
|
||
for (const shift of shifts.value) {
|
||
const tsId = shift.time_slot_id
|
||
if (!groups.has(tsId)) {
|
||
groups.set(tsId, {
|
||
timeSlotName: shift.time_slot?.name ?? 'Onbekend',
|
||
date: shift.time_slot?.date ?? '',
|
||
startTime: shift.effective_start_time,
|
||
endTime: shift.effective_end_time,
|
||
totalSlots: 0,
|
||
filledSlots: 0,
|
||
shifts: [],
|
||
})
|
||
}
|
||
const group = groups.get(tsId)!
|
||
|
||
group.shifts.push(shift)
|
||
group.totalSlots += shift.slots_total
|
||
group.filledSlots += shift.filled_slots
|
||
}
|
||
|
||
return Array.from(groups.values())
|
||
})
|
||
|
||
// --- Dialogs ---
|
||
|
||
const isCreateSectionOpen = ref(false)
|
||
const isEditSectionOpen = ref(false)
|
||
const isCreateShiftOpen = ref(false)
|
||
const isAssignShiftOpen = ref(false)
|
||
|
||
const editingShift = ref<Shift | null>(null)
|
||
const assigningShift = ref<Shift | null>(null)
|
||
|
||
// Delete section
|
||
const isDeleteSectionOpen = ref(false)
|
||
const deletingSectionId = ref<string | null>(null)
|
||
|
||
function onDeleteSectionConfirm(section: FestivalSection) {
|
||
deletingSectionId.value = section.id
|
||
isDeleteSectionOpen.value = true
|
||
}
|
||
|
||
function onDeleteSectionExecute() {
|
||
if (!deletingSectionId.value)
|
||
return
|
||
deleteSection(deletingSectionId.value, {
|
||
onSuccess: () => {
|
||
isDeleteSectionOpen.value = false
|
||
if (activeSectionId.value === deletingSectionId.value)
|
||
activeSectionId.value = sections.value?.[0]?.id ?? null
|
||
|
||
deletingSectionId.value = null
|
||
},
|
||
})
|
||
}
|
||
|
||
// Delete shift
|
||
const isDeleteShiftOpen = ref(false)
|
||
const deletingShiftId = ref<string | null>(null)
|
||
|
||
function onDeleteShiftConfirm(shift: Shift) {
|
||
deletingShiftId.value = shift.id
|
||
isDeleteShiftOpen.value = true
|
||
}
|
||
|
||
function onDeleteShiftExecute() {
|
||
if (!deletingShiftId.value)
|
||
return
|
||
deleteShiftMutation(deletingShiftId.value, {
|
||
onSuccess: () => {
|
||
isDeleteShiftOpen.value = false
|
||
deletingShiftId.value = null
|
||
},
|
||
})
|
||
}
|
||
|
||
function onEditShift(shift: Shift) {
|
||
editingShift.value = shift
|
||
isCreateShiftOpen.value = true
|
||
}
|
||
|
||
function onAssignShift(shift: Shift) {
|
||
assigningShift.value = shift
|
||
isAssignShiftOpen.value = true
|
||
}
|
||
|
||
function onAddShift() {
|
||
editingShift.value = null
|
||
isCreateShiftOpen.value = true
|
||
}
|
||
|
||
function onEditSection() {
|
||
isEditSectionOpen.value = true
|
||
}
|
||
|
||
// Status styling
|
||
const statusColor: Record<ShiftStatus, string> = {
|
||
draft: 'default',
|
||
open: 'info',
|
||
full: 'success',
|
||
in_progress: 'warning',
|
||
completed: 'success',
|
||
cancelled: 'error',
|
||
}
|
||
|
||
const statusLabel: Record<ShiftStatus, string> = {
|
||
draft: 'Concept',
|
||
open: 'Open',
|
||
full: 'Vol',
|
||
in_progress: 'Bezig',
|
||
completed: 'Voltooid',
|
||
cancelled: 'Geannuleerd',
|
||
}
|
||
|
||
function fillRateColor(shift: Shift): string {
|
||
if (shift.is_overbooked)
|
||
return 'warning'
|
||
if (shift.fill_rate >= 80)
|
||
return 'success'
|
||
if (shift.fill_rate >= 40)
|
||
return 'warning'
|
||
|
||
return 'error'
|
||
}
|
||
|
||
// Date formatting
|
||
const dateFormatter = new Intl.DateTimeFormat('nl-NL', {
|
||
weekday: 'short',
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
})
|
||
|
||
function formatDate(iso: string) {
|
||
if (!iso)
|
||
return ''
|
||
|
||
return dateFormatter.format(new Date(iso))
|
||
}
|
||
|
||
// Selected shift for detail panel (resolved from store ID)
|
||
const selectedShift = computed(() => {
|
||
if (!shiftDetailStore.selectedShiftId || !shifts.value)
|
||
return null
|
||
|
||
return shifts.value.find(s => s.id === shiftDetailStore.selectedShiftId) ?? null
|
||
})
|
||
|
||
// Success snackbar
|
||
const showSuccess = ref(false)
|
||
const successMessage = ref('')
|
||
const successColor = ref('success')
|
||
|
||
function onSectionUpdated() {
|
||
successMessage.value = 'Sectie bijgewerkt.'
|
||
successColor.value = 'success'
|
||
showSuccess.value = true
|
||
}
|
||
|
||
function onSectionCreated(payload: { name: string; redirectedToParent: boolean; parentEventName?: string }) {
|
||
if (payload.redirectedToParent) {
|
||
successMessage.value = `Sectie '${payload.name}' aangemaakt op festival-niveau (${payload.parentEventName}). Zichtbaar in alle programmaonderdelen.`
|
||
successColor.value = 'info'
|
||
}
|
||
else {
|
||
successMessage.value = 'Sectie aangemaakt.'
|
||
successColor.value = 'success'
|
||
}
|
||
showSuccess.value = true
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||
<!-- SECTIONS + SHIFTS (two-column layout) -->
|
||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||
<VRow>
|
||
<!-- LEFT COLUMN — Sections list -->
|
||
<VCol
|
||
cols="12"
|
||
md="3"
|
||
style="min-inline-size: 320px; max-inline-size: 380px;"
|
||
>
|
||
<VCard>
|
||
<VCardTitle class="d-flex align-center justify-space-between">
|
||
<div class="d-flex align-center gap-x-2">
|
||
<span>Secties</span>
|
||
|
||
<!-- Contextual help tooltip -->
|
||
<InfoTooltip v-if="isSubEvent && parentEvent">
|
||
<p>
|
||
Sommige secties zijn <strong>festival-breed</strong>: ze zijn bij elk
|
||
programmaonderdeel actief en worden centraal beheerd. Je herkent ze aan
|
||
de festivalnaam achter de sectienaam.
|
||
</p>
|
||
<div class="mt-2 pa-2 bg-surface rounded text-caption">
|
||
<strong>Voorbeeld:</strong> EHBO en Security zijn festival-breed — ze
|
||
staan bij elk programmaonderdeel.
|
||
</div>
|
||
</InfoTooltip>
|
||
<VTooltip
|
||
v-else
|
||
location="bottom"
|
||
max-width="300"
|
||
>
|
||
<template #activator="{ props: tooltipProps }">
|
||
<VIcon
|
||
v-bind="tooltipProps"
|
||
icon="tabler-info-circle"
|
||
size="18"
|
||
class="text-disabled"
|
||
/>
|
||
</template>
|
||
Secties zijn de operationele afdelingen van je evenement (bijv. Bar, Security, Hospitality). Elke sectie heeft eigen shifts en teamleden.
|
||
</VTooltip>
|
||
</div>
|
||
<VBtn
|
||
icon="tabler-plus"
|
||
variant="text"
|
||
size="small"
|
||
@click="isCreateSectionOpen = true"
|
||
/>
|
||
</VCardTitle>
|
||
|
||
<!-- Loading -->
|
||
<VSkeletonLoader
|
||
v-if="sectionsLoading"
|
||
type="list-item@4"
|
||
/>
|
||
|
||
<!-- Empty -->
|
||
<VCardText
|
||
v-else-if="!sections?.length"
|
||
class="text-center text-disabled"
|
||
>
|
||
Geen secties — maak er een aan
|
||
</VCardText>
|
||
|
||
<!-- Section list -->
|
||
<Draggable
|
||
v-else
|
||
v-model="sections"
|
||
item-key="id"
|
||
ghost-class="section-ghost"
|
||
chosen-class="section-chosen"
|
||
drag-class="section-drag"
|
||
:animation="200"
|
||
:delay="100"
|
||
delay-on-touch-only
|
||
direction="vertical"
|
||
class="v-list v-list--nav v-list--density-compact"
|
||
@end="onDragEnd"
|
||
>
|
||
<template #item="{ element }">
|
||
<VListItem
|
||
:active="element.id === activeSectionId"
|
||
color="primary"
|
||
class="section-item"
|
||
@click="activeSectionId = element.id"
|
||
>
|
||
<template #prepend>
|
||
<VIcon
|
||
v-if="element.icon"
|
||
:icon="element.icon"
|
||
size="16"
|
||
class="me-1"
|
||
/>
|
||
</template>
|
||
<VListItemTitle>
|
||
<span>{{ element.name }}</span>
|
||
<span
|
||
v-if="element.type === 'cross_event' && parentEvent"
|
||
class="text-caption text-medium-emphasis ms-1"
|
||
>
|
||
· {{ parentEvent.name }}
|
||
</span>
|
||
</VListItemTitle>
|
||
</VListItem>
|
||
</template>
|
||
</Draggable>
|
||
</VCard>
|
||
</VCol>
|
||
|
||
<!-- RIGHT COLUMN — Shifts for active section -->
|
||
<VCol>
|
||
<!-- No section selected -->
|
||
<VCard
|
||
v-if="!activeSection"
|
||
class="text-center pa-8"
|
||
>
|
||
<VIcon
|
||
icon="tabler-layout-grid"
|
||
size="48"
|
||
class="mb-4 text-disabled"
|
||
/>
|
||
<p class="text-body-1 text-disabled">
|
||
Selecteer een sectie om shifts te beheren
|
||
</p>
|
||
</VCard>
|
||
|
||
<!-- Section selected -->
|
||
<template v-else>
|
||
<!-- Context banner for cross_event sections viewed from sub-event -->
|
||
<VAlert
|
||
v-if="activeSection.type === 'cross_event' && isSubEvent && parentEvent"
|
||
type="info"
|
||
variant="tonal"
|
||
density="compact"
|
||
class="mb-4"
|
||
>
|
||
<div class="d-flex justify-space-between align-center flex-wrap gap-2">
|
||
<span>
|
||
Je bekijkt {{ activeSection.name }} vanuit
|
||
<strong>{{ eventDetail?.name }}</strong>
|
||
</span>
|
||
<RouterLink
|
||
:to="{ name: 'events-id-sections', params: { id: activeSection.event_id } }"
|
||
class="text-caption"
|
||
>
|
||
Bekijk alle diensten
|
||
</RouterLink>
|
||
</div>
|
||
</VAlert>
|
||
|
||
<!-- Header -->
|
||
<VCard class="mb-4">
|
||
<VCardTitle class="d-flex align-center justify-space-between flex-wrap gap-2">
|
||
<div class="d-flex align-center gap-x-2">
|
||
<VIcon
|
||
v-if="activeSection.icon"
|
||
:icon="activeSection.icon"
|
||
size="20"
|
||
/>
|
||
<span>{{ activeSection.name }}</span>
|
||
<VChip
|
||
v-if="activeSection.type === 'cross_event'"
|
||
size="small"
|
||
color="info"
|
||
variant="tonal"
|
||
>
|
||
festival-breed
|
||
</VChip>
|
||
<span
|
||
v-if="activeSection.crew_need"
|
||
class="text-body-2 text-disabled"
|
||
>
|
||
Crew nodig: {{ activeSection.crew_need }}
|
||
</span>
|
||
</div>
|
||
<div class="d-flex align-center gap-x-2">
|
||
<!-- Contextual help tooltip on shifts -->
|
||
<VTooltip
|
||
location="bottom"
|
||
max-width="300"
|
||
>
|
||
<template #activator="{ props: tooltipProps }">
|
||
<VIcon
|
||
v-bind="tooltipProps"
|
||
icon="tabler-info-circle"
|
||
size="18"
|
||
class="text-disabled"
|
||
/>
|
||
</template>
|
||
Een shift is een concrete taak binnen een sectie, gekoppeld aan een time slot. Stel het aantal plekken in en bepaal hoeveel daarvan door vrijwilligers geclaimd kunnen worden.
|
||
</VTooltip>
|
||
|
||
<VBtn
|
||
size="small"
|
||
variant="tonal"
|
||
prepend-icon="tabler-plus"
|
||
@click="onAddShift"
|
||
>
|
||
Shift
|
||
</VBtn>
|
||
<VBtn
|
||
size="small"
|
||
variant="tonal"
|
||
icon="tabler-edit"
|
||
@click="onEditSection"
|
||
/>
|
||
<VBtn
|
||
size="small"
|
||
variant="tonal"
|
||
icon="tabler-trash"
|
||
color="error"
|
||
@click="onDeleteSectionConfirm(activeSection)"
|
||
/>
|
||
</div>
|
||
</VCardTitle>
|
||
</VCard>
|
||
|
||
<!-- Loading shifts -->
|
||
<VSkeletonLoader
|
||
v-if="shiftsLoading"
|
||
type="card@3"
|
||
/>
|
||
|
||
<!-- No shifts -->
|
||
<VCard
|
||
v-else-if="!shifts?.length"
|
||
class="text-center pa-8"
|
||
>
|
||
<VIcon
|
||
icon="tabler-calendar-time"
|
||
size="48"
|
||
class="mb-4 text-disabled"
|
||
/>
|
||
<p class="text-body-1 text-disabled mb-4">
|
||
Nog geen shifts voor deze sectie
|
||
</p>
|
||
<VBtn
|
||
prepend-icon="tabler-plus"
|
||
@click="onAddShift"
|
||
>
|
||
Shift toevoegen
|
||
</VBtn>
|
||
</VCard>
|
||
|
||
<!-- Shifts grouped by time slot -->
|
||
<template v-else>
|
||
<VCard
|
||
v-for="(group, gi) in shiftsByTimeSlot"
|
||
:key="gi"
|
||
class="mb-4"
|
||
>
|
||
<!-- Group header -->
|
||
<VCardTitle class="d-flex align-center justify-space-between">
|
||
<div>
|
||
<span>{{ group.timeSlotName }}</span>
|
||
<span class="text-body-2 text-disabled ms-2">
|
||
{{ formatDate(group.date) }} {{ group.startTime }}–{{ group.endTime }}
|
||
</span>
|
||
</div>
|
||
<span class="text-body-2">
|
||
{{ group.filledSlots }}/{{ group.totalSlots }} ingevuld
|
||
</span>
|
||
</VCardTitle>
|
||
|
||
<VDivider />
|
||
|
||
<!-- Shifts in group -->
|
||
<VList density="compact">
|
||
<VListItem
|
||
v-for="shift in group.shifts"
|
||
:key="shift.id"
|
||
>
|
||
<div class="d-flex align-center gap-x-3 py-1 flex-wrap">
|
||
<!-- Title + lead badge -->
|
||
<div
|
||
class="d-flex align-center gap-x-2"
|
||
style="min-inline-size: 160px;"
|
||
>
|
||
<span class="text-body-1 font-weight-medium">
|
||
{{ shift.title ?? 'Shift' }}
|
||
</span>
|
||
<VChip
|
||
v-if="shift.is_lead_role"
|
||
size="x-small"
|
||
color="warning"
|
||
>
|
||
Hoofdrol
|
||
</VChip>
|
||
</div>
|
||
|
||
<!-- Fill rate -->
|
||
<div
|
||
class="d-flex align-center gap-x-2"
|
||
style="min-inline-size: 160px;"
|
||
>
|
||
<VProgressLinear
|
||
:model-value="shift.is_overbooked ? 100 : shift.fill_rate"
|
||
:color="fillRateColor(shift)"
|
||
height="8"
|
||
rounded
|
||
style="inline-size: 80px;"
|
||
/>
|
||
<span class="text-body-2 text-no-wrap">
|
||
{{ shift.filled_slots }}/{{ shift.slots_total }}
|
||
</span>
|
||
<VIcon
|
||
v-if="shift.is_overbooked"
|
||
icon="tabler-alert-triangle"
|
||
size="16"
|
||
color="warning"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Status -->
|
||
<VChip
|
||
:color="statusColor[shift.status]"
|
||
size="small"
|
||
>
|
||
{{ statusLabel[shift.status] }}
|
||
</VChip>
|
||
<VChip
|
||
v-if="shift.is_overbooked"
|
||
color="warning"
|
||
size="small"
|
||
prepend-icon="tabler-alert-triangle"
|
||
>
|
||
Overbezet
|
||
</VChip>
|
||
|
||
<VSpacer />
|
||
|
||
<!-- Actions -->
|
||
<div class="d-flex gap-x-1">
|
||
<VBtn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
@click="shiftDetailStore.openPanel(shift.id, activeSection!.id)"
|
||
>
|
||
<VIcon size="18">
|
||
tabler-eye
|
||
</VIcon>
|
||
<VTooltip activator="parent">
|
||
Details bekijken
|
||
</VTooltip>
|
||
</VBtn>
|
||
<VBtn
|
||
icon="tabler-user-plus"
|
||
variant="text"
|
||
size="small"
|
||
title="Toewijzen"
|
||
@click="onAssignShift(shift)"
|
||
/>
|
||
<VBtn
|
||
icon="tabler-edit"
|
||
variant="text"
|
||
size="small"
|
||
title="Bewerken"
|
||
@click="onEditShift(shift)"
|
||
/>
|
||
<VBtn
|
||
icon="tabler-trash"
|
||
variant="text"
|
||
size="small"
|
||
color="error"
|
||
title="Verwijderen"
|
||
@click="onDeleteShiftConfirm(shift)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</VListItem>
|
||
</VList>
|
||
</VCard>
|
||
</template>
|
||
</template>
|
||
</VCol>
|
||
</VRow>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||
<!-- DIALOGS -->
|
||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||
<CreateSectionDialog
|
||
v-model="isCreateSectionOpen"
|
||
:event-id="eventId"
|
||
:is-sub-event="isSubEvent"
|
||
:next-sort-order="sections.length"
|
||
@created="onSectionCreated"
|
||
/>
|
||
|
||
<EditSectionDialog
|
||
v-model="isEditSectionOpen"
|
||
:event-id="activeSectionEventId"
|
||
:section="activeSection"
|
||
@updated="onSectionUpdated"
|
||
/>
|
||
|
||
<CreateShiftDialog
|
||
v-if="activeSection"
|
||
v-model="isCreateShiftOpen"
|
||
:event-id="activeSectionEventId"
|
||
:section-id="activeSection.id"
|
||
:section="activeSection"
|
||
:shift="editingShift"
|
||
:is-sub-event="isSubEvent"
|
||
/>
|
||
|
||
<AssignPersonDialog
|
||
v-if="activeSection"
|
||
v-model="isAssignShiftOpen"
|
||
:event-id="activeSectionEventId"
|
||
:section-id="activeSection.id"
|
||
:shift="assigningShift"
|
||
/>
|
||
|
||
<!-- Delete section confirmation -->
|
||
<VDialog
|
||
v-model="isDeleteSectionOpen"
|
||
max-width="400"
|
||
>
|
||
<VCard title="Sectie verwijderen">
|
||
<VCardText>
|
||
Weet je zeker dat je deze sectie en alle bijbehorende shifts wilt verwijderen?
|
||
</VCardText>
|
||
<VCardActions>
|
||
<VSpacer />
|
||
<VBtn
|
||
variant="text"
|
||
@click="isDeleteSectionOpen = false"
|
||
>
|
||
Annuleren
|
||
</VBtn>
|
||
<VBtn
|
||
color="error"
|
||
@click="onDeleteSectionExecute"
|
||
>
|
||
Verwijderen
|
||
</VBtn>
|
||
</VCardActions>
|
||
</VCard>
|
||
</VDialog>
|
||
|
||
<!-- Delete shift confirmation -->
|
||
<VDialog
|
||
v-model="isDeleteShiftOpen"
|
||
max-width="400"
|
||
>
|
||
<VCard title="Shift verwijderen">
|
||
<VCardText>
|
||
Weet je zeker dat je deze shift wilt verwijderen?
|
||
</VCardText>
|
||
<VCardActions>
|
||
<VSpacer />
|
||
<VBtn
|
||
variant="text"
|
||
@click="isDeleteShiftOpen = false"
|
||
>
|
||
Annuleren
|
||
</VBtn>
|
||
<VBtn
|
||
color="error"
|
||
:loading="isDeleting"
|
||
@click="onDeleteShiftExecute"
|
||
>
|
||
Verwijderen
|
||
</VBtn>
|
||
</VCardActions>
|
||
</VCard>
|
||
</VDialog>
|
||
|
||
<!-- Shift detail side panel -->
|
||
<ShiftDetailPanel
|
||
v-model="shiftDetailStore.isOpen"
|
||
:event-id="activeSectionEventId"
|
||
:shift="selectedShift"
|
||
/>
|
||
|
||
<!-- Success snackbar -->
|
||
<VSnackbar
|
||
v-model="showSuccess"
|
||
:color="successColor"
|
||
:timeout="4000"
|
||
>
|
||
{{ successMessage }}
|
||
</VSnackbar>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.section-item {
|
||
cursor: grab;
|
||
}
|
||
|
||
:deep(.section-ghost) {
|
||
opacity: 0.4;
|
||
background: rgb(var(--v-theme-primary), 0.08);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
:deep(.section-chosen) {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
transform: scale(1.02);
|
||
border-radius: 8px;
|
||
background: rgb(var(--v-theme-surface));
|
||
}
|
||
|
||
:deep(.section-drag) {
|
||
cursor: grabbing;
|
||
}
|
||
</style>
|