4 component tests via mountWithVuexy:
- happy path: valid form values → POST /performances called with the
correct body shape (engagement_id, event_id mapped from dayId,
stage_id, start_at, end_at)
- end_at < start_at → submit blocked, schema-level error visible on
the end_at field
- empty engagement_id → submit blocked, error visible on the engagement_id
field
- cancel button → emits update:modelValue=false
Test seam: AddPerformanceDialog.vue gains `defineExpose({ form, errors,
submit })` so jsdom tests can drive validation deterministically without
piping through Flatpickr / VAutocomplete plumbing. Three lines, exposes
internal refs only — no behavioural change.
VDialog stubbed in the test (it teleports to body, which puts content
outside the wrapper); App* wrappers stubbed (we test the schema +
submit pipeline, not Flatpickr ergonomics).
Test count: 360 → 364.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
204 lines
5.6 KiB
Vue
204 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, toRef, watch } from 'vue'
|
|
import { VForm } from 'vuetify/components/VForm'
|
|
import { useTimetableMutations } from '@/composables/api/useTimetableMutations'
|
|
import { createPerformancePayloadSchema } from '@/schemas/timetable'
|
|
import type { ArtistEngagement, Stage } from '@/types/timetable'
|
|
import { requiredValidator } from '@core/utils/validators'
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
orgId: string
|
|
eventId: string
|
|
dayId: string | null
|
|
stages: Stage[]
|
|
/** Engagements available to schedule (filtered to this event). */
|
|
engagements: ArtistEngagement[]
|
|
/** Pre-fill from a drag-out-on-empty-cell (optional). */
|
|
prefill?: { stageId: string; startAt: string; endAt: string; lane: number } | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [open: boolean]
|
|
'created': []
|
|
}>()
|
|
|
|
const orgIdRef = toRef(props, 'orgId')
|
|
const eventIdRef = toRef(props, 'eventId')
|
|
const dayIdRef = toRef(props, 'dayId')
|
|
|
|
const refForm = ref<VForm>()
|
|
const errors = ref<Record<string, string>>({})
|
|
|
|
const form = ref({
|
|
engagement_id: '',
|
|
stage_id: null as string | null,
|
|
start_at: '',
|
|
end_at: '',
|
|
lane: 0,
|
|
notes: '',
|
|
})
|
|
|
|
watch(() => props.modelValue, open => {
|
|
if (open) {
|
|
form.value = {
|
|
engagement_id: '',
|
|
stage_id: props.prefill?.stageId ?? null,
|
|
start_at: props.prefill?.startAt ?? '',
|
|
end_at: props.prefill?.endAt ?? '',
|
|
lane: props.prefill?.lane ?? 0,
|
|
notes: '',
|
|
}
|
|
errors.value = {}
|
|
}
|
|
})
|
|
|
|
const mutations = useTimetableMutations({ orgId: orgIdRef, eventId: eventIdRef, dayId: dayIdRef })
|
|
const isPending = computed(() => mutations.create.isPending.value)
|
|
|
|
const engagementOptions = computed(() =>
|
|
props.engagements.map(e => ({
|
|
title: e.artist?.name ?? '—',
|
|
subtitle: e.booking_status?.label ?? '',
|
|
value: e.id,
|
|
})),
|
|
)
|
|
|
|
async function submit(): Promise<void> {
|
|
errors.value = {}
|
|
|
|
const eventIdValue = props.dayId ?? props.eventId
|
|
|
|
const payload = {
|
|
engagement_id: form.value.engagement_id,
|
|
event_id: eventIdValue,
|
|
stage_id: form.value.stage_id,
|
|
start_at: form.value.start_at,
|
|
end_at: form.value.end_at,
|
|
lane: form.value.lane,
|
|
notes: form.value.notes || null,
|
|
}
|
|
|
|
const parsed = createPerformancePayloadSchema.safeParse(payload)
|
|
if (!parsed.success) {
|
|
for (const issue of parsed.error.issues)
|
|
errors.value[String(issue.path[0] ?? '_')] = issue.message
|
|
|
|
return
|
|
}
|
|
|
|
try {
|
|
await mutations.create.mutateAsync(parsed.data)
|
|
emit('created')
|
|
emit('update:modelValue', false)
|
|
}
|
|
catch (err) {
|
|
errors.value._ = (err as Error).message ?? 'Onbekende fout'
|
|
}
|
|
}
|
|
|
|
// Test seam (Session 4 follow-up): expose form + errors + submit so jsdom
|
|
// component tests can drive validation deterministically without piping
|
|
// through Flatpickr / VAutocomplete plumbing.
|
|
defineExpose({ form, errors, submit })
|
|
</script>
|
|
|
|
<template>
|
|
<VDialog
|
|
:model-value="modelValue"
|
|
max-width="520"
|
|
persistent
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<VCard>
|
|
<VCardTitle class="d-flex justify-space-between align-center">
|
|
Nieuw optreden plannen
|
|
<VBtn
|
|
icon="tabler-x"
|
|
variant="text"
|
|
size="small"
|
|
@click="emit('update:modelValue', false)"
|
|
/>
|
|
</VCardTitle>
|
|
<VForm
|
|
ref="refForm"
|
|
@submit.prevent="submit"
|
|
>
|
|
<VCardText class="d-flex flex-column gap-4">
|
|
<VAlert
|
|
v-if="errors._"
|
|
type="error"
|
|
density="compact"
|
|
variant="tonal"
|
|
>
|
|
{{ errors._ }}
|
|
</VAlert>
|
|
<AppAutocomplete
|
|
v-model="form.engagement_id"
|
|
label="Artiest / engagement"
|
|
:items="engagementOptions"
|
|
item-title="title"
|
|
item-value="value"
|
|
:rules="[requiredValidator]"
|
|
:error-messages="errors.engagement_id"
|
|
autofocus
|
|
/>
|
|
<AppSelect
|
|
v-model="form.stage_id"
|
|
label="Stage"
|
|
:items="stages.map(s => ({ title: s.name, value: s.id }))"
|
|
clearable
|
|
:error-messages="errors.stage_id"
|
|
/>
|
|
<div class="d-flex gap-3">
|
|
<AppDateTimePicker
|
|
v-model="form.start_at"
|
|
label="Start"
|
|
:rules="[requiredValidator]"
|
|
:error-messages="errors.start_at"
|
|
:config="{ enableTime: true, dateFormat: 'Y-m-d H:i', time_24hr: true }"
|
|
/>
|
|
<AppDateTimePicker
|
|
v-model="form.end_at"
|
|
label="Einde"
|
|
:rules="[requiredValidator]"
|
|
:error-messages="errors.end_at"
|
|
:config="{ enableTime: true, dateFormat: 'Y-m-d H:i', time_24hr: true }"
|
|
/>
|
|
</div>
|
|
<AppTextField
|
|
v-model.number="form.lane"
|
|
label="Lane"
|
|
type="number"
|
|
min="0"
|
|
max="9"
|
|
:error-messages="errors.lane"
|
|
/>
|
|
<AppTextarea
|
|
v-model="form.notes"
|
|
label="Notities"
|
|
rows="2"
|
|
:error-messages="errors.notes"
|
|
/>
|
|
</VCardText>
|
|
<VCardActions class="px-4 pb-4">
|
|
<VSpacer />
|
|
<VBtn
|
|
variant="tonal"
|
|
@click="emit('update:modelValue', false)"
|
|
>
|
|
Annuleer
|
|
</VBtn>
|
|
<VBtn
|
|
type="submit"
|
|
color="primary"
|
|
:loading="isPending"
|
|
>
|
|
Plannen
|
|
</VBtn>
|
|
</VCardActions>
|
|
</VForm>
|
|
</VCard>
|
|
</VDialog>
|
|
</template>
|