diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 04d35dfda..a866c4be2 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -19,7 +19,7 @@ - + @@ -38,11 +38,15 @@
-

- - {{event.name}} -

-

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

+ +

+ + + + {{event.name}} +

+
+

{{ '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 853cff23a..c3a20bf9e 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -13,6 +13,15 @@ ion-app.app-root addon-calendar-calendar { .addon-calendar-day-events { @include text-align('start'); + + ion-icon { + @include margin-horizontal(null, 2px); + font-size: 1em; + } + } + + .addon-calendar-event { + cursor: pointer; } .calendar_event_type { diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 9bb41721e..053863483 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange } from '@angular/core'; +import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -20,6 +20,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** @@ -35,6 +36,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() courseId: number | string; @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(); periodName: string; weekDays: any[]; @@ -45,16 +47,39 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest protected month: number; protected categoriesRetrieved = false; protected categories = {}; + protected currentSiteId: string; + protected offlineEvents: {[monthId: string]: {[day: number]: any[]}} = {}; // Offline events classified in month & day. + protected offlineEditedEventsIds = []; // IDs of events edited in offline. + protected deletedEvents = []; // Events deleted in offline. + + // Observers. + protected undeleteEventObserver: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarOffline: AddonCalendarOfflineProvider, private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private coursesProvider: CoreCoursesProvider) { + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // 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. + this.undeleteEvent(data.eventId); + + // 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); + } + } + }, this.currentSiteId); } /** @@ -76,27 +101,63 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest ngOnChanges(changes: {[name: string]: SimpleChange}): void { if ((changes.courseId || changes.categoryId) && this.weeks) { - const courseId = this.courseId ? Number(this.courseId) : undefined, - categoryId = this.categoryId ? Number(this.categoryId) : undefined; - - this.filterEvents(courseId, categoryId); + this.filterEvents(); } } /** * Fetch contacts. * - * @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more. + * @param {boolean} [refresh=false] True if we are refreshing events. * @return {Promise} Promise resolved when done. */ fetchData(refresh: boolean = false): Promise { - const courseId = this.courseId ? Number(this.courseId) : undefined, - categoryId = this.categoryId ? Number(this.categoryId) : undefined, - promises = []; + const promises = []; promises.push(this.loadCategories()); - promises.push(this.calendarProvider.getMonthlyEvents(this.year, this.month, courseId, categoryId).then((result) => { + // 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. + 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; + })); + + return Promise.all(promises).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch the events for current month. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getMonthlyEvents(this.year, this.month).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).getTime(), 'core.strftimemonthyear'); @@ -104,13 +165,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); this.weeks = result.weeks; - this.filterEvents(courseId, categoryId); - })); + // Merge the online events with offline data. + this.mergeEvents(); - return Promise.all(promises).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); - }).finally(() => { - this.loaded = true; + // Filter events by course. + this.filterEvents(); }); } @@ -140,11 +199,10 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Filter events to only display events belonging to a certain course. - * - * @param {number} courseId Course ID. - * @param {number} categoryId Category the course belongs to. */ - filterEvents(courseId: number, categoryId: number): void { + filterEvents(): void { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; this.weeks.forEach((week) => { week.days.forEach((day) => { @@ -165,9 +223,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Refresh events. * + * @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(): Promise { + refreshData(sync?: boolean, showErrors?: boolean): Promise { const promises = []; promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); @@ -184,38 +244,145 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest * Load next month. */ loadNext(): void { - if (this.month === 12) { - this.month = 1; - this.year++; - } else { - this.month++; - } + this.increaseMonth(); this.loaded = false; - this.fetchData(); + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.decreaseMonth(); + }).finally(() => { + this.loaded = true; + }); } /** * Load previous month. */ loadPrevious(): void { + this.decreaseMonth(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.increaseMonth(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * An event was clicked. + * + * @param {any} event Event. + */ + eventClicked(event: any): void { + this.onEventClicked.emit(event.id); + } + + /** + * Decrease the current month. + */ + protected decreaseMonth(): void { if (this.month === 1) { this.month = 12; this.year--; } else { this.month--; } + } - this.loaded = false; + /** + * Increase the current month. + */ + protected increaseMonth(): void { + if (this.month === 12) { + this.month = 1; + this.year++; + } else { + this.month++; + } + } - this.fetchData(); + /** + * Merge online events with the offline events of that period. + */ + protected mergeEvents(): void { + const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)]; + + if (!monthOfflineEvents && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return; + } + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + day.events.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + day.events = day.events.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + } + + if (monthOfflineEvents && monthOfflineEvents[day.mday]) { + // Add the offline events (either new or edited). + day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + } + }); + }); + } + + /** + * 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; + }); + } + + /** + * Undelete a certain event. + * + * @param {number} eventId Event ID. + */ + protected undeleteEvent(eventId: number): void { + if (!this.weeks) { + return; + } + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + const event = day.events.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = false; + } + }); + }); } /** * Component destroyed. */ ngOnDestroy(): void { - // @todo + this.undeleteEventObserver && this.undeleteEventObserver.off(); } } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 42c3cd4b9..fa0877b46 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -7,6 +7,7 @@ + @@ -16,7 +17,12 @@ - + + + {{ '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 9ab19cd7e..8f134ed90 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -12,16 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core'; import { IonicPage, NavParams, NavController, PopoverController } 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 { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +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 { Network } from '@ionic-native/network'; /** * Page that displays the calendar events. @@ -31,7 +37,7 @@ import { TranslateService } from '@ngx-translate/core'; selector: 'page-addon-calendar-index', templateUrl: 'index.html', }) -export class AddonCalendarIndexPage implements OnInit { +export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; protected allCourses = { @@ -39,6 +45,18 @@ export class AddonCalendarIndexPage implements OnInit { fullname: this.translate.instant('core.fulllistofcourses'), category: -1 }; + protected eventId: number; + protected currentSiteId: string; + + // 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; courseId: number; categoryId: number; @@ -46,63 +64,177 @@ export class AddonCalendarIndexPage implements OnInit { courses: any[]; notificationsEnabled = false; 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 calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, private translate: TranslateService, + private eventsProvider: CoreEventsProvider, private coursesProvider: CoreCoursesProvider, - private popoverCtrl: PopoverController) { + private popoverCtrl: PopoverController, + private appProvider: CoreAppProvider) { this.courseId = navParams.get('courseId'); + this.eventId = navParams.get('eventId') || false; this.notificationsEnabled = localNotificationsProvider.isAvailable(); + 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 != 'index') { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Update the events when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Update the "hasOffline" property if an event deleted in offline is restored. + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + this.calendarOffline.hasOfflineData().then((hasOffline) => { + this.hasOffline = hasOffline; + }); + }, 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.fetchData(); + 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 {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(): Promise { - const promises = []; + fetchData(sync?: boolean, showErrors?: boolean): Promise { - // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + this.syncIcon = 'spinner'; + this.isOnline = this.appProvider.isOnline(); - if (this.courseId) { - // Search the course to get the category. - const course = this.courses.find((course) => { - return course.id == this.courseId; - }); + let promise; - if (course) { - this.categoryId = course.category; + if (sync) { + // Try to synchronize offline events. + promise = this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); } - } - })); - // Check if user can create events. - promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { - this.canCreate = canEdit; - })); + if (result.updated) { + // Trigger a manual sync event. + result.source = 'index'; - return Promise.all(promises).catch((error) => { + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + const promises = []; + + 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; + } + } + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + // Check if there is offline data. + promises.push(this.calendarOffline.hasOfflineData().then((hasOffline) => { + this.hasOffline = hasOffline; + })); + + return Promise.all(promises); + }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); }).finally(() => { this.loaded = true; + this.syncIcon = 'sync'; }); } @@ -110,13 +242,31 @@ export class AddonCalendarIndexPage implements OnInit { * 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): void { - if (!this.loaded) { - return; + 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().then(() => { @@ -126,11 +276,27 @@ export class AddonCalendarIndexPage implements OnInit { // Refresh the sub-component. promises.push(this.calendarComponent.refreshData()); - Promise.all(promises).finally(() => { - refresher && refresher.complete(); + return Promise.all(promises).finally(() => { + return this.fetchData(sync, showErrors); }); } + /** + * 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. * @@ -182,4 +348,18 @@ export class AddonCalendarIndexPage implements OnInit { openSettings(): void { this.navCtrl.push('AddonCalendarSettingsPage'); } + + /** + * 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/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index 3d48baa38..ee06f75c9 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -280,6 +280,18 @@ export class AddonCalendarOfflineProvider { }); } + /** + * Check whether there's offline data for a site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline data, false otherwise. + */ + hasOfflineData(siteId?: string): Promise { + return this.getAllEventsIds(siteId).then((ids) => { + return ids.length > 0; + }); + } + /** * Check if an event is deleted. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 94cfa1e93..225ec7ffb 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; +import * as moment from 'moment'; /** * Service that provides some features regarding lists of courses and categories. @@ -85,6 +86,41 @@ export class AddonCalendarHelperProvider { }); } + /** + * 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 {any[]} events Events to classify. + * @return {{[monthId: string]: {[day: number]: any[]}}} Object with the classified events. + */ + classifyIntoMonths(events: any[]): {[monthId: string]: {[day: number]: any[]}} { + + const result = {}; + + events.forEach((event) => { + const treatedDay = moment(new Date(event.timestart * 1000)), + endDay = moment(new Date((event.timestart + (event.timeduration || 0)) * 1000)); + + // Add the event to all the days it lasts. + while (!treatedDay.isAfter(endDay, 'day')) { + const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1), + 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. * @@ -97,7 +133,7 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } - if (e.id < 0) { + if (typeof e.duration != 'undefined') { // It's an offline event, add some calculated data. e.format = 1; e.visible = 1; @@ -140,6 +176,17 @@ export class AddonCalendarHelperProvider { return options; } + /** + * Get the month "id" (year + month). + * + * @param {number} year Year. + * @param {number} month Month. + * @return {string} The "id". + */ + getMonthId(year: number, month: number): string { + return year + '#' + month; + } + /** * Check if the data of an event has changed. *