diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue index 3dc66363684c38a0ae02fbfc3a5a9992b8562e9c..13fcd9ddcdb8872b96330f7a9748a45bcd42e55e 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue @@ -22,32 +22,46 @@ </div> </v-row> <v-row> - <v-col cols="12" :sm="6" class="pl-0"> + <v-col cols="12" :sm="startPeriods ? 4 : 6" class="pl-0"> <div aria-required="true"> <date-time-field :label="$t('forms.labels.start')" - :max-date="maxStartDate" - :max-time="maxStartTime" - :limit-selectable-range="false" :rules="$rules().required.build()" - :value="startDate" + :value="start.toISO()" @input="handleStartDate" /> </div> </v-col> - <v-col cols="12" :sm="6" class="pr-0"> + <v-col cols="12" :sm="2" v-if="startPeriods" align-self="end"> + <v-select + :label="$t('lesrooster.slot.period')" + :items="startPeriods" + item-text="period" + :value="startSlot" + @input="handleStartSlot" + return-object + /> + </v-col> + <v-col cols="12" :sm="endPeriods ? 4 : 6" class="pr-0"> <div aria-required="true"> <date-time-field :label="$t('forms.labels.end')" - :min-date="minEndDate" - :min-time="minEndTime" - :limit-selectable-range="false" :rules="$rules().required.build()" - :value="endDate" + :value="end.toISO()" @input="handleEndDate" /> </div> </v-col> + <v-col cols="12" :sm="2" v-if="endPeriods" align-self="end"> + <v-select + :label="$t('lesrooster.slot.period')" + :items="endPeriods" + item-text="period" + :value="endSlot" + @input="handleEndSlot" + return-object + /> + </v-col> </v-row> <v-row> <v-text-field @@ -76,7 +90,7 @@ import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue"; import DateTimeField from "aleksis.core/components/generic/forms/DateTimeField.vue"; import PersonField from "aleksis.core/components/generic/forms/PersonField.vue"; -import { gqlPersons } from "./absenceCreation.graphql"; +import { gqlPersons, periodsByDay } from "./absenceCreation.graphql"; import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js"; import { DateTime } from "luxon"; @@ -96,10 +110,14 @@ export default { "comment", "absence-reason", ], - data() { - return { - gqlQuery: gqlPersons, - }; + apollo: { + periodsByDay: { + query: periodsByDay, + result(_) { + this.handleStartDate(this.start.toISO()); + this.handleEndDate(this.end.toISO()); + }, + }, }, props: { persons: { @@ -127,60 +145,98 @@ export default { required: true, }, }, + data() { + return { + gqlQuery: gqlPersons, + startDT: DateTime.fromISO(this.startDate), + endDT: DateTime.fromISO(this.endDate), + startPeriods: false, + endPeriods: false, + startSlot: undefined, + endSlot: undefined, + }; + }, computed: { - maxStartTime() { - // Only if on the same day - const start = DateTime.fromISO(this.startDate); - const end = DateTime.fromISO(this.endDate); - - if (start.day !== end.day) return; - - return end.minus({ minutes: 5 }).toFormat("HH:mm"); - }, - minEndTime() { - // Only if on the same day - const start = DateTime.fromISO(this.startDate); - const end = DateTime.fromISO(this.endDate); - - if (start.day !== end.day) return; - - return start.plus({ minutes: 5 }).toFormat("HH:mm"); + start: { + get() { + return this.startDT; + }, + set(dt) { + this.startDT = dt; + if (dt >= this.end) { + this.end = dt.plus({ minutes: 5 }); + } + this.$emit("start-date", dt.toISO()); + }, }, - maxStartDate() { - const end = DateTime.fromISO(this.endDate); - return end.toISODate(); - }, - minEndDate() { - const start = DateTime.fromISO(this.startDate); - return start.toISODate(); + end: { + get() { + return this.endDT; + }, + set(dt) { + this.endDT = dt; + if (dt <= this.start) { + this.start = dt.minus({ minutes: 5 }); + } + this.$emit("end-date", dt.toISO()); + }, }, }, methods: { - handleStartDate(startDate) { - const parsedStart = DateTime.fromISO(startDate); - const parsedEnd = DateTime.fromISO(this.endDate); - if (parsedStart >= parsedEnd) { - this.$emit( - "end-date", - parsedStart.plus({ minutes: 5 }).toISO({ suppressSeconds: true }), + getPeriodsForWeekday(weekday) { + // Adapt from python conventions + const pythonWeekday = weekday - 1; + let periodsForWeekday = this.periodsByDay.find( + (period) => period.weekday === pythonWeekday, + ); + if (!periodsForWeekday) return false; + return periodsForWeekday.periods; + }, + handleStartDate(date) { + this.start = DateTime.fromISO(date); + + if (this.periodsByDay && this.periodsByDay.length > 0) { + // Select periods for day + this.startPeriods = this.getPeriodsForWeekday(this.start.weekday); + if (!this.startPeriods) return; + // Sync PeriodSelect + const startTime = this.start.toFormat("HH:mm:ss"); + this.startSlot = this.startPeriods.find( + (period) => period.timeStart === startTime, ); } - this.$emit("start-date", startDate); - this.$refs.form.resetValidation(); - this.$refs.form.validate(); }, - handleEndDate(endDate) { - const parsedStart = DateTime.fromISO(this.startDate); - const parsedEnd = DateTime.fromISO(endDate); - if (parsedEnd <= parsedStart) { - this.$emit( - "start-date", - parsedEnd.minus({ minutes: 5 }).toISO({ suppressSeconds: true }), + handleEndDate(date) { + this.end = DateTime.fromISO(date); + + if (this.periodsByDay && this.periodsByDay.length > 0) { + // Select periods for day + this.endPeriods = this.getPeriodsForWeekday(this.end.weekday); + if (!this.endPeriods) return; + // Sync PeriodSelect + const endTime = this.end.toFormat("HH:mm:ss"); + this.endSlot = this.endPeriods.find( + (period) => period.endTime === endTime, ); } - this.$emit("end-date", endDate); - this.$refs.form.resetValidation(); - this.$refs.form.validate(); + }, + handleStartSlot(slot) { + // Sync TimeSelect + const startTime = DateTime.fromISO(slot.timeStart); + this.start = this.start.set({ + hour: startTime.hour, + minute: startTime.minute, + second: startTime.second, + }); + }, + handleEndSlot(slot) { + // Sync TimeSelect + const endTime = DateTime.fromISO(slot.timeEnd); + this.end = this.end.set({ + hour: endTime.hour, + minute: endTime.minute, + second: endTime.second, + }); }, }, }; diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql index 9ca58dd7bc5d58b269210d316c47641a952cdcba..34a22cef6431fd15450e48b40d7ce1d94553fb29 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql @@ -7,6 +7,17 @@ query gqlPersons { } } +query periodsByDay { + periodsByDay: periodsByDay { + weekday + periods { + period + timeStart + timeEnd + } + } +} + query lessonsForPersons($persons: [ID]!, $start: DateTime!, $end: DateTime!) { items: lessonsForPersons(persons: $persons, start: $start, end: $end) { id diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index f337be4f836c8f73383e952c88f79e229613e02a..1614cf0f3000c95915a1fdef92d114b443525e03 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -1,5 +1,7 @@ +from collections import defaultdict from datetime import datetime +from django.apps import apps from django.db.models import BooleanField, ExpressionWrapper, Q import graphene @@ -53,6 +55,17 @@ from .personal_note import ( from .statistics import StatisticsByPersonType +class PeriodType(graphene.ObjectType): + period = graphene.Int() + time_start = graphene.Time() + time_end = graphene.Time() + + +class WeekdayType(graphene.ObjectType): + weekday = graphene.Int() + periods = graphene.List(PeriodType) + + class Query(graphene.ObjectType): documentations_by_course_id = FilterOrderList( DocumentationType, course_id=graphene.ID(required=True) @@ -100,6 +113,8 @@ class Query(graphene.ObjectType): group=graphene.ID(required=True), ) + periods_by_day = graphene.List(WeekdayType) + def resolve_documentations_by_course_id(root, info, course_id, **kwargs): documentations = Documentation.objects.filter( pk__in=Documentation.objects.filter(course_id=course_id) @@ -347,6 +362,35 @@ class Query(graphene.ObjectType): annotate_person_statistics_for_school_term(members, school_term, group=group), info ) + @staticmethod + def resolve_periods_by_day(root, info): + if apps.is_installed("aleksis.apps.lesrooster"): + Slot = apps.get_model("lesrooster", "Slot") + ValidityRange = apps.get_model("lesrooster", "ValidityRange") + slots = ( + Slot.objects.filter( + time_grid__validity_range=ValidityRange.current, period__isnull=False + ) + .order_by("weekday") + .values("weekday", "period", "time_start", "time_end") + ) + # Key by weekday + by_weekday = defaultdict(list) + for slot in slots: + # return nested dicts: {weekday periods { period time_* }} + # sort periods by period + by_weekday[slot["weekday"]].append(slot) + # Nest and sort periods + periods = [] + for weekday, slots in by_weekday.items(): + periods.append( + {"weekday": weekday, "periods": sorted(slots, key=lambda slot: slot["period"])} + ) + + return periods + else: + return [] + class Mutation(graphene.ObjectType): create_or_update_documentations = DocumentationBatchCreateOrUpdateMutation.Field()