MOBILE-3021 calendar: Display offline events too
parent
661709acb4
commit
b202d92dc3
|
@ -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). -->
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue