// (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 { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { PopoverController, IonRefresher } from '@ionic/angular'; import { CoreApp } from '@services/app'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { AddonCalendar, AddonCalendarProvider, AddonCalendarUpdatedEventEvent } from '../../services/calendar'; import { AddonCalendarOffline } from '../../services/calendar-offline'; import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper'; import { Network, NgZone } from '@singletons'; import { Subscription } from 'rxjs'; import { CoreEnrolledCourseData } from '@features/courses/services/courses'; import { ActivatedRoute, Params } from '@angular/router'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter'; import { CoreNavHelper } from '@services/nav-helper'; import { CoreLocalNotifications } from '@services/local-notifications'; /** * Page that displays the calendar events. */ @Component({ selector: 'page-addon-calendar-index', templateUrl: 'index.html', }) export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent?: AddonCalendarCalendarComponent; @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent?: AddonCalendarUpcomingEventsComponent; protected eventId?: number; protected currentSiteId: string; // Observers. protected newEventObserver?: CoreEventObserver; protected discardedObserver?: CoreEventObserver; protected editEventObserver?: CoreEventObserver; protected deleteEventObserver?: CoreEventObserver; protected undeleteEventObserver?: CoreEventObserver; protected syncObserver?: CoreEventObserver; protected manualSyncObserver?: CoreEventObserver; protected onlineObserver?: Subscription; protected filterChangedObserver?: CoreEventObserver; year?: number; month?: number; canCreate = false; courses: Partial[] = []; notificationsEnabled = false; loaded = false; hasOffline = false; isOnline = false; syncIcon = 'spinner'; showCalendar = true; loadUpcoming = false; filter: AddonCalendarFilter = { filtered: false, courseId: -1, categoryId: undefined, course: true, group: true, site: true, user: true, category: true, }; constructor( protected popoverCtrl: PopoverController, protected route: ActivatedRoute, ) { this.currentSiteId = CoreSites.instance.getCurrentSiteId(); // Listen for events added. When an event is added, reload the data. this.newEventObserver = CoreEvents.on( AddonCalendarProvider.NEW_EVENT_EVENT, (data: AddonCalendarUpdatedEventEvent) => { if (data && data.eventId) { this.loaded = false; this.refreshData(true, false); } }, this.currentSiteId, ); // Listen for new event discarded event. When it does, reload the data. this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { this.loaded = false; this.refreshData(true, false); }, this.currentSiteId); // Listen for events edited. When an event is edited, reload the data. this.editEventObserver = CoreEvents.on( AddonCalendarProvider.EDIT_EVENT_EVENT, (data: AddonCalendarUpdatedEventEvent) => { if (data && data.eventId) { this.loaded = false; this.refreshData(true, false); } }, this.currentSiteId, ); // Refresh data if calendar events are synchronized automatically. this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => { this.loaded = false; this.refreshData(false, false); }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data: AddonCalendarSyncEvents) => { if (data && data.source != 'index') { this.loaded = false; this.refreshData(false, false); } }, this.currentSiteId); // Update the events when an event is deleted. this.deleteEventObserver = CoreEvents.on(AddonCalendarProvider.DELETED_EVENT_EVENT, () => { this.loaded = false; this.refreshData(false, false); }, this.currentSiteId); // Update the "hasOffline" property if an event deleted in offline is restored. this.undeleteEventObserver = CoreEvents.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, async () => { this.hasOffline = await AddonCalendarOffline.instance.hasOfflineData(); }, this.currentSiteId); this.filterChangedObserver = CoreEvents.on( AddonCalendarProvider.FILTER_CHANGED_EVENT, async (filterData: AddonCalendarFilter) => { this.filter = filterData; // Course viewed has changed, check if the user can create events for this course calendar. this.canCreate = await AddonCalendarHelper.instance.canEditEvents(this.filter['courseId']); }, ); // Refresh online status when changes. this.onlineObserver = Network.instance.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.instance.run(() => { this.isOnline = CoreApp.instance.isOnline(); }); }); } /** * View loaded. */ ngOnInit(): void { this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable(); this.route.queryParams.subscribe(params => { this.eventId = parseInt(params['eventId'], 10) || undefined; this.filter.courseId = parseInt(params['courseId'], 10) || -1; this.year = parseInt(params['year'], 10) || undefined; this.month = parseInt(params['month'], 10) || undefined; this.loadUpcoming = !!params['upcoming']; this.showCalendar = !this.loadUpcoming; this.filter.filtered = this.filter.courseId > 0; if (this.eventId) { // There is an event to load, open the event in a new state. this.gotoEvent(this.eventId); } this.fetchData(true, false); }); } /** * Fetch all the data required for the view. * * @param sync Whether it should try to synchronize offline events. * @param showErrors Whether to show sync errors to the user. * @return Promise resolved when done. */ async fetchData(sync?: boolean, showErrors?: boolean): Promise { this.syncIcon = 'spinner'; this.isOnline = CoreApp.instance.isOnline(); if (sync) { // Try to synchronize offline events. try { const result = await AddonCalendarSync.instance.syncEvents(); if (result.warnings && result.warnings.length) { CoreDomUtils.instance.showErrorModal(result.warnings[0]); } if (result.updated) { // Trigger a manual sync event. result.source = 'index'; CoreEvents.trigger( AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId, ); } } catch (error) { if (showErrors) { CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true); } } } try { const promises: Promise[] = []; this.hasOffline = false; // Load courses for the popover. promises.push(CoreCoursesHelper.instance.getCoursesForPopover(this.filter.courseId).then((data) => { this.courses = data.courses; return; })); // Check if user can create events. promises.push(AddonCalendarHelper.instance.canEditEvents(this.filter.courseId).then((canEdit) => { this.canCreate = canEdit; return; })); // Check if there is offline data. promises.push(AddonCalendarOffline.instance.hasOfflineData().then((hasOffline) => { this.hasOffline = hasOffline; return; })); await Promise.all(promises); } catch (error) { CoreDomUtils.instance.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } this.loaded = true; this.syncIcon = 'fas-sync-alt'; } /** * Refresh the data. * * @param refresher Refresher. * @param done Function to call when done. * @param showErrors Whether to show sync errors to the user. * @return Promise resolved when done. */ async doRefresh(refresher?: CustomEvent, done?: () => void, showErrors?: boolean): Promise { if (!this.loaded) { return; } await this.refreshData(true, showErrors).finally(() => { refresher?.detail.complete(); done && done(); }); } /** * Refresh the data. * * @param sync Whether it should try to synchronize offline events. * @param showErrors Whether to show sync errors to the user. * @param afterChange Whether the refresh is done after an event has changed or has been synced. * @return Promise resolved when done. */ async refreshData(sync = false, showErrors = false): Promise { this.syncIcon = 'spinner'; const promises: Promise[] = []; promises.push(AddonCalendar.instance.invalidateAllowedEventTypes()); // Refresh the sub-component. if (this.showCalendar && this.calendarComponent) { promises.push(this.calendarComponent.refreshData()); } else if (!this.showCalendar && this.upcomingEventsComponent) { promises.push(this.upcomingEventsComponent.refreshData()); } await Promise.all(promises).finally(() => this.fetchData(sync, showErrors)); } /** * Navigate to a particular event. * * @param eventId Event to load. */ gotoEvent(eventId: number): void { if (eventId < 0) { // It's an offline event, go to the edit page. this.openEdit(eventId); } else { CoreNavHelper.instance.goInCurrentMainMenuTab('/calendar/event', { id: eventId, }); } } /** * View a certain day. * * @param data Data with the year, month and day. */ gotoDay(data: {day: number; month: number; year: number}): void { const params: Params = { day: data.day, month: data.month, year: data.year, }; Object.keys(this.filter).forEach((key) => { params[key] = this.filter[key]; }); CoreNavHelper.instance.goInCurrentMainMenuTab('/calendar/day', params); } /** * Show the context menu. * * @param event Event. */ async openFilter(event: MouseEvent): Promise { const popover = await this.popoverCtrl.create({ component: AddonCalendarFilterPopoverComponent, componentProps: { courses: this.courses, filter: this.filter, }, event, }); await popover.present(); } /** * Open page to create/edit an event. * * @param eventId Event ID to edit. */ openEdit(eventId?: number): void { const params: Params = {}; if (eventId) { params.eventId = eventId; } if (this.filter.courseId) { params.courseId = this.filter.courseId; } CoreNavHelper.instance.goInCurrentMainMenuTab('/calendar/edit', params); } /** * Open calendar events settings. */ openSettings(): void { CoreNavHelper.instance.goInCurrentMainMenuTab('/calendar/settings'); } /** * Toogle display: monthly view or upcoming events. */ toggleDisplay(): void { this.showCalendar = !this.showCalendar; if (!this.showCalendar) { this.loadUpcoming = true; } } /** * Page destroyed. */ ngOnDestroy(): void { this.newEventObserver?.off(); this.discardedObserver?.off(); this.editEventObserver?.off(); this.deleteEventObserver?.off(); this.undeleteEventObserver?.off(); this.syncObserver?.off(); this.manualSyncObserver?.off(); this.filterChangedObserver?.off(); this.onlineObserver?.unsubscribe(); } }