Files
crewli/apps/app/src/components/sections/CreateShiftDialog.vue
2026-04-10 14:03:02 +02:00

343 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import { useCreateShift, useUpdateShift } from '@/composables/api/useShifts'
import { useTimeSlotList } from '@/composables/api/useTimeSlots'
import { requiredValidator } from '@core/utils/validators'
import type { Shift, ShiftStatus } from '@/types/section'
const props = withDefaults(defineProps<{
eventId: string
sectionId: string
shift?: Shift | null
isSubEvent?: boolean
}>(), {
isSubEvent: false,
})
const emit = defineEmits<{
openTimeSlots: []
}>()
const modelValue = defineModel<boolean>({ required: true })
const eventIdRef = computed(() => props.eventId)
const sectionIdRef = computed(() => props.sectionId)
const isEditing = computed(() => !!props.shift)
const isSubEventRef = computed(() => props.isSubEvent)
const { data: timeSlots } = useTimeSlotList(eventIdRef, { includeParent: isSubEventRef })
const { mutate: createShift, isPending: isCreating } = useCreateShift(eventIdRef, sectionIdRef)
const { mutate: updateShift, isPending: isUpdating } = useUpdateShift(eventIdRef, sectionIdRef)
const isPending = computed(() => isCreating.value || isUpdating.value)
const form = ref({
time_slot_id: '',
title: '',
report_time: '',
actual_start_time: '',
actual_end_time: '',
slots_total: 1,
slots_open_for_claiming: 0,
is_lead_role: false,
allow_overlap: false,
instructions: '',
status: 'draft' as ShiftStatus,
})
const errors = ref<Record<string, string>>({})
const refVForm = ref<VForm>()
// Populate form when editing
watch(
() => props.shift,
(shift) => {
if (shift) {
form.value = {
time_slot_id: shift.time_slot_id,
title: shift.title ?? '',
report_time: shift.report_time ?? '',
actual_start_time: shift.actual_start_time ?? '',
actual_end_time: shift.actual_end_time ?? '',
slots_total: shift.slots_total,
slots_open_for_claiming: shift.slots_open_for_claiming,
is_lead_role: shift.is_lead_role,
allow_overlap: shift.allow_overlap,
instructions: shift.instructions ?? '',
status: shift.status,
}
}
},
{ immediate: true },
)
const timeSlotItems = computed(() => {
if (!timeSlots.value?.length) return []
const hasFestival = timeSlots.value.some(ts => ts.source === 'festival')
if (!hasFestival) {
return timeSlots.value.map(ts => ({
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
}))
}
const subEventSlots = timeSlots.value.filter(ts => ts.source !== 'festival')
const festivalSlots = timeSlots.value.filter(ts => ts.source === 'festival')
const items: Array<{ title: string; value?: string; type?: string }> = []
if (subEventSlots.length) {
items.push({ title: subEventSlots[0]?.event_name ?? 'Programma', type: 'subheader' })
items.push(...subEventSlots.map(ts => ({
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
})))
}
if (festivalSlots.length) {
items.push({ title: festivalSlots[0]?.event_name ?? 'Festival', type: 'subheader' })
items.push(...festivalSlots.map(ts => ({
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
})))
}
return items
})
const statusOptions = [
{ title: 'Concept', value: 'draft' },
{ title: 'Open', value: 'open' },
]
function resetForm() {
form.value = {
time_slot_id: '',
title: '',
report_time: '',
actual_start_time: '',
actual_end_time: '',
slots_total: 1,
slots_open_for_claiming: 0,
is_lead_role: false,
allow_overlap: false,
instructions: '',
status: 'draft',
}
errors.value = {}
refVForm.value?.resetValidation()
}
function buildPayload() {
return {
time_slot_id: form.value.time_slot_id,
slots_total: form.value.slots_total,
slots_open_for_claiming: form.value.slots_open_for_claiming,
is_lead_role: form.value.is_lead_role,
allow_overlap: form.value.allow_overlap,
status: form.value.status,
...(form.value.title ? { title: form.value.title } : {}),
...(form.value.report_time ? { report_time: form.value.report_time } : {}),
...(form.value.actual_start_time ? { actual_start_time: form.value.actual_start_time } : {}),
...(form.value.actual_end_time ? { actual_end_time: form.value.actual_end_time } : {}),
...(form.value.instructions ? { instructions: form.value.instructions } : {}),
}
}
function onSubmit() {
refVForm.value?.validate().then(({ valid }) => {
if (!valid) return
errors.value = {}
const callbacks = {
onSuccess: () => {
modelValue.value = false
},
onError: (err: any) => {
const data = err.response?.data
if (data?.errors) {
errors.value = Object.fromEntries(
Object.entries(data.errors).map(([k, v]) => [k, (v as string[])[0]]),
)
}
},
}
if (isEditing.value && props.shift) {
updateShift({ id: props.shift.id, ...buildPayload() }, callbacks)
}
else {
createShift(buildPayload(), callbacks)
}
})
}
</script>
<template>
<VDialog
v-model="modelValue"
max-width="600"
@after-leave="!isEditing && resetForm()"
>
<VCard :title="isEditing ? 'Shift bewerken' : 'Shift toevoegen'">
<VForm
ref="refVForm"
@submit.prevent="onSubmit"
>
<VCardText>
<VRow>
<VCol cols="12">
<AppSelect
v-if="timeSlotItems.length"
v-model="form.time_slot_id"
label="Time Slot"
:items="timeSlotItems"
:rules="[requiredValidator]"
:error-messages="errors.time_slot_id"
/>
<VAlert
v-else
type="info"
variant="tonal"
>
<div class="d-flex align-center justify-space-between flex-wrap gap-2">
<span>Maak eerst een time slot aan</span>
<VBtn
size="small"
variant="text"
color="primary"
prepend-icon="tabler-clock"
@click="emit('openTimeSlots'); modelValue = false"
>
Time Slots beheren
</VBtn>
</div>
</VAlert>
</VCol>
<VCol cols="12">
<AppTextField
v-model="form.title"
label="Titel / Rol"
placeholder="Tapper, Barhoofd, Stage Manager..."
:error-messages="errors.title"
autocomplete="one-time-code"
/>
</VCol>
<VCol
cols="12"
sm="4"
>
<AppTextField
v-model="form.report_time"
label="Aanwezig om (rapport tijd)"
type="time"
:error-messages="errors.report_time"
/>
</VCol>
<VCol
cols="12"
sm="4"
>
<AppTextField
v-model="form.actual_start_time"
label="Afwijkende starttijd"
type="time"
hint="Leeg = time slot tijd"
persistent-hint
:error-messages="errors.actual_start_time"
/>
</VCol>
<VCol
cols="12"
sm="4"
>
<AppTextField
v-model="form.actual_end_time"
label="Afwijkende eindtijd"
type="time"
:error-messages="errors.actual_end_time"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model.number="form.slots_total"
label="Totaal slots"
type="number"
min="1"
:rules="[requiredValidator]"
:error-messages="errors.slots_total"
/>
</VCol>
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model.number="form.slots_open_for_claiming"
label="Open voor claimen"
type="number"
min="0"
:max="form.slots_total"
:rules="[requiredValidator]"
:error-messages="errors.slots_open_for_claiming"
/>
</VCol>
<VCol cols="12">
<VSwitch
v-model="form.is_lead_role"
label="Dit is een leidinggevende rol"
/>
</VCol>
<VCol cols="12">
<VSwitch
v-model="form.allow_overlap"
label="Overlap toegestaan"
hint="Persoon mag meerdere shifts in hetzelfde tijdvak hebben"
persistent-hint
/>
</VCol>
<VCol cols="12">
<AppTextarea
v-model="form.instructions"
label="Instructies"
rows="3"
:error-messages="errors.instructions"
autocomplete="one-time-code"
/>
</VCol>
<VCol cols="12">
<AppSelect
v-model="form.status"
label="Status"
:items="statusOptions"
:error-messages="errors.status"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="text"
@click="modelValue = false"
>
Annuleren
</VBtn>
<VBtn
type="submit"
color="primary"
:loading="isPending"
>
{{ isEditing ? 'Opslaan' : 'Toevoegen' }}
</VBtn>
</VCardActions>
</VForm>
</VCard>
</VDialog>
</template>