studium/frontend/src/components/schedule/SlotFormDialog.vue
Antoine Pelletier 08b69851cd wip
2026-09-29 08:57:04 +02:00

200 lines
6.1 KiB
Vue

<script setup lang="ts">
/** Adds a slot to a course, prefilled with the time range selected in the schedule */
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useAddSlot, useCourses } from '@/services/api/courses'
import { useTags } from '@/services/api/tags'
import {
DAY_END,
DAY_START,
WEEKDAYS,
toApiTime,
toMinutes,
type SlotSelection,
} from '@/utils/timetable'
const props = defineProps<{ selection?: SlotSelection }>()
const open = defineModel<boolean>('open', { required: true })
const { t } = useI18n()
const { data: courses } = useCourses()
const { data: tags } = useTags('slot')
const addSlot = useAddSlot()
const courseId = ref<number>()
const tagId = ref<number>()
const weekday = ref(1)
const start = ref('')
const end = ref('')
const submitted = ref(false)
// Reset the form every time the dialog opens, keeping the last course and tag picked
watch(
open,
(isOpen) => {
if (!isOpen) return
weekday.value = props.selection?.weekday ?? 1
start.value = props.selection?.start ?? '08:15'
end.value = props.selection?.end ?? '10:00'
if (!courses.value?.some((course) => course.id === courseId.value)) {
courseId.value = courses.value?.[0]?.id
}
tagId.value ??= tags.value[0]?.id
submitted.value = false
},
{ immediate: true },
)
// Same rules as the backend, checked here to point at the faulty field
const errors = computed(() => {
const startMinutes = toMinutes(start.value)
const endMinutes = toMinutes(end.value)
return {
course: courseId.value === undefined,
time:
!start.value ||
!end.value ||
startMinutes >= endMinutes ||
startMinutes < toMinutes(DAY_START) ||
endMinutes > toMinutes(DAY_END),
}
})
async function submit() {
submitted.value = true
if (errors.value.course || errors.value.time) return
if (courseId.value === undefined || tagId.value === undefined) return
try {
await addSlot.mutateAsync({
courseId: courseId.value,
body: {
tag_id: tagId.value,
weekday: weekday.value,
start_time: toApiTime(start.value),
end_time: toApiTime(end.value),
},
})
toast.success(t('schedule.slot-created'))
open.value = false
} catch (err) {
toast.error(t('schedule.slot-save-error'), {
description: err instanceof Error ? err.message : undefined,
})
}
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ $t('schedule.new-slot') }}</DialogTitle>
<DialogDescription>{{ $t('schedule.new-slot-description') }}</DialogDescription>
</DialogHeader>
<form id="slot-form" class="grid grid-cols-2 gap-4" novalidate @submit.prevent="submit">
<div class="col-span-2 flex flex-col gap-2">
<Label for="slot-course">{{ $t('schedule.form.course') }}</Label>
<Select v-model="courseId">
<SelectTrigger id="slot-course" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name }}
<span v-if="course.code" class="font-mono text-xs text-muted-foreground">
{{ course.code }}
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex flex-col gap-2">
<Label for="slot-tag">{{ $t('courses.form.tag') }}</Label>
<Select v-model="tagId">
<SelectTrigger id="slot-tag" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="tag in tags" :key="tag.id" :value="tag.id">
<span class="size-2 rounded-full" :style="{ backgroundColor: tag.color }" />
{{ tag.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex flex-col gap-2">
<Label for="slot-weekday">{{ $t('courses.form.weekday') }}</Label>
<Select v-model="weekday">
<SelectTrigger id="slot-weekday" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="day in WEEKDAYS" :key="day" :value="day">
{{ $t(`weekdays.${day}`) }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex flex-col gap-2">
<Label for="slot-start">{{ $t('courses.form.start') }}</Label>
<Input
id="slot-start"
v-model="start"
type="time"
:min="DAY_START"
:max="DAY_END"
step="300"
:aria-invalid="submitted && errors.time"
/>
</div>
<div class="flex flex-col gap-2">
<Label for="slot-end">{{ $t('courses.form.end') }}</Label>
<Input
id="slot-end"
v-model="end"
type="time"
:min="DAY_START"
:max="DAY_END"
step="300"
:aria-invalid="submitted && errors.time"
/>
</div>
<p v-if="submitted && errors.time" class="col-span-2 text-sm text-destructive">
{{ $t('courses.form.errors.slot-time') }}
</p>
</form>
<DialogFooter>
<Button variant="outline" @click="open = false">{{ $t('common.cancel') }}</Button>
<Button type="submit" form="slot-form" :disabled="addSlot.isPending.value">
{{ $t('common.save') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>