<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" @lastQuery="lastQuery = $event" ref="iterator" disable-pagination hide-default-footer use-deep-search > <template #additionalActions="{ attrs, on }"> <coursebook-filters v-model="filters" /> </template> <template #default="{ items }"> <v-list-item v-for="{ date, docs, first, last } in groupDocsByDay(items)" v-intersect="{ handler: onIntersect, options: { threshold: [0, 1] } }" :data-date="date.toISODate()" :data-first="first" :data-last="last" two-line :key="'day-' + date" :id="'documentation_' + date.toISODate()" > <v-list-item-content> <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 docs" :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="$route.hash.substring(1)" /> </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, Interval } 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, }, }, data() { return { gqlQuery: documentationsForCoursebook, lastQuery: null, visible: [], dateStart: "", dateEnd: "", // Placeholder values while query isn't completed yet groups: [], courses: [], incomplete: false, }; }, computed: { // Assertion: Should only fire on page load or selection change. // Resets date range. gqlQueryArgs() { console.log('computing gqlQueryArgs'); return { own: this.filterType === "all" ? false : true, objId: this.objId ? Number(this.objId) : undefined, objType: this.objType?.toUpperCase(), dateStart: this.dateStart, dateEnd: this.dateEnd, 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", params: { filterType: selectedFilters.filterType ? selectedFilters.filterType : this.filterType, objType: selectedFilters.objType, objId: selectedFilters.objId, }, hash: this.$route.hash, }); // computed should not have side effects // but this was actually done before filters was refactored into // its own component this.resetDate(); // might skip query until both set = atomic } }, }, }, methods: { // TODO: Scroll to actual first date! resetDate() { // Assure current date console.log('Resetting date range', this.$route.hash); if (!this.$route.hash) { console.log('Set default date'); this.$router.replace({ hash: DateTime.now().toISODate() }) } // if (date) { // this.$router.replace({ hash: date }) // } const date = DateTime.fromISO(this.$route.hash.substring(1)); // Reset visible this.visible = []; this.dateStart = date.minus({ days: 3 }).toISODate(); this.dateEnd = date.plus({ days: 4 }).toISODate(); }, groupDocsByDay(docs) { // => {dt: [dt doc ...] ...} const docsByDay = docs.reduce((byDay, doc) => { // This works with dummy. Does actual doc have dateStart instead? const day = DateTime.fromISO(doc.datetimeStart).startOf("day"); byDay[day] ??= {date: day, docs: [], first: false, last: false}; byDay[day]['docs'].push(doc); return byDay; }, {}); // => [[dt doc ...] ...] return Object.keys(docsByDay) .sort() .map((key, i, {length}) => { const day = docsByDay[key]; if (i === 0) { day['first'] = true; } else if (i === length - 1) { day['last'] = true; } return day; }); // sorting is necessary since backend can send docs unordered }, // docsByDay: {dt: [dt doc ...] ...} fetchMore(from, to) { console.log('fetching', from, to); this.lastQuery.fetchMore({ variables: { dateStart: from, dateEnd: to, }, // Transform the previous result with new data updateQuery: (previousResult, { fetchMoreResult }) => { console.log('previousResult', previousResult); console.log('fetchMoreResult', fetchMoreResult); return { items: previousResult.items.concat(fetchMoreResult.items) }; } }); }, setDate(date) { this.$router.replace({ hash: date }) }, onIntersect(entries, observer) { const entry = entries[0]; if (entry.isIntersecting) { if (entry.boundingClientRect.top <= 0) { console.log('@', entry.target.dataset.date); this.setDate(entry.target.dataset.date); } // load more if (entry.target.dataset.first) { console.log('load up'); entry.target.dataset.first = false; const date = DateTime.fromISO(entry.target.dataset.date); this.fetchMore(date.minus({ days: 4 }).toISODate(), date.minus({ days: 1 }).toISODate()); } else if (entry.target.dataset.last) { console.log('load down'); entry.target.dataset.last = false; const date = DateTime.fromISO(entry.target.dataset.date); this.fetchMore(date.plus({ days: 1 }).toISODate(), date.plus({ days: 5 }).toISODate()); } } }, }, created() { this.resetDate(); }, }; </script> <style> .max-width { max-width: 25rem; } </style>