MOBILE-3021 calendar: Display offline events too

main
Dani Palou 2019-07-03 15:59:27 +02:00
parent 661709acb4
commit b202d92dc3
7 changed files with 494 additions and 69 deletions

View File

@ -19,7 +19,7 @@
</ion-grid> </ion-grid>
<!-- Calendar view. --> <!-- Calendar view. -->
<ion-grid no-padding> <ion-grid padding-horizontal>
<!-- List of days. --> <!-- List of days. -->
<ion-row> <ion-row>
<ion-col text-center *ngFor="let day of weekDays" class="addon-calendar-weekdays"> <ion-col text-center *ngFor="let day of weekDays" class="addon-calendar-weekdays">
@ -38,11 +38,15 @@
<!-- In tablet, display list of events. --> <!-- In tablet, display list of events. -->
<div class="hidden-phone" class="addon-calendar-day-events"> <div class="hidden-phone" class="addon-calendar-day-events">
<p *ngFor="let event of day.filteredEvents | slice:0:3"> <ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
<span class="calendar_event_type calendar_event_{{event.eventtype}}"></span> <p *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event" (click)="eventClicked(event)">
{{event.name}} <span class="calendar_event_type calendar_event_{{event.eventtype}}"></span>
</p> <ion-icon *ngIf="event.offline && !event.deleted" name="time"></ion-icon>
<p *ngIf="day.filteredEvents.length > 3">{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</p> <ion-icon *ngIf="event.deleted" name="trash"></ion-icon>
{{event.name}}
</p>
</ng-container>
<p *ngIf="day.filteredEvents.length > 4"><b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b></p>
</div> </div>
</ion-col> </ion-col>
<ion-col *ngFor="let value of week.postpadding" class="dayblank"></ion-col> <!-- Empty slots (last week). --> <ion-col *ngFor="let value of week.postpadding" class="dayblank"></ion-col> <!-- Empty slots (last week). -->

View File

@ -13,6 +13,15 @@ ion-app.app-root addon-calendar-calendar {
.addon-calendar-day-events { .addon-calendar-day-events {
@include text-align('start'); @include text-align('start');
ion-icon {
@include margin-horizontal(null, 2px);
font-size: 1em;
}
}
.addon-calendar-event {
cursor: pointer;
} }
.calendar_event_type { .calendar_event_type {

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -20,6 +20,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarHelperProvider } from '../../providers/helper';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
/** /**
@ -35,6 +36,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
@Input() courseId: number | string; @Input() courseId: number | string;
@Input() categoryId: number | string; // Category ID the course belongs to. @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. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true.
@Output() onEventClicked = new EventEmitter<number>();
periodName: string; periodName: string;
weekDays: any[]; weekDays: any[];
@ -45,16 +47,39 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
protected month: number; protected month: number;
protected categoriesRetrieved = false; protected categoriesRetrieved = false;
protected categories = {}; 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, constructor(eventsProvider: CoreEventsProvider,
sitesProvider: CoreSitesProvider, sitesProvider: CoreSitesProvider,
private calendarProvider: AddonCalendarProvider, private calendarProvider: AddonCalendarProvider,
private calendarHelper: AddonCalendarHelperProvider, private calendarHelper: AddonCalendarHelperProvider,
private calendarOffline: AddonCalendarOfflineProvider,
private domUtils: CoreDomUtilsProvider, private domUtils: CoreDomUtilsProvider,
private timeUtils: CoreTimeUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private utils: CoreUtilsProvider, private utils: CoreUtilsProvider,
private coursesProvider: CoreCoursesProvider) { 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 { ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if ((changes.courseId || changes.categoryId) && this.weeks) { if ((changes.courseId || changes.categoryId) && this.weeks) {
const courseId = this.courseId ? Number(this.courseId) : undefined, this.filterEvents();
categoryId = this.categoryId ? Number(this.categoryId) : undefined;
this.filterEvents(courseId, categoryId);
} }
} }
/** /**
* Fetch contacts. * 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<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
fetchData(refresh: boolean = false): Promise<any> { fetchData(refresh: boolean = false): Promise<any> {
const courseId = this.courseId ? Number(this.courseId) : undefined, const promises = [];
categoryId = this.categoryId ? Number(this.categoryId) : undefined,
promises = [];
promises.push(this.loadCategories()); 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<any>} Promise resolved when done.
*/
fetchEvents(): Promise<any> {
// 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. // 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'); 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.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno);
this.weeks = result.weeks; this.weeks = result.weeks;
this.filterEvents(courseId, categoryId); // Merge the online events with offline data.
})); this.mergeEvents();
return Promise.all(promises).catch((error) => { // Filter events by course.
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.filterEvents();
}).finally(() => {
this.loaded = true;
}); });
} }
@ -140,11 +199,10 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
/** /**
* Filter events to only display events belonging to a certain course. * 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) => { this.weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
@ -165,9 +223,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
/** /**
* Refresh events. * 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<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
refreshData(): Promise<any> { refreshData(sync?: boolean, showErrors?: boolean): Promise<any> {
const promises = []; const promises = [];
promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month));
@ -184,38 +244,145 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest
* Load next month. * Load next month.
*/ */
loadNext(): void { loadNext(): void {
if (this.month === 12) { this.increaseMonth();
this.month = 1;
this.year++;
} else {
this.month++;
}
this.loaded = false; 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. * Load previous month.
*/ */
loadPrevious(): void { 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) { if (this.month === 1) {
this.month = 12; this.month = 12;
this.year--; this.year--;
} else { } else {
this.month--; 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. * Component destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
// @todo this.undeleteEventObserver && this.undeleteEventObserver.off();
} }
} }

View File

@ -7,6 +7,7 @@
</button> </button>
<core-context-menu> <core-context-menu>
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item> <core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item>
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu> </core-context-menu>
</ion-buttons> </ion-buttons>
</ion-navbar> </ion-navbar>
@ -16,7 +17,12 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<addon-calendar-calendar [courseId]="courseId" [categoryId]="categoryId"></addon-calendar-calendar> <!-- There is data to be synchronized -->
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}
</ion-card>
<addon-calendar-calendar [courseId]="courseId" [categoryId]="categoryId" (onEventClicked)="gotoEvent($event)"></addon-calendar-calendar>
<!-- Create a calendar event. --> <!-- Create a calendar event. -->
<ion-fab core-fab bottom end *ngIf="canCreate"> <ion-fab core-fab bottom end *ngIf="canCreate">

View File

@ -12,16 +12,22 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarHelperProvider } from '../../providers/helper';
import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar';
import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Network } from '@ionic-native/network';
/** /**
* Page that displays the calendar events. * Page that displays the calendar events.
@ -31,7 +37,7 @@ import { TranslateService } from '@ngx-translate/core';
selector: 'page-addon-calendar-index', selector: 'page-addon-calendar-index',
templateUrl: 'index.html', templateUrl: 'index.html',
}) })
export class AddonCalendarIndexPage implements OnInit { export class AddonCalendarIndexPage implements OnInit, OnDestroy {
@ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent;
protected allCourses = { protected allCourses = {
@ -39,6 +45,18 @@ export class AddonCalendarIndexPage implements OnInit {
fullname: this.translate.instant('core.fulllistofcourses'), fullname: this.translate.instant('core.fulllistofcourses'),
category: -1 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; courseId: number;
categoryId: number; categoryId: number;
@ -46,63 +64,177 @@ export class AddonCalendarIndexPage implements OnInit {
courses: any[]; courses: any[];
notificationsEnabled = false; notificationsEnabled = false;
loaded = false; loaded = false;
hasOffline = false;
isOnline = false;
syncIcon: string;
constructor(localNotificationsProvider: CoreLocalNotificationsProvider, constructor(localNotificationsProvider: CoreLocalNotificationsProvider,
navParams: NavParams, navParams: NavParams,
network: Network,
zone: NgZone,
sitesProvider: CoreSitesProvider,
private navCtrl: NavController, private navCtrl: NavController,
private domUtils: CoreDomUtilsProvider, private domUtils: CoreDomUtilsProvider,
private calendarProvider: AddonCalendarProvider, private calendarProvider: AddonCalendarProvider,
private calendarOffline: AddonCalendarOfflineProvider,
private calendarHelper: AddonCalendarHelperProvider, private calendarHelper: AddonCalendarHelperProvider,
private calendarSync: AddonCalendarSyncProvider,
private translate: TranslateService, private translate: TranslateService,
private eventsProvider: CoreEventsProvider,
private coursesProvider: CoreCoursesProvider, private coursesProvider: CoreCoursesProvider,
private popoverCtrl: PopoverController) { private popoverCtrl: PopoverController,
private appProvider: CoreAppProvider) {
this.courseId = navParams.get('courseId'); this.courseId = navParams.get('courseId');
this.eventId = navParams.get('eventId') || false;
this.notificationsEnabled = localNotificationsProvider.isAvailable(); 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. * View loaded.
*/ */
ngOnInit(): void { 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. * 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<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
fetchData(): Promise<any> { fetchData(sync?: boolean, showErrors?: boolean): Promise<any> {
const promises = [];
// Load courses for the popover. this.syncIcon = 'spinner';
promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { this.isOnline = this.appProvider.isOnline();
// Add "All courses".
courses.unshift(this.allCourses);
this.courses = courses;
if (this.courseId) { let promise;
// Search the course to get the category.
const course = this.courses.find((course) => {
return course.id == this.courseId;
});
if (course) { if (sync) {
this.categoryId = course.category; // 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. if (result.updated) {
promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { // Trigger a manual sync event.
this.canCreate = canEdit; 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); this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
}).finally(() => { }).finally(() => {
this.loaded = true; this.loaded = true;
this.syncIcon = 'sync';
}); });
} }
@ -110,13 +242,31 @@ export class AddonCalendarIndexPage implements OnInit {
* Refresh the data. * Refresh the data.
* *
* @param {any} [refresher] Refresher. * @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<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
doRefresh(refresher?: any): void { doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise<any> {
if (!this.loaded) { if (this.loaded) {
return; 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<any>} Promise resolved when done.
*/
refreshData(sync?: boolean, showErrors?: boolean): Promise<any> {
this.syncIcon = 'spinner';
const promises = []; const promises = [];
promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => {
@ -126,11 +276,27 @@ export class AddonCalendarIndexPage implements OnInit {
// Refresh the sub-component. // Refresh the sub-component.
promises.push(this.calendarComponent.refreshData()); promises.push(this.calendarComponent.refreshData());
Promise.all(promises).finally(() => { return Promise.all(promises).finally(() => {
refresher && refresher.complete(); 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. * Show the context menu.
* *
@ -182,4 +348,18 @@ export class AddonCalendarIndexPage implements OnInit {
openSettings(): void { openSettings(): void {
this.navCtrl.push('AddonCalendarSettingsPage'); 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();
}
} }

View File

@ -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<boolean>} Promise resolved with boolean: true if has offline data, false otherwise.
*/
hasOfflineData(siteId?: string): Promise<boolean> {
return this.getAllEventsIds(siteId).then((ids) => {
return ids.length > 0;
});
}
/** /**
* Check if an event is deleted. * Check if an event is deleted.
* *

View File

@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
import { AddonCalendarProvider } from './calendar'; import { AddonCalendarProvider } from './calendar';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import * as moment from 'moment';
/** /**
* Service that provides some features regarding lists of courses and categories. * 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. * Convenience function to format some event data to be rendered.
* *
@ -97,7 +133,7 @@ export class AddonCalendarHelperProvider {
e.moduleIcon = e.icon; e.moduleIcon = e.icon;
} }
if (e.id < 0) { if (typeof e.duration != 'undefined') {
// It's an offline event, add some calculated data. // It's an offline event, add some calculated data.
e.format = 1; e.format = 1;
e.visible = 1; e.visible = 1;
@ -140,6 +176,17 @@ export class AddonCalendarHelperProvider {
return options; 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. * Check if the data of an event has changed.
* *