2020-12-14 14:50:37 +01:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import { CoreSites } from '@services/sites';
|
|
|
|
import {
|
|
|
|
AddonCalendar,
|
|
|
|
AddonCalendarDayName,
|
|
|
|
AddonCalendarEvent,
|
|
|
|
AddonCalendarEventBase,
|
|
|
|
AddonCalendarEventToDisplay,
|
|
|
|
AddonCalendarEventType,
|
|
|
|
AddonCalendarGetEventsEvent,
|
|
|
|
AddonCalendarProvider,
|
|
|
|
AddonCalendarWeek,
|
|
|
|
AddonCalendarWeekDay,
|
|
|
|
} from './calendar';
|
|
|
|
import { CoreConfig } from '@services/config';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
|
|
import { CoreCourse } from '@features/course/services/course';
|
|
|
|
import { ContextLevel, CoreConstants } from '@/core/constants';
|
|
|
|
import moment from 'moment';
|
|
|
|
import { makeSingleton } from '@singletons';
|
|
|
|
import { AddonCalendarSyncInvalidateEvent } from './calendar-sync';
|
|
|
|
import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
|
|
|
|
import { CoreCategoryData } from '@features/courses/services/courses';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Context levels enumeration.
|
|
|
|
*/
|
|
|
|
export enum AddonCalendarEventIcons {
|
|
|
|
SITE = 'fas-globe',
|
|
|
|
CATEGORY = 'fas-cubes',
|
|
|
|
COURSE = 'fas-graduation-cap',
|
|
|
|
GROUP = 'fas-users',
|
|
|
|
USER = 'fas-user',
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Service that provides some features regarding lists of courses and categories.
|
|
|
|
*/
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
|
|
export class AddonCalendarHelperProvider {
|
|
|
|
|
|
|
|
protected eventTypeIcons: string[] = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns event icon based on event type.
|
|
|
|
*
|
|
|
|
* @param eventType Type of the event.
|
|
|
|
* @return Event icon.
|
|
|
|
*/
|
|
|
|
getEventIcon(eventType: AddonCalendarEventType): string {
|
|
|
|
if (this.eventTypeIcons.length == 0) {
|
2021-03-02 11:41:04 +01:00
|
|
|
CoreUtils.enumKeys(AddonCalendarEventType).forEach((name) => {
|
2020-12-14 14:50:37 +01:00
|
|
|
const value = AddonCalendarEventType[name];
|
|
|
|
this.eventTypeIcons[value] = AddonCalendarEventIcons[name];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.eventTypeIcons[eventType] || '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculate some day data based on a list of events for that day.
|
|
|
|
*
|
|
|
|
* @param day Day.
|
|
|
|
* @param events Events.
|
|
|
|
*/
|
|
|
|
calculateDayData(day: AddonCalendarWeekDay, events: AddonCalendarEventToDisplay[]): void {
|
|
|
|
day.hasevents = events.length > 0;
|
|
|
|
day.haslastdayofevent = false;
|
|
|
|
|
|
|
|
const types = {};
|
|
|
|
events.forEach((event) => {
|
|
|
|
types[event.formattedType || event.eventtype] = true;
|
|
|
|
|
|
|
|
if (event.islastday) {
|
|
|
|
day.haslastdayofevent = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
day.calendareventtypes = Object.keys(types) as AddonCalendarEventType[];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if current user can create/edit events.
|
|
|
|
*
|
|
|
|
* @param courseId Course ID. If not defined, site calendar.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved with boolean: whether the user can create events.
|
|
|
|
*/
|
|
|
|
async canEditEvents(courseId?: number, siteId?: string): Promise<boolean> {
|
|
|
|
try {
|
2021-03-02 11:41:04 +01:00
|
|
|
const canEdit = await AddonCalendar.canEditEvents(siteId);
|
2020-12-14 14:50:37 +01:00
|
|
|
if (!canEdit) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
const types = await AddonCalendar.getAllowedEventTypes(courseId, siteId);
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
return Object.keys(types).length > 0;
|
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Classify events into their respective months and days. If an event duration covers more than one day,
|
|
|
|
* it will be included in all the days it lasts.
|
|
|
|
*
|
|
|
|
* @param events Events to classify.
|
|
|
|
* @return Object with the classified events.
|
|
|
|
*/
|
|
|
|
classifyIntoMonths(
|
|
|
|
offlineEvents: AddonCalendarOfflineEventDBRecord[],
|
|
|
|
): { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } {
|
|
|
|
// Format data.
|
|
|
|
const events: AddonCalendarEventToDisplay[] = offlineEvents.map((event) =>
|
2021-03-02 11:41:04 +01:00
|
|
|
AddonCalendarHelper.formatOfflineEventData(event));
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
const result = {};
|
|
|
|
|
|
|
|
events.forEach((event) => {
|
|
|
|
const treatedDay = moment(new Date(event.timestart * 1000));
|
|
|
|
const endDay = moment(new Date((event.timestart + event.timeduration) * 1000));
|
|
|
|
|
|
|
|
// Add the event to all the days it lasts.
|
|
|
|
while (!treatedDay.isAfter(endDay, 'day')) {
|
|
|
|
const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1);
|
|
|
|
const day = treatedDay.date();
|
|
|
|
|
|
|
|
if (!result[monthId]) {
|
|
|
|
result[monthId] = {};
|
|
|
|
}
|
|
|
|
if (!result[monthId][day]) {
|
|
|
|
result[monthId][day] = [];
|
|
|
|
}
|
|
|
|
result[monthId][day].push(event);
|
|
|
|
|
|
|
|
treatedDay.add(1, 'day'); // Treat next day.
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience function to format some event data to be rendered.
|
|
|
|
*
|
|
|
|
* @param event Event to format.
|
|
|
|
*/
|
|
|
|
formatEventData(event: AddonCalendarEvent | AddonCalendarEventBase | AddonCalendarGetEventsEvent): AddonCalendarEventToDisplay {
|
|
|
|
|
|
|
|
const eventFormatted: AddonCalendarEventToDisplay = {
|
|
|
|
id: event.id!,
|
|
|
|
name: event.name,
|
|
|
|
eventtype: event.eventtype,
|
|
|
|
categoryid: event.categoryid,
|
|
|
|
groupid: event.groupid,
|
|
|
|
description: event.description,
|
|
|
|
location: 'location' in event? event.location : undefined,
|
|
|
|
timestart: event.timestart,
|
|
|
|
timeduration: event.timeduration,
|
|
|
|
eventcount: 'eventcount' in event? event.eventcount || 0 : 0,
|
|
|
|
repeatid: event.repeatid || 0,
|
|
|
|
// repeateditall: event.repeateditall,
|
|
|
|
userid: event.userid,
|
|
|
|
timemodified: event.timemodified,
|
|
|
|
eventIcon: this.getEventIcon(event.eventtype),
|
2021-03-02 11:41:04 +01:00
|
|
|
formattedType: AddonCalendar.getEventType(event),
|
2020-12-14 14:50:37 +01:00
|
|
|
modulename: event.modulename,
|
|
|
|
format: 1,
|
|
|
|
visible: 1,
|
|
|
|
offline: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (event.modulename) {
|
2021-03-02 11:41:04 +01:00
|
|
|
eventFormatted.eventIcon = CoreCourse.getModuleIconSrc(event.modulename);
|
2020-12-14 14:50:37 +01:00
|
|
|
eventFormatted.moduleIcon = eventFormatted.eventIcon;
|
2021-04-27 13:14:31 +02:00
|
|
|
eventFormatted.iconTitle = CoreCourse.translateModuleName(event.modulename);
|
2020-12-14 14:50:37 +01:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
eventFormatted.formattedType = AddonCalendar.getEventType(event);
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
// Calculate context.
|
|
|
|
if ('course' in event) {
|
|
|
|
eventFormatted.courseid = event.course?.id;
|
|
|
|
} else if ('courseid' in event) {
|
|
|
|
eventFormatted.courseid = event.courseid;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate context.
|
|
|
|
if ('category' in event) {
|
|
|
|
eventFormatted.categoryid = event.category?.id;
|
|
|
|
} else if ('categoryid' in event) {
|
|
|
|
eventFormatted.categoryid = event.categoryid;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ('canedit' in event) {
|
|
|
|
eventFormatted.canedit = event.canedit;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ('candelete' in event) {
|
|
|
|
eventFormatted.candelete = event.candelete;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.formatEventContext(eventFormatted, eventFormatted.courseid, eventFormatted.categoryid);
|
|
|
|
|
|
|
|
return eventFormatted;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience function to format some event data to be rendered.
|
|
|
|
*
|
|
|
|
* @param e Event to format.
|
|
|
|
*/
|
|
|
|
formatOfflineEventData(event: AddonCalendarOfflineEventDBRecord): AddonCalendarEventToDisplay {
|
|
|
|
|
|
|
|
const eventFormatted: AddonCalendarEventToDisplay = {
|
|
|
|
id: event.id!,
|
|
|
|
name: event.name,
|
|
|
|
timestart: event.timestart,
|
|
|
|
eventtype: event.eventtype,
|
|
|
|
categoryid: event.categoryid,
|
|
|
|
courseid: event.courseid || event.groupcourseid,
|
|
|
|
groupid: event.groupid,
|
|
|
|
description: event.description,
|
|
|
|
location: event.location,
|
|
|
|
duration: event.duration,
|
|
|
|
timedurationuntil: event.timedurationuntil,
|
|
|
|
timedurationminutes: event.timedurationminutes,
|
|
|
|
// repeat: event.repeat,
|
|
|
|
eventcount: event.repeats || 0,
|
|
|
|
repeatid: event.repeatid || 0,
|
|
|
|
// repeateditall: event.repeateditall,
|
|
|
|
userid: event.userid,
|
|
|
|
timemodified: event.timecreated || 0,
|
|
|
|
eventIcon: this.getEventIcon(event.eventtype),
|
|
|
|
formattedType: event.eventtype,
|
|
|
|
format: 1,
|
|
|
|
visible: 1,
|
|
|
|
offline: true,
|
|
|
|
timeduration: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Calculate context.
|
|
|
|
const categoryId = event.categoryid;
|
|
|
|
const courseId = event.courseid || event.groupcourseid;
|
|
|
|
this.formatEventContext(eventFormatted, courseId, categoryId);
|
|
|
|
|
|
|
|
if (eventFormatted.duration == 1) {
|
|
|
|
eventFormatted.timeduration = (event.timedurationuntil || 0) - event.timestart;
|
|
|
|
} else if (eventFormatted.duration == 2) {
|
|
|
|
eventFormatted.timeduration = (event.timedurationminutes || 0) * CoreConstants.SECONDS_MINUTE;
|
|
|
|
} else {
|
|
|
|
eventFormatted.timeduration = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return eventFormatted;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Modifies event data with the context information.
|
|
|
|
*
|
|
|
|
* @param eventFormatted Event formatted to be displayed.
|
|
|
|
* @param courseId Course Id if any.
|
|
|
|
* @param categoryId Category Id if any.
|
|
|
|
*/
|
|
|
|
protected formatEventContext(eventFormatted: AddonCalendarEventToDisplay, courseId?: number, categoryId?: number): void {
|
|
|
|
if (categoryId && categoryId > 0) {
|
|
|
|
eventFormatted.contextLevel = ContextLevel.COURSECAT;
|
|
|
|
eventFormatted.contextInstanceId = categoryId;
|
|
|
|
} else if (courseId && courseId > 0) {
|
|
|
|
eventFormatted.contextLevel = ContextLevel.COURSE;
|
|
|
|
eventFormatted.contextInstanceId = courseId;
|
|
|
|
} else {
|
|
|
|
eventFormatted.contextLevel = ContextLevel.USER;
|
|
|
|
eventFormatted.contextInstanceId = eventFormatted.userid;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get options (name & value) for each allowed event type.
|
|
|
|
*
|
|
|
|
* @param eventTypes Result of getAllowedEventTypes.
|
|
|
|
* @return Options.
|
|
|
|
*/
|
|
|
|
getEventTypeOptions(eventTypes: {[name: string]: boolean}): AddonCalendarEventTypeOption[] {
|
|
|
|
const options: AddonCalendarEventTypeOption[] = [];
|
|
|
|
|
|
|
|
if (eventTypes.user) {
|
|
|
|
options.push({ name: 'core.user', value: AddonCalendarEventType.USER });
|
|
|
|
}
|
|
|
|
if (eventTypes.group) {
|
|
|
|
options.push({ name: 'core.group', value: AddonCalendarEventType.GROUP });
|
|
|
|
}
|
|
|
|
if (eventTypes.course) {
|
|
|
|
options.push({ name: 'core.course', value: AddonCalendarEventType.COURSE });
|
|
|
|
}
|
|
|
|
if (eventTypes.category) {
|
|
|
|
options.push({ name: 'core.category', value: AddonCalendarEventType.CATEGORY });
|
|
|
|
}
|
|
|
|
if (eventTypes.site) {
|
|
|
|
options.push({ name: 'core.site', value: AddonCalendarEventType.SITE });
|
|
|
|
}
|
|
|
|
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the month "id" (year + month).
|
|
|
|
*
|
|
|
|
* @param year Year.
|
|
|
|
* @param month Month.
|
|
|
|
* @return The "id".
|
|
|
|
*/
|
|
|
|
getMonthId(year: number, month: number): string {
|
|
|
|
return year + '#' + month;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get weeks of a month in offline (with no events).
|
|
|
|
*
|
|
|
|
* The result has the same structure than getMonthlyEvents, but it only contains fields that are actually used by the app.
|
|
|
|
*
|
|
|
|
* @param year Year to get.
|
|
|
|
* @param month Month to get.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Promise resolved with the response.
|
|
|
|
*/
|
|
|
|
async getOfflineMonthWeeks(
|
|
|
|
year: number,
|
|
|
|
month: number,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<{ daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] }> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-12-14 14:50:37 +01:00
|
|
|
// Get starting week day user preference, fallback to site configuration.
|
|
|
|
let startWeekDayStr = site.getStoredConfig('calendar_startwday');
|
2021-03-02 11:41:04 +01:00
|
|
|
startWeekDayStr = await CoreConfig.get(AddonCalendarProvider.STARTING_WEEK_DAY, startWeekDayStr);
|
2020-12-14 14:50:37 +01:00
|
|
|
const startWeekDay = parseInt(startWeekDayStr, 10);
|
|
|
|
|
|
|
|
const today = moment();
|
|
|
|
const isCurrentMonth = today.year() == year && today.month() == month - 1;
|
|
|
|
const weeks: Partial<AddonCalendarWeek>[] = [];
|
|
|
|
|
|
|
|
let date = moment({ year, month: month - 1, date: 1 });
|
|
|
|
for (let mday = 1; mday <= date.daysInMonth(); mday++) {
|
|
|
|
date = moment({ year, month: month - 1, date: mday });
|
|
|
|
|
|
|
|
// Add new week and calculate prepadding.
|
|
|
|
if (!weeks.length || date.day() == startWeekDay) {
|
|
|
|
const prepaddingLength = (date.day() - startWeekDay + 7) % 7;
|
|
|
|
const prepadding: number[] = [];
|
|
|
|
for (let i = 0; i < prepaddingLength; i++) {
|
|
|
|
prepadding.push(i);
|
|
|
|
}
|
|
|
|
weeks.push({ prepadding, postpadding: [], days: [] });
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate postpadding of last week.
|
|
|
|
if (mday == date.daysInMonth()) {
|
|
|
|
const postpaddingLength = (startWeekDay - date.day() + 6) % 7;
|
|
|
|
const postpadding: number[] = [];
|
|
|
|
for (let i = 0; i < postpaddingLength; i++) {
|
|
|
|
postpadding.push(i);
|
|
|
|
}
|
|
|
|
weeks[weeks.length - 1].postpadding = postpadding;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add day to current week.
|
|
|
|
weeks[weeks.length - 1].days!.push({
|
|
|
|
events: [],
|
|
|
|
hasevents: false,
|
|
|
|
mday: date.date(),
|
|
|
|
isweekend: date.day() == 0 || date.day() == 6,
|
|
|
|
istoday: isCurrentMonth && today.date() == date.date(),
|
|
|
|
calendareventtypes: [],
|
|
|
|
// Added to match the type. And possibly unused.
|
|
|
|
popovertitle: '',
|
|
|
|
ispast: today.date() > date.date(),
|
|
|
|
seconds: date.seconds(),
|
|
|
|
minutes: date.minutes(),
|
|
|
|
hours: date.hours(),
|
|
|
|
wday: date.weekday(),
|
|
|
|
year: year,
|
|
|
|
yday: date.dayOfYear(),
|
|
|
|
timestamp: date.date(),
|
|
|
|
haslastdayofevent: false,
|
|
|
|
neweventtimestamp: 0,
|
|
|
|
previousperiod: 0, // Previousperiod.
|
|
|
|
nextperiod: 0, // Nextperiod.
|
|
|
|
navigation: '', // Navigation.
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return { weeks, daynames: [{ dayno: startWeekDay }] };
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the data of an event has changed.
|
|
|
|
*
|
|
|
|
* @param data Current data.
|
|
|
|
* @param original Original data.
|
|
|
|
* @return True if data has changed, false otherwise.
|
|
|
|
*/
|
|
|
|
hasEventDataChanged(data: AddonCalendarOfflineEventDBRecord, original?: AddonCalendarOfflineEventDBRecord): boolean {
|
|
|
|
if (!original) {
|
|
|
|
// There is no original data, assume it hasn't changed.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the fields that don't depend on any other.
|
|
|
|
if (data.name != original.name || data.timestart != original.timestart || data.eventtype != original.eventtype ||
|
|
|
|
data.description != original.description || data.location != original.location ||
|
|
|
|
data.duration != original.duration || data.repeat != original.repeat) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check data that depends on eventtype.
|
|
|
|
if ((data.eventtype == AddonCalendarEventType.CATEGORY && data.categoryid != original.categoryid) ||
|
|
|
|
(data.eventtype == AddonCalendarEventType.COURSE && data.courseid != original.courseid) ||
|
|
|
|
(data.eventtype == AddonCalendarEventType.GROUP && data.groupcourseid != original.groupcourseid &&
|
|
|
|
data.groupid != original.groupid)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check data that depends on duration.
|
|
|
|
if ((data.duration == 1 && data.timedurationuntil != original.timedurationuntil) ||
|
|
|
|
(data.duration == 2 && data.timedurationminutes != original.timedurationminutes)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data.repeat && data.repeats != original.repeats) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filter events to be shown on the events list.
|
|
|
|
*
|
|
|
|
* @param events Events without filtering.
|
|
|
|
* @param filter Filter from popover.
|
|
|
|
* @param categories Categories indexed by ID.
|
|
|
|
* @return Filtered events.
|
|
|
|
*/
|
|
|
|
getFilteredEvents(
|
|
|
|
events: AddonCalendarEventToDisplay[],
|
|
|
|
filter: AddonCalendarFilter,
|
|
|
|
categories: { [id: number]: CoreCategoryData },
|
|
|
|
): AddonCalendarEventToDisplay[] {
|
|
|
|
// Do not filter.
|
|
|
|
if (!filter.filtered) {
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
|
|
|
const courseId = filter.courseId ? Number(filter.courseId) : undefined;
|
|
|
|
|
|
|
|
if (!courseId || courseId < 0) {
|
|
|
|
// Filter only by type.
|
|
|
|
return events.filter((event) => filter[event.formattedType]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const categoryId = filter.categoryId ? Number(filter.categoryId) : undefined;
|
|
|
|
|
2021-05-13 14:12:42 +02:00
|
|
|
return events.filter((event) => filter[event.formattedType] &&
|
2020-12-14 14:50:37 +01:00
|
|
|
this.shouldDisplayEvent(event, categories, courseId, categoryId));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if an event should be displayed based on the filter.
|
|
|
|
*
|
|
|
|
* @param event Event object.
|
|
|
|
* @param courseId Course ID to filter.
|
|
|
|
* @param categoryId Category ID the course belongs to.
|
|
|
|
* @param categories Categories indexed by ID.
|
|
|
|
* @return Whether it should be displayed.
|
|
|
|
*/
|
|
|
|
protected shouldDisplayEvent(
|
|
|
|
event: AddonCalendarEventToDisplay,
|
|
|
|
categories: { [id: number]: CoreCategoryData },
|
|
|
|
courseId: number,
|
|
|
|
categoryId?: number,
|
|
|
|
): boolean {
|
|
|
|
if (event.eventtype == 'user' || event.eventtype == 'site') {
|
|
|
|
// User or site event, display it.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.eventtype == 'category' && categories) {
|
|
|
|
if (!event.categoryid || !Object.keys(categories).length) {
|
|
|
|
// We can't tell if the course belongs to the category, display them all.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.categoryid == categoryId) {
|
|
|
|
// The event is in the same category as the course, display it.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check parent categories.
|
|
|
|
let category = categories[categoryId!];
|
|
|
|
while (category) {
|
|
|
|
if (!category.parent) {
|
|
|
|
// Category doesn't have parent, stop.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.categoryid == category.parent) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
category = categories[category.parent];
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const eventCourse = (event.course && event.course.id) || event.courseid;
|
|
|
|
|
|
|
|
// Show the event if it is from site home or if it matches the selected course.
|
2021-03-02 11:41:04 +01:00
|
|
|
return !!eventCourse && (eventCourse == CoreSites.getCurrentSiteHomeId() || eventCourse == courseId);
|
2020-12-14 14:50:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Refresh the month & day for several created/edited/deleted events, and invalidate the months & days
|
|
|
|
* for their repeated events if needed.
|
|
|
|
*
|
|
|
|
* @param events Events that have been touched and number of times each event is repeated.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Resolved when done.
|
|
|
|
*/
|
|
|
|
async refreshAfterChangeEvents(events: AddonCalendarSyncInvalidateEvent[], siteId?: string): Promise<void> {
|
2021-03-02 11:41:04 +01:00
|
|
|
const site = await CoreSites.getSite(siteId);
|
2020-12-14 14:50:37 +01:00
|
|
|
const fetchTimestarts: number[] = [];
|
|
|
|
const invalidateTimestarts: number[] = [];
|
2021-06-02 16:16:22 +02:00
|
|
|
let promises: Promise<unknown>[] = [];
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
// Always fetch upcoming events.
|
2021-03-02 11:41:04 +01:00
|
|
|
promises.push(AddonCalendar.getUpcomingEvents(undefined, undefined, true, site.id));
|
2020-12-14 14:50:37 +01:00
|
|
|
|
2021-06-02 16:16:22 +02:00
|
|
|
promises = promises.concat(events.map(async (eventData) => {
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
if (eventData.repeated <= 1) {
|
|
|
|
// Not repeated.
|
|
|
|
fetchTimestarts.push(eventData.timestart);
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
return AddonCalendar.invalidateEvent(eventData.id);
|
2020-12-14 14:50:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (eventData.repeatid) {
|
|
|
|
// Being edited or deleted.
|
|
|
|
// We need to calculate the days to invalidate because the event date could have changed.
|
|
|
|
// We don't know if the repeated events are before or after this one, invalidate them all.
|
|
|
|
fetchTimestarts.push(eventData.timestart);
|
|
|
|
|
|
|
|
for (let i = 1; i < eventData.repeated; i++) {
|
|
|
|
invalidateTimestarts.push(eventData.timestart + CoreConstants.SECONDS_DAY * 7 * i);
|
|
|
|
invalidateTimestarts.push(eventData.timestart - CoreConstants.SECONDS_DAY * 7 * i);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the repeated events to invalidate them.
|
|
|
|
const repeatedEvents =
|
2021-03-02 11:41:04 +01:00
|
|
|
await AddonCalendar.getLocalEventsByRepeatIdFromLocalDb(eventData.repeatid, site.id);
|
2020-12-14 14:50:37 +01:00
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreUtils.allPromises(repeatedEvents.map((event) =>
|
|
|
|
AddonCalendar.invalidateEvent(event.id!)));
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Being added.
|
|
|
|
let time = eventData.timestart;
|
|
|
|
fetchTimestarts.push(time);
|
|
|
|
|
|
|
|
while (eventData.repeated > 1) {
|
|
|
|
time += CoreConstants.SECONDS_DAY * 7;
|
|
|
|
eventData.repeated--;
|
|
|
|
invalidateTimestarts.push(time);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
try {
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreUtils.allPromisesIgnoringErrors(promises);
|
2020-12-14 14:50:37 +01:00
|
|
|
} finally {
|
|
|
|
const treatedMonths = {};
|
|
|
|
const treatedDays = {};
|
2021-03-02 11:41:04 +01:00
|
|
|
const finalPromises: Promise<unknown>[] =[AddonCalendar.invalidateAllUpcomingEvents()];
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
// Fetch months and days.
|
|
|
|
fetchTimestarts.map((fetchTime) => {
|
|
|
|
const day = moment(new Date(fetchTime * 1000));
|
|
|
|
|
|
|
|
const monthId = this.getMonthId(day.year(), day.month() + 1);
|
|
|
|
if (!treatedMonths[monthId]) {
|
|
|
|
// Month not refetch or invalidated already, do it now.
|
|
|
|
treatedMonths[monthId] = true;
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
finalPromises.push(AddonCalendar.getMonthlyEvents(
|
2020-12-14 14:50:37 +01:00
|
|
|
day.year(),
|
|
|
|
day.month() + 1,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
true,
|
|
|
|
site.id,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
const dayId = monthId + '#' + day.date();
|
|
|
|
if (!treatedDays[dayId]) {
|
|
|
|
// Dat not refetch or invalidated already, do it now.
|
|
|
|
treatedDays[dayId] = true;
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
finalPromises.push(AddonCalendar.getDayEvents(
|
2020-12-14 14:50:37 +01:00
|
|
|
day.year(),
|
|
|
|
day.month() + 1,
|
|
|
|
day.date(),
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
true,
|
|
|
|
site.id,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Invalidate months and days.
|
|
|
|
invalidateTimestarts.map((fetchTime) => {
|
|
|
|
const day = moment(new Date(fetchTime * 1000));
|
|
|
|
|
|
|
|
const monthId = this.getMonthId(day.year(), day.month() + 1);
|
|
|
|
if (!treatedMonths[monthId]) {
|
|
|
|
// Month not refetch or invalidated already, do it now.
|
|
|
|
treatedMonths[monthId] = true;
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
finalPromises.push(AddonCalendar.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id));
|
2020-12-14 14:50:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const dayId = monthId + '#' + day.date();
|
|
|
|
if (!treatedDays[dayId]) {
|
|
|
|
// Dat not refetch or invalidated already, do it now.
|
|
|
|
treatedDays[dayId] = true;
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
finalPromises.push(AddonCalendar.invalidateDayEvents(
|
2020-12-14 14:50:37 +01:00
|
|
|
day.year(),
|
|
|
|
day.month() + 1,
|
|
|
|
day.date(),
|
|
|
|
site.id,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
await CoreUtils.allPromisesIgnoringErrors(finalPromises);
|
2020-12-14 14:50:37 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Refresh the month & day for a created/edited/deleted event, and invalidate the months & days
|
|
|
|
* for their repeated events if needed.
|
|
|
|
*
|
|
|
|
* @param event Event that has been touched.
|
|
|
|
* @param repeated Number of times the event is repeated.
|
|
|
|
* @param siteId Site ID. If not defined, current site.
|
|
|
|
* @return Resolved when done.
|
|
|
|
*/
|
|
|
|
refreshAfterChangeEvent(
|
|
|
|
event: {
|
|
|
|
id?: number;
|
|
|
|
repeatid?: number;
|
|
|
|
timestart: number;
|
|
|
|
},
|
|
|
|
repeated: number,
|
|
|
|
siteId?: string,
|
|
|
|
): Promise<void> {
|
|
|
|
return this.refreshAfterChangeEvents(
|
|
|
|
[{
|
|
|
|
id: event.id!,
|
|
|
|
repeatid: event.repeatid,
|
|
|
|
timestart: event.timestart,
|
|
|
|
repeated: repeated,
|
|
|
|
}],
|
|
|
|
siteId,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sort events by timestart.
|
|
|
|
*
|
|
|
|
* @param events List to sort.
|
|
|
|
*/
|
|
|
|
sortEvents(events: (AddonCalendarEventToDisplay)[]): (AddonCalendarEventToDisplay)[] {
|
|
|
|
return events.sort((a, b) => {
|
|
|
|
if (a.timestart == b.timestart) {
|
|
|
|
return a.timeduration - b.timeduration;
|
|
|
|
}
|
|
|
|
|
|
|
|
return a.timestart - b.timestart;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
export const AddonCalendarHelper = makeSingleton(AddonCalendarHelperProvider);
|
2020-12-14 14:50:37 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculated data for Calendar filtering.
|
|
|
|
*/
|
|
|
|
export type AddonCalendarFilter = {
|
|
|
|
filtered: boolean; // If filter enabled (some filters applied).
|
2021-02-23 15:29:17 +01:00
|
|
|
courseId: number | undefined; // Course Id to filter.
|
2020-12-14 14:50:37 +01:00
|
|
|
categoryId?: number; // Category Id to filter.
|
|
|
|
course: boolean; // Filter to show course events.
|
|
|
|
group: boolean; // Filter to show group events.
|
|
|
|
site: boolean; // Filter to show show site events.
|
|
|
|
user: boolean; // Filter to show user events.
|
|
|
|
category: boolean; // Filter to show category events.
|
|
|
|
};
|
|
|
|
|
|
|
|
export type AddonCalendarEventTypeOption = {
|
|
|
|
name: string;
|
|
|
|
value: AddonCalendarEventType;
|
|
|
|
};
|