<template> <c-r-u-d-iterator i18n-key="alsijil.coursebook" :gql-query="gqlQuery" :gql-additional-query-args="gqlQueryArgs" :enable-create="false" :enable-edit="false" :elevated="false" :items-per-page="-1" @lastQuery="lastQuery = $event" ref="iterator" hide-default-footer use-deep-search > <template #additionalActions="{ attrs, on }"> <coursebook-filters v-model="filters" /> </template> <template #default="{ items }"> <v-list-item v-for="day in groupDocsByDay(items)" two-line :key="'day-' + day[0]" > <v-list-item-content :id="'documentation_' + day[0].toISODate()"> <v-subheader class="text-h6">{{ $d(day[0], "dateWithWeekday") }}</v-subheader> <v-list max-width="100%" class="pt-0 mt-n1"> <v-list-item v-for="doc in day.slice(1)" :key="'documentation-' + (doc.oldId || doc.id)" > <documentation-modal :documentation="doc" :affected-query="lastQuery" /> </v-list-item> </v-list> </v-list-item-content> </v-list-item> <date-select-footer :value="date" @click="handleDateMove" /> </template> <template #loading> <CoursebookLoader /> </template> <template #no-data> <CoursebookEmptyMessage icon="mdi-book-off-outline"> {{ $t("alsijil.coursebook.no_data") }} </CoursebookEmptyMessage> </template> <template #no-results> <CoursebookEmptyMessage icon="mdi-book-alert-outline"> {{ $t("alsijil.coursebook.no_results", { search: $refs.iterator.search }) }} </CoursebookEmptyMessage> </template> </c-r-u-d-iterator> </template> <script> import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue"; import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.vue"; import DocumentationModal from "./documentation/DocumentationModal.vue"; import { DateTime } from "luxon"; import { documentationsForCoursebook } from "./coursebook.graphql"; import CoursebookFilters from "./CoursebookFilters.vue"; import CoursebookLoader from "./CoursebookLoader.vue"; import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue"; export default { name: "Coursebook", components: { CoursebookEmptyMessage, CoursebookFilters, CoursebookLoader, CRUDIterator, DateSelectFooter, DocumentationModal, }, props: { filterType: { type: String, required: true, }, objId: { type: [Number, String], required: false, default: null, }, objType: { type: String, required: false, default: null, }, // ISODate date: { type: String, required: false, default: "", }, }, data() { return { gqlQuery: documentationsForCoursebook, lastQuery: null, // Placeholder values while query isn't completed yet groups: [], courses: [], dateStart: null, dateEnd: null, incomplete: false, }; }, computed: { gqlQueryArgs() { return { // Assure courseId is a number own: this.filterType === "all" ? false : true, objId: this.objId ? Number(this.objId) : null, objType: this.objType?.toUpperCase(), dateStart: this.dateStart ?? this.date, dateEnd: this.dateEnd ?? DateTime.fromISO(this.date).plus({ weeks: 1 }).toISODate(), incomplete: !!this.incomplete, }; }, filters: { get() { return { objType: this.objType, objId: this.objId, filterType: this.filterType, incomplete: this.incomplete, }; }, set(selectedFilters) { if (Object.hasOwn(selectedFilters, "incomplete")) { this.incomplete = selectedFilters.incomplete; } else if ( Object.hasOwn(selectedFilters, "filterType") || Object.hasOwn(selectedFilters, "objId") || Object.hasOwn(selectedFilters, "objType") ) { this.$router.push({ name: "alsijil.coursebook_by_type_and_date", params: { filterType: selectedFilters.filterType ? selectedFilters.filterType : this.filterType, objType: selectedFilters.objType, objId: selectedFilters.objId, date: this.date, }, }); } }, }, }, methods: { // => [[dt doc ...] ...] groupDocsByDay(docs) { const byDay = docs.reduce((byDay, doc) => { // This works with dummy. Does actual doc have dateStart instead? const day = DateTime.fromISO(doc.datetimeStart).startOf("day"); byDay[day] ??= [day]; byDay[day].push(doc); return byDay; }, {}); return Object.keys(byDay) .sort() .map((key) => byDay[key]); }, /** * @param {"prev"|"next"} direction */ handleDateMove(direction) { const dateStartParsed = DateTime.fromISO(this.dateStart); const dateEndParsed = DateTime.fromISO(this.dateEnd); const dateParsed = DateTime.fromISO(this.date); const newDate = direction === "prev" ? dateParsed.minus({ days: 1 }) : dateParsed.plus({ days: 1 }); /* TODO: Everything below this line is also needed for when a date is selected via the calendar. → probably move this into a different function and create a second event listener for the input event. */ // Load 3 days into the future/past if (dateStartParsed >= newDate) { this.dateStart = newDate.minus({ days: 3 }).toISODate(); } if (dateEndParsed <= newDate) { this.dateEnd = newDate.plus({ days: 3 }).toISODate(); } this.$router.push({ name: "alsijil.coursebook_by_type_and_date", params: { filterType: this.filterType, objType: this.objType, objId: this.objId, date: newDate.toISODate(), }, }); // Define the function to find the nearest ID const ids = Array.from( document.querySelectorAll("[id^='documentation_']"), ).map((el) => el.id); // TODO: This should only be done after loading the new data const nearestId = this.findNearestId(newDate, direction, ids); this.$vuetify.goTo("#" + nearestId); }, findNearestId(targetDate, direction, ids) { const sortedIds = ids .map((id) => DateTime.fromISO(id.split("_")[1])) .sort((a, b) => a - b); if (direction === "prev") { sortedIds.reverse(); } const nearestId = sortedIds.find((id) => direction === "next" ? id >= targetDate : id <= targetDate, ) || sortedIds[sortedIds.length - 1]; return "documentation_" + nearestId.toISODate(); }, }, mounted() { this.dateStart = this.date; this.dateEnd = DateTime.fromISO(this.dateStart) .plus({ weeks: 1 }) .toISODate(); }, }; </script> <style> .max-width { max-width: 25rem; } </style>