Skip to content
Snippets Groups Projects
Commit 6469ef1a authored by Hangzhi Yu's avatar Hangzhi Yu
Browse files

Generalise date select/dynamic loading mechanism from coursebook & use for substitution page

parent 24eb6ee1
No related branches found
No related tags found
1 merge request!329Introduce substitution to do list
Pipeline #181518 canceled
<script setup>
import SubstitutionDay from "./substitutions/SubstitutionDay.vue";
import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
import DateField from "aleksis.core/components/generic/forms/DateField.vue";
import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.vue";
import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
import { DateTime } from "luxon";
import {
amendedLessonsFromAbsences,
patchAmendLessonsWithAmends,
groupsByOwner,
} from "./amendLesson.graphql";
import dateSortedIteratorMixin from "../mixins/dateSortedIteratorMixin.js";
</script>
<template>
......@@ -52,7 +52,7 @@ import {
<template #default="{ items }">
<substitution-day
v-for="{ date, substitutions, first, last } in groupSubstitutionsByDay(items)"
v-for="{ date, itemsOfDay, first, last } in groupItemsByDay(items)"
v-intersect="{
handler: intersectHandler(date, first, last),
options: {
......@@ -61,13 +61,20 @@ import {
},
}"
:date="date"
:substitutions="substitutions"
:substitutions="itemsOfDay"
:lastQuery="lastQuery"
:focus-on-mount="initDate && (initDate.toMillis() === date.toMillis())"
@init="transition"
:key="'day-' + date"
ref="days"
/>
<date-select-footer
:value="currentDate"
@input="gotoDate"
@prev="gotoPrev"
@next="gotoNext"
/>
</template>
</c-r-u-d-iterator>
</template>
......@@ -75,82 +82,34 @@ import {
<script>
export default {
name: "SubstitutionOverview",
mixins: [dateSortedIteratorMixin],
props: {
objId: {
type: [Number, String],
required: false,
default: null,
},
// Next two in ISODate
dateStart: {
type: String,
required: false,
default: "",
},
dateEnd: {
type: String,
required: false,
default: "",
},
/**
* Margin from substitution list to top of viewport in pixels
*/
topMargin: {
type: Number,
required: false,
default: 165,
},
},
data() {
return {
gqlQuery: amendedLessonsFromAbsences,
gqlPatchMutation: patchAmendLessonsWithAmends,
lastQuery: null,
groups: [],
initDate: false,
ready: false,
};
},
methods: {
groupSubstitutionsByDay(substitutions) {
// => {dt: {date: dt, substitutions: doc ...} ...}
const substitutionsByDay = substitutions.reduce((byDay, substitution) => {
const day = DateTime.fromISO(substitution.datetimeStart).startOf("day");
byDay[day] ??= {date: day, substitutions: []};
byDay[day].substitutions.push(substitution);
return byDay;
}, {});
// => [{date: dt, substitutions: doc ..., idx: idx, lastIdx: last-idx} ...]
// sorting is necessary since backend can send substitutions unordered
return Object.keys(substitutionsByDay)
.sort()
.map((key, idx, {length}) => {
const day = substitutionsByDay[key];
day.first = idx === 0;
const lastIdx = length - 1;
day.last = idx === lastIdx;
return day;
});
},
intersectHandler(date, first, last) {
console.log("intersect", date, first, last);
},
transition() {
this.initDate = false
this.ready = true
},
gqlGetPatchData(item) {
return { id: item.id, teachers: item.teachers };
},
changeSelection(selection) {
this.$router.push({
name: "chronos.substitutionOvervievByTypeAndDate",
name: "chronos.substitutionOverview",
params: {
objId: selection.id,
dateStart: this.dateStart,
dateEnd: this.dateEnd,
},
hash: this.$route.hash,
});
this.resetDate();
},
},
computed: {
......
......@@ -41,14 +41,11 @@ export default {
component: () => import("./components/SubstitutionOverview.vue"),
redirect: (to) => {
return {
name: "chronos.substitutionOverviewByTypeAndDate",
params: {
dateStart: DateTime.now().toISODate(),
dateEnd: DateTime.now().plus({ weeks: 1 }).toISODate(),
},
name: "chronos.substitutionOverview",
hash: "#" + DateTime.now().toISODate(),
};
},
name: "chronos.substitutionOverview",
name: "chronos.substitutionOverviewLanding",
props: true,
meta: {
inMenu: true,
......@@ -60,9 +57,9 @@ export default {
},
children: [
{
path: ":dateStart(\\d\\d\\d\\d-\\d\\d-\\d\\d)/:dateEnd(\\d\\d\\d\\d-\\d\\d-\\d\\d)/:objId(\\d+)?/",
path: ":objId(\\d+)?/",
component: () => import("./components/SubstitutionOverview.vue"),
name: "chronos.substitutionOverviewByTypeAndDate",
name: "chronos.substitutionOverview",
meta: {
titleKey: "chronos.substitutions.overview.menu_title",
toolbarTitle: "chronos.substitutions.overview.menu_title",
......
import { DateTime } from "luxon";
export default {
props: {
/**
* Number of consecutive days to load at once
* This number of days is initially loaded and loaded
* incrementally while scrolling.
*/
dayIncrement: {
type: Number,
required: false,
default: 7,
},
/**
* Margin from list to top of viewport in pixels
*/
topMargin: {
type: Number,
required: false,
default: 165,
},
},
data() {
return {
lastQuery: null,
dateStart: "",
dateEnd: "",
ready: false,
initDate: false,
currentDate: "",
hashUpdater: false,
};
},
methods: {
resetDate(toDate) {
// Assure current date
console.log('Resetting date range', this.$route.hash);
this.currentDate = toDate || this.$route.hash?.substring(1);
if (!this.currentDate) {
console.log('Set default date');
this.setDate(DateTime.now().toISODate());
}
const date = DateTime.fromISO(this.currentDate);
this.initDate = date;
this.dateStart = date.minus({ days: this.dayIncrement }).toISODate();
this.dateEnd = date.plus({ days: this.dayIncrement }).toISODate();
},
transition() {
this.initDate = false
this.ready = true
},
groupItemsByDay(items) {
// => {dt: {date: dt, itemsOfDay: doc ...} ...}
const itemsByDay = items.reduce((byDay, item) => {
// This works with dummy. Does actual doc have dateStart instead?
const day = DateTime.fromISO(item.datetimeStart).startOf("day");
byDay[day] ??= {date: day, itemsOfDay: []};
byDay[day].itemsOfDay.push(item);
return byDay;
}, {});
// => [{date: dt, itemsOfDay: doc ..., idx: idx, lastIdx: last-idx} ...]
// sorting is necessary since backend can send items unordered
return Object.keys(itemsByDay)
.sort()
.map((key, idx, {length}) => {
const day = itemsByDay[key];
day.first = idx === 0;
const lastIdx = length - 1;
day.last = idx === lastIdx;
return day;
});
},
fetchMore(from, to, then) {
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('Received more');
then();
return { items: previousResult.items.concat(fetchMoreResult.items) };
}
});
},
setDate(date) {
this.currentDate = date;
if (!this.hashUpdater) {
this.hashUpdater = window.requestIdleCallback(() => {
if (!(this.$route.hash.substring(1) === this.currentDate)) {
this.$router.replace({ hash: this.currentDate })
}
this.hashUpdater = false;
});
}
},
fixScrollPos(height, top) {
this.$nextTick(() => {
if (height < document.documentElement.scrollHeight) {
document.documentElement.scrollTop = document.documentElement.scrollHeight - height + top;
this.ready = true;
} else {
// Update top, could have changed in the meantime.
this.fixScrollPos(height, document.documentElement.scrollTop);
}
});
},
intersectHandler(date, first, last) {
let once = true;
return (entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
if ((entry.boundingClientRect.top <= this.topMargin) || first) {
console.log('@ ', date.toISODate());
this.setDate(date.toISODate());
}
if (once && this.ready && first) {
console.log('load up', date.toISODate());
this.ready = false;
this.fetchMore(date.minus({ days: this.dayIncrement }).toISODate(),
date.minus({ days: 1 }).toISODate(),
() => {
this.fixScrollPos(document.documentElement.scrollHeight,
document.documentElement.scrollTop);
});
once = false;
} else if (once && this.ready && last) {
console.log('load down', date.toISODate());
this.ready = false;
this.fetchMore(date.plus({ days: 1 }).toISODate(),
date.plus({ days: this.dayIncrement }).toISODate(),
() => { this.ready = true });
once = false;
}
}
};
},
// Improve me?
// The navigation logic could be a bit simpler if the current days
// where known as a sorted array (= result of groupItemsByDay) But
// then the list would need its own component and this gets rather
// complicated. Then the calendar could also show the present days
// / gray out the missing.
//
// Next two: arg date is ts object
findPrev(date) {
return this.$refs.days
.map((day) => day.date)
.sort()
.reverse()
.find((date2) => date2 < date);
},
findNext(date) {
return this.$refs.days
.map((day) => day.date)
.sort()
.find((date2) => date2 > date);
},
gotoDate(date) {
const present = this.$refs.days
.find((day) => day.date.toISODate() === date);
if (present) {
// React immediatly -> smoother navigation
// Also intersect handler does not always react to scrollIntoView
this.setDate(date);
present.focus("smooth");
} else if (!this.findPrev(DateTime.fromISO(date)) || !this.findNext(DateTime.fromISO(date))) {
this.resetDate(date);
}
},
gotoPrev() {
const prev = this.findPrev(DateTime.fromISO(this.currentDate));
if (prev) {
this.gotoDate(prev.toISODate());
}
},
gotoNext() {
const next = this.findNext(DateTime.fromISO(this.currentDate));
if (next) {
this.gotoDate(next.toISODate());
}
},
},
created() {
this.resetDate();
},
};
......@@ -211,7 +211,9 @@ class Query(graphene.ObjectType):
and not info.context.user.has_perm(
"chronos.manage_substitutions_for_group_rule", Group.objects.get(id=obj_id)
)
) or (not obj_id and not info.context.user.has_perm("chronos.view_substitution_overview_rule")):
) or (
not obj_id and not info.context.user.has_perm("chronos.view_substitution_overview_rule")
):
raise PermissionDenied()
return LessonEvent.get_for_substitution_overview(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment