From 083ca7cd7ee9b3228443dbe32f71d649820389a8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 5 Jul 2019 10:30:38 +0200 Subject: [PATCH] MOBILE-3021 calendar: Implement day view --- scripts/langindex.json | 2 + .../calendar/addon-calendar-calendar.html | 4 +- .../components/calendar/calendar.scss | 2 +- .../calendar/components/calendar/calendar.ts | 10 + .../upcoming-events/upcoming-events.ts | 2 +- src/addon/calendar/lang/en.json | 2 + src/addon/calendar/pages/day/day.html | 71 ++ src/addon/calendar/pages/day/day.module.ts | 35 + src/addon/calendar/pages/day/day.ts | 607 ++++++++++++++++++ .../calendar/pages/edit-event/edit-event.ts | 4 +- src/addon/calendar/pages/event/event.ts | 1 - src/addon/calendar/pages/index/index.html | 2 +- src/addon/calendar/pages/index/index.ts | 72 +-- src/addon/calendar/pages/list/list.ts | 66 +- src/addon/calendar/providers/calendar.ts | 99 +++ src/assets/lang/en.json | 2 + src/core/courses/providers/helper.ts | 74 ++- 17 files changed, 963 insertions(+), 92 deletions(-) create mode 100644 src/addon/calendar/pages/day/day.html create mode 100644 src/addon/calendar/pages/day/day.module.ts create mode 100644 src/addon/calendar/pages/day/day.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 1d2d82d13..d80355727 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -90,6 +90,8 @@ "addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.confirmeventdelete": "calendar", "addon.calendar.confirmeventseriesdelete": "calendar", + "addon.calendar.daynext": "calendar", + "addon.calendar.dayprev": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.deleteallevents": "calendar", "addon.calendar.deleteevent": "calendar", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 007100baf..7c607ca63 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -31,7 +31,7 @@ -

{{ day.mday }}

+

{{ day.mday }}

@@ -47,7 +47,7 @@ {{event.name}}

-

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

+

{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index c3a20bf9e..af7182c32 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -20,7 +20,7 @@ ion-app.app-root addon-calendar-calendar { } } - .addon-calendar-event { + .addon-calendar-event, .addon-calendar-day-number, .addon-calendar-day-more { cursor: pointer; } diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 203db79e8..2b40b8b3f 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -37,6 +37,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. @Output() onEventClicked = new EventEmitter(); + @Output() onDayClicked = new EventEmitter<{day: number, month: number, year: number}>(); periodName: string; weekDays: any[]; @@ -288,6 +289,15 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.onEventClicked.emit(event.id); } + /** + * A day was clicked. + * + * @param {number} day Day. + */ + dayClicked(day: number): void { + this.onDayClicked.emit({day: day, month: this.month, year: this.year}); + } + /** * Decrease the current month. */ diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index 41751c0d5..f358b5785 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -34,7 +34,6 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, @Input() categoryId: number | string; // Category ID the course belongs to. @Output() onEventClicked = new EventEmitter(); - events = []; // Events (both online and offline). filteredEvents = []; loaded = false; @@ -43,6 +42,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, protected categoriesRetrieved = false; protected categories = {}; protected currentSiteId: string; + protected events = []; // Events (both online and offline). protected onlineEvents = []; protected offlineEvents = []; // Offline events. protected deletedEvents = []; // Events deleted in offline. diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index ba81a9a89..8d19b4559 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -6,6 +6,8 @@ "calendarreminders": "Calendar reminders", "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "daynext": "Next day", + "dayprev": "Previous day", "defaultnotificationtime": "Default notification time", "deleteallevents": "Delete all events", "deleteevent": "Delete event", diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html new file mode 100644 index 000000000..e9b48bd52 --- /dev/null +++ b/src/addon/calendar/pages/day/day.html @@ -0,0 +1,71 @@ + + + {{ 'addon.calendar.calendarevents' | translate }} + + + + + + + + + + + + + + + + + + + + + + +

{{ periodName }}

+
+ + + + + +
+
+ + + + {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }} + + + + + + + + + + +

+

+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+
+
+ + + + + +
diff --git a/src/addon/calendar/pages/day/day.module.ts b/src/addon/calendar/pages/day/day.module.ts new file mode 100644 index 000000000..c4e33dbb9 --- /dev/null +++ b/src/addon/calendar/pages/day/day.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarDayPage } from './day'; + +@NgModule({ + declarations: [ + AddonCalendarDayPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonCalendarDayPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarDayPageModule {} diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts new file mode 100644 index 000000000..924bfa0e7 --- /dev/null +++ b/src/addon/calendar/pages/day/day.ts @@ -0,0 +1,607 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 OFx ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, NgZone } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; +import { Network } from '@ionic-native/network'; +import * as moment from 'moment'; + +/** + * Page that displays the calendar events for a certain day. + */ +@IonicPage({ segment: 'addon-calendar-day' }) +@Component({ + selector: 'page-addon-calendar-day', + templateUrl: 'day.html', +}) +export class AddonCalendarDayPage implements OnInit, OnDestroy { + + protected currentSiteId: string; + protected year: number; + protected month: number; + protected day: number; + protected categories = {}; + protected events = []; // Events (both online and offline). + protected onlineEvents = []; + protected offlineEvents = {}; // Offline events. + protected offlineEditedEventsIds = []; // IDs of events edited in offline. + protected deletedEvents = []; // Events deleted in offline. + protected timeFormat: string; + protected currentMoment: moment.Moment; + + // Observers. + protected newEventObserver: any; + protected discardedObserver: any; + protected editEventObserver: any; + protected deleteEventObserver: any; + protected undeleteEventObserver: any; + protected syncObserver: any; + protected manualSyncObserver: any; + protected onlineObserver: any; + + periodName: string; + filteredEvents = []; + courseId: number; + categoryId: number; + canCreate = false; + courses: any[]; + loaded = false; + hasOffline = false; + isOnline = false; + syncIcon: string; + + constructor(localNotificationsProvider: CoreLocalNotificationsProvider, + navParams: NavParams, + network: Network, + zone: NgZone, + sitesProvider: CoreSitesProvider, + private navCtrl: NavController, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, + private eventsProvider: CoreEventsProvider, + private coursesProvider: CoreCoursesProvider, + private coursesHelper: CoreCoursesHelperProvider, + private appProvider: CoreAppProvider) { + + const now = new Date(); + + this.year = navParams.get('year') || now.getFullYear(); + this.month = navParams.get('month') || (now.getMonth() + 1); + this.day = navParams.get('day') || now.getDate(); + this.courseId = navParams.get('courseId'); + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events added. When an event is added, reload the data. + this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Listen for new event discarded event. When it does, reload the data. + this.discardedObserver = eventsProvider.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 = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized manually but not by this page. + this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { + if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Update the events when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + if (data && !data.sent) { + // Event was deleted in offline. Just mark it as deleted, no need to refresh. + this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline; + this.deletedEvents.push(data.eventId); + } else { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + const found = this.markAsDeleted(data.eventId, false); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + + if (found) { + // The deleted event belongs to current list. Re-calculate "hasOffline". + this.hasOffline = false; + + if (this.events.length != this.onlineEvents.length) { + this.hasOffline = true; + } else { + const event = this.events.find((event) => { + return event.deleted || event.offline; + }); + + this.hasOffline = !!event; + } + } + } + }, this.currentSiteId); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = this.appProvider.isOnline(); + }); + }); + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.currentMoment = moment().year(this.year).month(this.month - 1).day(this.day); + + this.fetchData(true, false); + } + + /** + * Fetch all the data required for the view. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + fetchData(sync?: boolean, showErrors?: boolean): Promise { + + this.syncIcon = 'spinner'; + this.isOnline = this.appProvider.isOnline(); + + const promise = sync ? this.sync() : Promise.resolve(); + + return promise.then(() => { + const promises = []; + + // Load courses for the popover. + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => { + this.courses = data.courses; + this.categoryId = data.categoryId; + })); + + // Get categories. + promises.push(this.loadCategories()); + + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + + // Classify them by month & day. + this.offlineEvents = this.calendarHelper.classifyIntoMonths(events); + + // // Get the IDs of events edited in offline. + const filtered = events.filter((event) => { + return event.id > 0; + }); + this.offlineEditedEventsIds = filtered.map((event) => { + return event.id; + }); + })); + + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + // Get user preferences. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + + return Promise.all(promises); + }).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + this.syncIcon = 'sync'; + }); + } + + /** + * Fetch the events for current day. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getDayEvents(this.year, this.month, this.day).then((result) => { + + // Calculate the period name. We don't use the one in result because it's in server's language. + this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1, this.day).getTime(), + 'core.strftimedaydate'); + + this.onlineEvents = result.events; + this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + // Merge the online events with offline data. + this.events = this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + + // Re-calculate the formatted time so it uses the device date. + this.events.forEach((event) => { + event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + }); + }); + } + + /** + * Merge online events with the offline events of that period. + * + * @return {any[]} Merged events. + */ + protected mergeEvents(): any[] { + this.hasOffline = false; + + if (!this.offlineEditedEventsIds.length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return this.onlineEvents; + } + + const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)], + dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day]; + let result = this.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + + if (event.deleted) { + this.hasOffline = true; + } + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + + if (result.length != this.onlineEvents.length) { + this.hasOffline = true; + } + } + + if (dayOfflineEvents && dayOfflineEvents.length) { + // Add the offline events (either new or edited). + this.hasOffline = true; + result = this.sortEvents(result.concat(dayOfflineEvents)); + } + + return result; + } + + /** + * Filter events to only display events belonging to a certain course. + */ + protected filterEvents(): void { + if (!this.courseId || this.courseId < 0) { + this.filteredEvents = this.events; + } else { + this.filteredEvents = this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories); + }); + } + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.loaded) { + return this.refreshData(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + + /** + * Refresh the data. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + + const promises = []; + + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); + promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + return Promise.all(promises).finally(() => { + return this.fetchData(sync, showErrors); + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Try to synchronize offline events. + * + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + protected sync(showErrors?: boolean): Promise { + return this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + if (result.updated) { + // Trigger a manual sync event. + result.source = 'day'; + result.day = this.day; + result.month = this.month; + result.year = this.year; + + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } + + /** + * Navigate to a particular event. + * + * @param {number} 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 { + this.navCtrl.push('AddonCalendarEventPage', { + id: eventId + }); + } + } + + /** + * Show the context menu. + * + * @param {MouseEvent} event Event. + */ + openCourseFilter(event: MouseEvent): void { + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); + + this.filterEvents(); + } + }); + } + + /** + * Open page to create/edit an event. + * + * @param {number} [eventId] Event ID to edit. + */ + openEdit(eventId?: number): void { + const params: any = {}; + + if (eventId) { + params.eventId = eventId; + } else { + // It's a new event, set the time. + params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000; + } + + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarEditEventPage', params); + } + + /** + * Load next month. + */ + loadNext(): void { + this.increaseDay(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.decreaseDay(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Load previous month. + */ + loadPrevious(): void { + this.decreaseDay(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.increaseDay(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Decrease the current day. + */ + protected decreaseDay(): void { + this.currentMoment.subtract(1, 'day'); + + this.year = this.currentMoment.year(); + this.month = this.currentMoment.month() + 1; + this.day = this.currentMoment.date(); + } + + /** + * Increase the current day. + */ + protected increaseDay(): void { + this.currentMoment.add(1, 'day'); + + this.year = this.currentMoment.year(); + this.month = this.currentMoment.month() + 1; + this.day = this.currentMoment.date(); + } + + /** + * Find an event and mark it as deleted. + * + * @param {number} eventId Event ID. + * @param {boolean} deleted Whether to mark it as deleted or not. + * @return {boolean} Whether the event was found. + */ + protected markAsDeleted(eventId: number, deleted: boolean): boolean { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = deleted; + + return true; + } + + return false; + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); + } +} diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index edda98102..93f8b3499 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -99,6 +99,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent'; + const timestamp = navParams.get('timestamp'); + this.currentSite = sitesProvider.getCurrentSite(); this.errors = { required: this.translate.instant('core.required') @@ -114,7 +116,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.groupControl = this.fb.control(''); this.descriptionControl = this.fb.control(''); - const currentDate = this.timeUtils.toDatetimeFormat(); + const currentDate = this.timeUtils.toDatetimeFormat(timestamp); this.eventForm.addControl('name', this.fb.control('', Validators.required)); this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required)); diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 9f312a3e8..bfb45499c 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -493,7 +493,6 @@ export class AddonCalendarEventPage implements OnDestroy { eventId: this.eventId }, this.sitesProvider.getCurrentSiteId()); - this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); this.event.deleted = false; }).catch((error) => { diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 120c9dc2c..099892df8 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -28,7 +28,7 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 396690427..59088156b 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core'; -import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -25,9 +25,7 @@ import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; -import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { Network } from '@ionic-native/network'; /** @@ -42,11 +40,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent; - protected allCourses = { - id: -1, - fullname: this.translate.instant('core.fulllistofcourses'), - category: -1 - }; protected eventId: number; protected currentSiteId: string; @@ -83,10 +76,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarSync: AddonCalendarSyncProvider, - private translate: TranslateService, private eventsProvider: CoreEventsProvider, - private coursesProvider: CoreCoursesProvider, - private popoverCtrl: PopoverController, + private coursesHelper: CoreCoursesHelperProvider, private appProvider: CoreAppProvider) { this.courseId = navParams.get('courseId'); @@ -206,21 +197,9 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.hasOffline = false; // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; - - if (this.courseId) { - // Search the course to get the category. - const course = this.courses.find((course) => { - return course.id == this.courseId; - }); - - if (course) { - this.categoryId = course.category; - } - } + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => { + this.courses = data.courses; + this.categoryId = data.categoryId; })); // Check if user can create events. @@ -273,9 +252,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { const promises = []; - promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { - return this.fetchData(); - })); + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); // Refresh the sub-component. if (this.showCalendar && this.calendarComponent) { @@ -305,21 +282,35 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { } } + /** + * View a certain day. + * + * @param {any} data Data with the year, month and day. + */ + gotoDay(data: any): void { + const params: any = { + day: data.day, + month: data.month, + year: data.year + }; + + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarDayPage', params); + } + /** * Show the context menu. * * @param {MouseEvent} event Event. */ openCourseFilter(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { - courses: this.courses, - courseId: this.courseId - }); - - popover.onDidDismiss((course) => { - if (course) { - this.courseId = course.id > 0 ? course.id : undefined; - this.categoryId = course.id > 0 ? course.category : undefined; + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; // Course viewed has changed, check if the user can create events for this course calendar. this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { @@ -327,9 +318,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { }); } }); - popover.present({ - ev: event - }); } /** diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 00c6657d4..1c7572e12 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -13,19 +13,18 @@ // limitations under the License. import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core'; -import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; -import { TranslateService } from '@ngx-translate/core'; +import { IonicPage, Content, NavParams, NavController } from 'ionic-angular'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; -import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; import { CoreEventsProvider } from '@providers/events'; import { CoreAppProvider } from '@providers/app'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -50,11 +49,6 @@ export class AddonCalendarListPage implements OnDestroy { protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events. protected categoriesRetrieved = false; protected getCategories = false; - protected allCourses = { - id: -1, - fullname: this.translate.instant('core.fulllistofcourses'), - category: -1 - }; protected categories = {}; protected siteHomeId: number; protected obsDefaultTimeChange: any; @@ -80,18 +74,17 @@ export class AddonCalendarListPage implements OnDestroy { filteredEvents = []; canLoadMore = false; loadMoreError = false; - filter = { - course: this.allCourses - }; + courseId: number; + categoryId: number; canCreate = false; hasOffline = false; isOnline = false; syncIcon: string; // Sync icon. - constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, + constructor(private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, zone: NgZone, - localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, + localNotificationsProvider: CoreLocalNotificationsProvider, private coursesHelper: CoreCoursesHelperProvider, private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, network: Network, private timeUtils: CoreTimeUtilsProvider) { @@ -177,6 +170,7 @@ export class AddonCalendarListPage implements OnDestroy { if (data && !data.sent) { // Event was deleted in offline. Just mark it as deleted, no need to refresh. this.markAsDeleted(data.eventId, true); + this.deletedEvents.push(data.eventId); this.hasOffline = true; } else { // Event deleted, clear the details if needed and refresh the view. @@ -278,19 +272,17 @@ export class AddonCalendarListPage implements OnDestroy { return promise.then(() => { const promises = []; - const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; this.hasOffline = false; - promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { this.canCreate = canEdit; })); // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((result) => { + this.courses = result.courses; + this.categoryId = result.categoryId; if (this.preSelectedCourseId) { this.filter.course = courses.find((course) => { @@ -418,14 +410,13 @@ export class AddonCalendarListPage implements OnDestroy { * @return {any[]} Filtered events. */ protected getFilteredEvents(): any[] { - if (this.filter.course.id == -1) { + if (!this.courseId) { // No filter, display everything. return this.events; } return this.events.filter((event) => { - return this.calendarHelper.shouldDisplayEvent(event, this.filter.course.id, this.filter.course.category, - this.categories); + return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories); }); } @@ -613,28 +604,21 @@ export class AddonCalendarListPage implements OnDestroy { * @param {MouseEvent} event Event. */ openCourseFilter(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { - courses: this.courses, - courseId: this.filter.course.id - }); - popover.onDidDismiss((course) => { - if (course) { - this.filter.course = course; - this.domUtils.scrollToTop(this.content); + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); this.filteredEvents = this.getFilteredEvents(); - // Course viewed has changed, check if the user can create events for this course calendar. - const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; - - this.calendarHelper.canEditEvents(courseId).then((canEdit) => { - this.canCreate = canEdit; - }); + this.domUtils.scrollToTop(this.content); } }); - popover.present({ - ev: event - }); } /** @@ -650,8 +634,8 @@ export class AddonCalendarListPage implements OnDestroy { if (eventId) { params.eventId = eventId; } - if (this.filter.course.id != this.allCourses.id) { - params.courseId = this.filter.course.id; + if (this.courseId) { + params.courseId = this.courseId; } this.splitviewCtrl.push('AddonCalendarEditEventPage', params); diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index c759210db..3bb868034 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -858,6 +858,79 @@ export class AddonCalendarProvider { }); } + /** + * Get calendar events for a certain day. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = { + year: year, + month: month, + day: day + }; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getDayEventsCacheKey(year, month, day, courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_day_view', data, preSets); + }); + } + + /** + * Get prefix cache key for day events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getDayEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'day:'; + } + + /** + * Get prefix cache key for a certain day for day events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @return {string} Prefix Cache key. + */ + protected getDayEventsDayPrefixCacheKey(year: number, month: number, day: number): string { + return this.getDayEventsPrefixCacheKey() + year + ':' + month + ':' + day + ':'; + } + + /** + * Get cache key for day events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getDayEventsCacheKey(year: number, month: number, day: number, courseId?: number, categoryId?: number): string { + return this.getDayEventsDayPrefixCacheKey(year, month, day) + (courseId ? courseId : '') + ':' + + (categoryId ? categoryId : ''); + } + /** * Get a calendar reminders from local Db. * @@ -1120,6 +1193,32 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates day events for all days. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllDayEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates day events for a certain day. + * + * @param {number} year Year. + * @param {number} month Month. + * @param {number} day Day. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDayEvents(year: number, month: number, day: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsDayPrefixCacheKey(year, month, day)); + }); + } + /** * Invalidates events list and all the single events and related info. * diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7dae77a70..b78160d84 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -90,6 +90,8 @@ "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "addon.calendar.daynext": "Next day", + "addon.calendar.dayprev": "Previous day", "addon.calendar.defaultnotificationtime": "Default notification time", "addon.calendar.deleteallevents": "Delete all events", "addon.calendar.deleteevent": "Delete event", diff --git a/src/core/courses/providers/helper.ts b/src/core/courses/providers/helper.ts index 6ff3ba5cf..abd9634fe 100644 --- a/src/core/courses/providers/helper.ts +++ b/src/core/courses/providers/helper.ts @@ -13,9 +13,12 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { PopoverController } from 'ionic-angular'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCoursesProvider } from './courses'; import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; /** * Helper to gather some common courses functions. @@ -23,8 +26,46 @@ import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers @Injectable() export class CoreCoursesHelperProvider { - constructor(private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, - private courseCompletionProvider: AddonCourseCompletionProvider) { } + constructor(private coursesProvider: CoreCoursesProvider, + private utils: CoreUtilsProvider, + private courseCompletionProvider: AddonCourseCompletionProvider, + private translate: TranslateService, + private popoverCtrl: PopoverController) { } + + /** + * Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId. + * + * @param {number} [courseId] Course ID to get the category. + * @return {Promise<{courses: any[], categoryId: number}>} Promise resolved with the list of courses and the category. + */ + getCoursesForPopover(courseId?: number): Promise<{courses: any[], categoryId: number}> { + return this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift({ + id: -1, + fullname: this.translate.instant('core.fulllistofcourses'), + category: -1 + }); + + let categoryId; + + if (courseId) { + // Search the course to get the category. + const course = courses.find((course) => { + return course.id == courseId; + }); + + if (course) { + categoryId = course.category; + } + } + + return { + courses: courses, + categoryId: categoryId + }; + }); + } /** * Given a course object returned by core_enrol_get_users_courses and another one returned by core_course_get_courses_by_field, @@ -174,4 +215,33 @@ export class CoreCoursesHelperProvider { }); }); } + + /** + * Show a context menu to select a course, and return the courseId and categoryId of the selected course (-1 for all courses). + * Returns an empty object if popover closed without picking a course. + * + * @param {MouseEvent} event Click event. + * @param {any[]} courses List of courses, from CoreCoursesHelperProvider.getCoursesForPopover. + * @param {number} courseId The course to select at start. + * @return {Promise<{courseId?: number, categoryId?: number}>} Promise resolved with the course ID and category ID. + */ + selectCourse(event: MouseEvent, courses: any[], courseId: number): Promise<{courseId?: number, categoryId?: number}> { + return new Promise((resolve, reject): any => { + const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { + courses: courses, + courseId: courseId + }); + + popover.onDidDismiss((course) => { + if (course) { + resolve({courseId: course.id, categoryId: course.category}); + } else { + resolve({}); + } + }); + popover.present({ + ev: event + }); + }); + } }