MOBILE-1927 calendar: Allow creating events in offline

main
Dani Palou 2019-06-21 09:17:46 +02:00
parent 5a9a7b1a11
commit 7ccfade21e
13 changed files with 437 additions and 44 deletions

View File

@ -14,6 +14,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
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 { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
@ -25,6 +26,7 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers). // List of providers (without handlers).
export const ADDON_CALENDAR_PROVIDERS: any[] = [ export const ADDON_CALENDAR_PROVIDERS: any[] = [
AddonCalendarProvider, AddonCalendarProvider,
AddonCalendarOfflineProvider,
AddonCalendarHelperProvider AddonCalendarHelperProvider
]; ];
@ -35,6 +37,7 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [
], ],
providers: [ providers: [
AddonCalendarProvider, AddonCalendarProvider,
AddonCalendarOfflineProvider,
AddonCalendarHelperProvider, AddonCalendarHelperProvider,
AddonCalendarMainMenuHandler AddonCalendarMainMenuHandler
] ]

View File

@ -44,7 +44,7 @@
<ion-item text-wrap *ngIf="eventTypeControl.value == 'course'"> <ion-item text-wrap *ngIf="eventTypeControl.value == 'course'">
<ion-label id="addon-calendar-course-label"><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label> <ion-label id="addon-calendar-course-label"><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label>
<ion-select [formControlName]="'courseid'" aria-labelledby="addon-calendar-course-label" interface="action-sheet" [placeholder]="'core.noselection' | translate"> <ion-select [formControlName]="'courseid'" aria-labelledby="addon-calendar-course-label" interface="action-sheet" [placeholder]="'core.noselection' | translate">
<ion-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-option> <ion-option *ngFor="let course of courses" [value]="course.id"><core-format-text [text]="course.fullname"></core-format-text></ion-option>
</ion-select> </ion-select>
</ion-item> </ion-item>
@ -54,7 +54,7 @@
<ion-item text-wrap> <ion-item text-wrap>
<ion-label id="addon-calendar-groupcourse-label"><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label> <ion-label id="addon-calendar-groupcourse-label"><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label>
<ion-select [formControlName]="'groupcourseid'" aria-labelledby="addon-calendar-groupcourse-label" interface="action-sheet" [placeholder]="'core.noselection' | translate" (ionChange)="groupCourseSelected($event)"> <ion-select [formControlName]="'groupcourseid'" aria-labelledby="addon-calendar-groupcourse-label" interface="action-sheet" [placeholder]="'core.noselection' | translate" (ionChange)="groupCourseSelected($event)">
<ion-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-option> <ion-option *ngFor="let course of courses" [value]="course.id"><core-format-text [text]="course.fullname"></core-format-text></ion-option>
</ion-select> </ion-select>
</ion-item> </ion-item>
<!-- The course has no groups. --> <!-- The course has no groups. -->

View File

@ -26,6 +26,7 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts';
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 { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
@ -79,6 +80,7 @@ export class AddonCalendarEditEventPage implements OnInit {
private coursesProvider: CoreCoursesProvider, private coursesProvider: CoreCoursesProvider,
private utils: CoreUtilsProvider, private utils: CoreUtilsProvider,
private calendarProvider: AddonCalendarProvider, private calendarProvider: AddonCalendarProvider,
private calendarOffline: AddonCalendarOfflineProvider,
private calendarHelper: AddonCalendarHelperProvider, private calendarHelper: AddonCalendarHelperProvider,
private fb: FormBuilder, private fb: FormBuilder,
@Optional() private svComponent: CoreSplitViewComponent) { @Optional() private svComponent: CoreSplitViewComponent) {
@ -102,8 +104,10 @@ export class AddonCalendarEditEventPage implements OnInit {
this.groupControl = this.fb.control(''); this.groupControl = this.fb.control('');
this.descriptionControl = this.fb.control(''); this.descriptionControl = this.fb.control('');
const currentDate = this.timeUtils.toDatetimeFormat();
this.eventForm.addControl('name', this.fb.control('', Validators.required)); this.eventForm.addControl('name', this.fb.control('', Validators.required));
this.eventForm.addControl('timestart', this.fb.control(new Date().toISOString(), Validators.required)); this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required));
this.eventForm.addControl('eventtype', this.eventTypeControl); this.eventForm.addControl('eventtype', this.eventTypeControl);
this.eventForm.addControl('categoryid', this.fb.control('')); this.eventForm.addControl('categoryid', this.fb.control(''));
this.eventForm.addControl('courseid', this.fb.control(this.courseId)); this.eventForm.addControl('courseid', this.fb.control(this.courseId));
@ -112,7 +116,7 @@ export class AddonCalendarEditEventPage implements OnInit {
this.eventForm.addControl('description', this.descriptionControl); this.eventForm.addControl('description', this.descriptionControl);
this.eventForm.addControl('location', this.fb.control('')); this.eventForm.addControl('location', this.fb.control(''));
this.eventForm.addControl('duration', this.fb.control(0)); this.eventForm.addControl('duration', this.fb.control(0));
this.eventForm.addControl('timedurationuntil', this.fb.control(new Date().toISOString())); this.eventForm.addControl('timedurationuntil', this.fb.control(currentDate));
this.eventForm.addControl('timedurationminutes', this.fb.control('')); this.eventForm.addControl('timedurationminutes', this.fb.control(''));
this.eventForm.addControl('repeat', this.fb.control(false)); this.eventForm.addControl('repeat', this.fb.control(false));
this.eventForm.addControl('repeats', this.fb.control('1')); this.eventForm.addControl('repeats', this.fb.control('1'));
@ -131,9 +135,10 @@ export class AddonCalendarEditEventPage implements OnInit {
/** /**
* Fetch the data needed to render the form. * Fetch the data needed to render the form.
* *
* @param {boolean} [refresh] Whether it's refreshing data.
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
protected fetchData(): Promise<any> { protected fetchData(refresh?: boolean): Promise<any> {
let accessInfo; let accessInfo;
// Get access info. // Get access info.
@ -151,6 +156,33 @@ export class AddonCalendarEditEventPage implements OnInit {
return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar')); return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar'));
} }
if (this.eventId && !refresh) {
// Get the event data if there's any.
promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => {
this.hasOffline = true;
// Load the data in the form.
this.eventForm.controls.name.setValue(event.name);
this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000));
this.eventForm.controls.eventtype.setValue(event.eventtype);
this.eventForm.controls.categoryid.setValue(event.categoryid || '');
this.eventForm.controls.courseid.setValue(event.courseid || '');
this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || '');
this.eventForm.controls.groupid.setValue(event.groupid || '');
this.eventForm.controls.description.setValue(event.description);
this.eventForm.controls.location.setValue(event.location);
this.eventForm.controls.duration.setValue(event.duration);
this.eventForm.controls.timedurationuntil.setValue(
this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now()));
this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || '');
this.eventForm.controls.repeat.setValue(!!event.repeat);
this.eventForm.controls.repeats.setValue(event.repeats || '1');
}).catch(() => {
// No offline data.
this.hasOffline = false;
}));
}
if (types.category) { if (types.category) {
// Get the categories. // Get the categories.
promises.push(this.coursesProvider.getCategories(0, true).then((cats) => { promises.push(this.coursesProvider.getCategories(0, true).then((cats) => {
@ -185,11 +217,13 @@ export class AddonCalendarEditEventPage implements OnInit {
} }
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
// Set event types. If course is allowed, select it first. if (!this.eventTypeControl.value) {
if (types.course) { // Initialize event type value. If course is allowed, select it first.
this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); if (types.course) {
} else { this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE);
this.eventTypeControl.setValue(eventTypes[0].value); } else {
this.eventTypeControl.setValue(eventTypes[0].value);
}
} }
this.eventTypes = eventTypes; this.eventTypes = eventTypes;
@ -227,7 +261,7 @@ export class AddonCalendarEditEventPage implements OnInit {
} }
Promise.all(promises).finally(() => { Promise.all(promises).finally(() => {
this.fetchData().finally(() => { this.fetchData(true).finally(() => {
refresher.complete(); refresher.complete();
}); });
}); });
@ -333,8 +367,8 @@ export class AddonCalendarEditEventPage implements OnInit {
// Send the data. // Send the data.
const modal = this.domUtils.showModalLoading('core.sending'); const modal = this.domUtils.showModalLoading('core.sending');
this.calendarProvider.submitEvent(this.eventId, data).then((event) => { this.calendarProvider.submitEvent(this.eventId, data).then((result) => {
this.returnToList(event); this.returnToList(result.event);
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error sending data.'); this.domUtils.showErrorModalDefault(error, 'Error sending data.');
}).finally(() => { }).finally(() => {
@ -348,10 +382,14 @@ export class AddonCalendarEditEventPage implements OnInit {
* @param {number} [event] Event. * @param {number} [event] Event.
*/ */
protected returnToList(event?: any): void { protected returnToList(event?: any): void {
const data: any = { if (event) {
event: event const data: any = {
}; event: event
this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); };
this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId());
} else {
this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId());
}
if (this.svComponent && this.svComponent.isOn()) { if (this.svComponent && this.svComponent.isOn()) {
// Empty form. // Empty form.
@ -369,7 +407,12 @@ export class AddonCalendarEditEventPage implements OnInit {
*/ */
discard(): void { discard(): void {
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
// @todo. this.calendarOffline.deleteEvent(this.eventId).then(() => {
this.returnToList();
}).catch(() => {
// Shouldn't happen.
this.domUtils.showErrorModal('Error discarding event.');
});
}).catch(() => { }).catch(() => {
// Cancelled. // Cancelled.
}); });

View File

@ -17,9 +17,28 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="eventsLoaded"> <core-loading [hideUntil]="eventsLoaded">
<!-- 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>
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate"> <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
</core-empty-box> </core-empty-box>
<ion-list *ngIf="offlineEvents && offlineEvents.length" no-margin>
<ng-container *ngFor="let event of offlineEvents">
<ion-item-divider> {{ 'core.notsent' | translate }}</ion-item-divider>
<a ion-item text-wrap [title]="event.name" (click)="gotoEvent(event.id)" [class.core-split-item-selected]="event.id == eventId">
<core-icon *ngIf="event.icon" [name]="event.icon" item-start></core-icon>
<h2><core-format-text [text]="event.name"></core-format-text></h2>
<p>
{{ event.timestart * 1000 | coreFormatDate: "strftimedatetimeshort" }}
<span *ngIf="event.timeduration"> - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}</span>
</p>
</a>
</ng-container>
</ion-list>
<ion-list *ngIf="filteredEvents && filteredEvents.length" no-margin> <ion-list *ngIf="filteredEvents && filteredEvents.length" no-margin>
<ng-container *ngFor="let event of filteredEvents"> <ng-container *ngFor="let event of filteredEvents">
<ion-item-divider *ngIf="event.showDate"> <ion-item-divider *ngIf="event.showDate">
@ -43,7 +62,7 @@
<!-- Create a calendar event. --> <!-- Create a calendar event. -->
<ion-fab core-fab bottom end *ngIf="canCreate"> <ion-fab core-fab bottom end *ngIf="canCreate">
<button ion-fab (click)="openCreate()" [attr.aria-label]="'addon.calendar.newevent' | translate"> <button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
<ion-icon name="add"></ion-icon> <ion-icon name="add"></ion-icon>
</button> </button>
</ion-fab> </ion-fab>

View File

@ -16,6 +16,7 @@ import { Component, ViewChild, OnDestroy } from '@angular/core';
import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
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 { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -55,10 +56,12 @@ export class AddonCalendarListPage implements OnDestroy {
protected eventId: number; protected eventId: number;
protected preSelectedCourseId: number; protected preSelectedCourseId: number;
protected newEventObserver: any; protected newEventObserver: any;
protected discardedObserver: any;
courses: any[]; courses: any[];
eventsLoaded = false; eventsLoaded = false;
events = []; events = [];
offlineEvents = [];
notificationsEnabled = false; notificationsEnabled = false;
filteredEvents = []; filteredEvents = [];
canLoadMore = false; canLoadMore = false;
@ -67,12 +70,14 @@ export class AddonCalendarListPage implements OnDestroy {
course: this.allCourses course: this.allCourses
}; };
canCreate = false; canCreate = false;
hasOffline = false;
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider,
private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController,
eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider) { eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider,
private calendarOffline: AddonCalendarOfflineProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = localNotificationsProvider.isAvailable(); this.notificationsEnabled = localNotificationsProvider.isAvailable();
@ -87,7 +92,7 @@ export class AddonCalendarListPage implements OnDestroy {
this.eventId = navParams.get('eventId') || false; this.eventId = navParams.get('eventId') || false;
this.preSelectedCourseId = navParams.get('courseId') || null; this.preSelectedCourseId = navParams.get('courseId') || null;
// Listen for events added. When an event is added, we reload the data. // Listen for events added. When an event is added, reload the data.
this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => {
if (data && data.event) { if (data && data.event) {
if (this.splitviewCtrl.isOn()) { if (this.splitviewCtrl.isOn()) {
@ -97,19 +102,25 @@ export class AddonCalendarListPage implements OnDestroy {
this.eventsLoaded = false; this.eventsLoaded = false;
this.refreshEvents(false).finally(() => { this.refreshEvents(false).finally(() => {
this.eventsLoaded = true;
// In tablet mode try to open the event. // In tablet mode try to open the event (only if it's an online event).
if (this.splitviewCtrl.isOn()) { if (this.splitviewCtrl.isOn() && data.event.id > 0) {
if (data.event.id) { this.gotoEvent(data.event.id);
this.gotoEvent(data.event.id);
} else {
// It's an offline event.
}
} }
}); });
} }
}, sitesProvider.getCurrentSiteId()); }, sitesProvider.getCurrentSiteId());
// Listen for new event discarded event. When it does, reload the data.
this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
if (this.splitviewCtrl.isOn()) {
// Discussion added, clear details page.
this.splitviewCtrl.emptyDetails();
}
this.eventsLoaded = false;
this.refreshEvents(false);
}, sitesProvider.getCurrentSiteId());
} }
/** /**
@ -126,8 +137,6 @@ export class AddonCalendarListPage implements OnDestroy {
// Take first and load it. // Take first and load it.
this.gotoEvent(this.events[0].id); this.gotoEvent(this.events[0].id);
} }
}).finally(() => {
this.eventsLoaded = true;
}); });
} }
@ -167,7 +176,18 @@ export class AddonCalendarListPage implements OnDestroy {
return this.fetchEvents(refresh); return this.fetchEvents(refresh);
})); }));
return Promise.all(promises); // Get offline events.
promises.push(this.calendarOffline.getAllEvents().then((events) => {
this.hasOffline = !!events.length;
// Format data and sort by timestart.
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart);
}));
return Promise.all(promises).finally(() => {
this.eventsLoaded = true;
});
} }
/** /**
@ -194,6 +214,8 @@ export class AddonCalendarListPage implements OnDestroy {
return this.fetchEvents(); return this.fetchEvents();
} }
} else { } else {
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
// Sort the events by timestart, they're ordered by id. // Sort the events by timestart, they're ordered by id.
events.sort((a, b) => { events.sort((a, b) => {
if (a.timestart == b.timestart) { if (a.timestart == b.timestart) {
@ -203,7 +225,6 @@ export class AddonCalendarListPage implements OnDestroy {
return a.timestart - b.timestart; return a.timestart - b.timestart;
}); });
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
this.getCategories = this.shouldLoadCategories(events); this.getCategories = this.shouldLoadCategories(events);
if (refresh) { if (refresh) {
@ -427,10 +448,16 @@ export class AddonCalendarListPage implements OnDestroy {
} }
/** /**
* Open page to create an event. * Open page to create/edit an event.
*
* @param {number} [eventId] Event ID to edit.
*/ */
openCreate(): void { openEdit(eventId?: number): void {
const params: any = {}; const params: any = {};
if (eventId) {
params.eventId = eventId;
}
if (this.filter.course.id != this.allCourses.id) { if (this.filter.course.id != this.allCourses.id) {
params.courseId = this.filter.course.id; params.courseId = this.filter.course.id;
} }
@ -452,7 +479,15 @@ export class AddonCalendarListPage implements OnDestroy {
*/ */
gotoEvent(eventId: number): void { gotoEvent(eventId: number): void {
this.eventId = eventId; this.eventId = eventId;
this.splitviewCtrl.push('AddonCalendarEventPage', { id: eventId });
if (eventId < 0) {
// It's an offline event, go to the edit page.
this.openEdit(eventId);
} else {
this.splitviewCtrl.push('AddonCalendarEventPage', {
id: eventId
});
}
} }
/** /**

View File

@ -0,0 +1,215 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { AddonCalendarProvider } from './calendar';
/**
* Service to handle offline calendar events.
*/
@Injectable()
export class AddonCalendarOfflineProvider {
// Variables for database.
static EVENTS_TABLE = 'addon_calendar_offline_events';
protected siteSchema: CoreSiteSchema = {
name: 'AddonCalendarOfflineProvider',
version: 1,
tables: [
{
name: AddonCalendarOfflineProvider.EVENTS_TABLE,
columns: [
{
name: 'id', // Negative for offline entries.
type: 'INTEGER',
},
{
name: 'name',
type: 'TEXT',
notNull: true
},
{
name: 'timestart',
type: 'INTEGER',
notNull: true
},
{
name: 'eventtype',
type: 'TEXT',
notNull: true
},
{
name: 'categoryid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'groupcourseid',
type: 'INTEGER',
},
{
name: 'groupid',
type: 'INTEGER',
},
{
name: 'description',
type: 'TEXT',
},
{
name: 'location',
type: 'TEXT',
},
{
name: 'duration',
type: 'INTEGER',
},
{
name: 'timedurationuntil',
type: 'INTEGER',
},
{
name: 'timedurationminutes',
type: 'INTEGER',
},
{
name: 'repeat',
type: 'INTEGER',
},
{
name: 'repeats',
type: 'TEXT',
},
{
name: 'userid',
type: 'INTEGER',
},
{
name: 'timecreated',
type: 'INTEGER',
}
],
primaryKeys: ['id']
}
]
};
constructor(private sitesProvider: CoreSitesProvider) {
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
/**
* Delete an offline event.
*
* @param {number} eventId Event ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
*/
deleteEvent(eventId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions: any = {
id: eventId
};
return site.getDb().deleteRecords(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions);
});
}
/**
* Get all offline events.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with events.
*/
getAllEvents(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE);
});
}
/**
* Get an offline event.
*
* @param {number} eventId Event ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the event.
*/
getEvent(eventId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions: any = {
id: eventId
};
return site.getDb().getRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions);
});
}
/**
* Check if there are offline events to send.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline events, false otherwise.
*/
hasEvents(siteId?: string): Promise<boolean> {
return this.getAllEvents(siteId).then((events) => {
return !!events.length;
}).catch(() => {
// No offline data found, return false.
return false;
});
}
/**
* Offline version for adding a new discussion to a forum.
*
* @param {number} eventId Event ID. If it's a new event, set it to undefined/null.
* @param {any} data Event data.
* @param {number} [timeCreated] The time the event was created. If not defined, current time.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the stored event.
*/
saveEvent(eventId: number, data: any, timeCreated?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
timeCreated = timeCreated || Date.now();
const event = {
id: eventId || -timeCreated,
name: data.name,
timestart: data.timestart,
eventtype: data.eventtype,
categoryid: data.categoryid || null,
courseid: data.courseid || null,
groupcourseid: data.groupcourseid || null,
groupid: data.groupid || null,
description: data.description && data.description.text,
location: data.location,
duration: data.duration,
timedurationuntil: data.timedurationuntil,
timedurationminutes: data.timedurationminutes,
repeat: data.repeat ? 1 : 0,
repeats: data.repeats,
timecreated: timeCreated,
userid: site.getUserId()
};
return site.getDb().insertRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, event).then(() => {
return event;
});
});
}
}

View File

@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreAppProvider } from '@providers/app';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreGroupsProvider } from '@providers/groups'; import { CoreGroupsProvider } from '@providers/groups';
@ -25,6 +26,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreConfigProvider } from '@providers/config'; import { CoreConfigProvider } from '@providers/config';
import { ILocalNotification } from '@ionic-native/local-notifications'; import { ILocalNotification } from '@ionic-native/local-notifications';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { AddonCalendarOfflineProvider } from './calendar-offline';
/** /**
* Service to handle calendar events. * Service to handle calendar events.
@ -37,6 +39,7 @@ export class AddonCalendarProvider {
static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
static DEFAULT_NOTIFICATION_TIME = 60; static DEFAULT_NOTIFICATION_TIME = 60;
static NEW_EVENT_EVENT = 'addon_calendar_new_event'; static NEW_EVENT_EVENT = 'addon_calendar_new_event';
static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
static TYPE_CATEGORY = 'category'; static TYPE_CATEGORY = 'category';
static TYPE_COURSE = 'course'; static TYPE_COURSE = 'course';
static TYPE_GROUP = 'group'; static TYPE_GROUP = 'group';
@ -214,7 +217,8 @@ export class AddonCalendarProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider,
private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider,
private utils: CoreUtilsProvider) { private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider,
private appProvider: CoreAppProvider) {
this.logger = logger.getInstance('AddonCalendarProvider'); this.logger = logger.getInstance('AddonCalendarProvider');
this.sitesProvider.registerSiteSchema(this.siteSchema); this.sitesProvider.registerSiteSchema(this.siteSchema);
} }
@ -918,14 +922,58 @@ export class AddonCalendarProvider {
} }
/** /**
* Submit an event, either to create it or to edit it. * Submit a calendar event.
*
* @param {number} eventId ID of the event. If undefined/null, create a new event.
* @param {any} formData Form data.
* @param {number} [timeCreated] The time the event was created. Only if modifying a new offline event.
* @param {boolean} [forceOffline] True to always save it in offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<{sent: boolean, event: any}>} Promise resolved with the event and a boolean indicating if data was
* sent to server or stored in offline.
*/
submitEvent(eventId: number, formData: any, timeCreated?: number, forceOffline?: boolean, siteId?: string):
Promise<{sent: boolean, event: any}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Function to store the event to be synchronized later.
const storeOffline = (): Promise<{sent: boolean, event: any}> => {
return this.calendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) => {
return {sent: false, event: event};
});
};
if (forceOffline || !this.appProvider.isOnline()) {
// App is offline, store the event.
return storeOffline();
}
// If the event is already stored, discard it first.
return this.calendarOffline.deleteEvent(eventId, siteId).then(() => {
return this.submitEventOnline(eventId, formData, siteId).then((event) => {
return {sent: true, event: event};
}).catch((error) => {
if (error && !this.utils.isWebServiceError(error)) {
// Couldn't connect to server, store in offline.
return storeOffline();
} else {
// The WebService has thrown an error, reject.
return Promise.reject(error);
}
});
});
}
/**
* Submit an event, either to create it or to edit it. It will fail if offline or cannot connect.
* *
* @param {number} eventId ID of the event. If undefined/null, create a new event. * @param {number} eventId ID of the event. If undefined/null, create a new event.
* @param {any} formData Form data. * @param {any} formData Form data.
* @param {string} [siteId] Site ID. If not provided, current site. * @param {string} [siteId] Site ID. If not provided, current site.
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
submitEvent(eventId: number, formData: any, siteId?: string): Promise<any> { submitEventOnline(eventId: number, formData: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
// Add data that is "hidden" in web. // Add data that is "hidden" in web.
formData.id = eventId || 0; formData.id = eventId || 0;
@ -940,7 +988,7 @@ export class AddonCalendarProvider {
return site.write('core_calendar_submit_create_update_form', params).then((result) => { return site.write('core_calendar_submit_create_update_form', params).then((result) => {
if (result.validationerror) { if (result.validationerror) {
return Promise.reject(null); return Promise.reject(this.utils.createFakeWSError(''));
} }
return result.event; return result.event;

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
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';
/** /**
* Service that provides some features regarding lists of courses and categories. * Service that provides some features regarding lists of courses and categories.
@ -47,6 +48,20 @@ export class AddonCalendarHelperProvider {
e.icon = this.courseProvider.getModuleIconSrc(e.modulename); e.icon = this.courseProvider.getModuleIconSrc(e.modulename);
e.moduleIcon = e.icon; e.moduleIcon = e.icon;
} }
if (e.id < 0) {
// It's an offline event, add some calculated data.
e.format = 1;
e.visible = 1;
if (e.duration == 1) {
e.timeduration = e.timedurationuntil - e.timestart;
} else if (e.duration == 2) {
e.timeduration = e.timedurationminutes * CoreConstants.SECONDS_MINUTE;
} else {
e.timeduration = 0;
}
}
} }
/** /**

View File

@ -51,10 +51,10 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo
val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' + val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' +
this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date(); this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date();
this.search['f_' + this.field.id] = val.toISOString(); this.search['f_' + this.field.id] = this.timeUtils.toDatetimeFormat(val.getTime());
} else { } else {
val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date(); val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date();
val = val.toISOString(); val = this.timeUtils.toDatetimeFormat(val.getTime());
} }
this.addControl('f_' + this.field.id, val); this.addControl('f_' + this.field.id, val);

View File

@ -15,6 +15,7 @@ import { Injector, Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; import { AddonModDataFieldHandler } from '../../../providers/fields-delegate';
import { AddonModDataFieldDateComponent } from '../component/date'; import { AddonModDataFieldDateComponent } from '../component/date';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
/** /**
* Handler for date data field plugin. * Handler for date data field plugin.
@ -24,7 +25,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldDateHandler'; name = 'AddonModDataFieldDateHandler';
type = 'date'; type = 'date';
constructor(private translate: TranslateService) { } constructor(private translate: TranslateService, private timeUtils: CoreTimeUtilsProvider) { }
/** /**
* Return the Component to use to display the plugin data. * Return the Component to use to display the plugin data.
@ -129,7 +130,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || ''; input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || '';
originalFieldData = (originalFieldData && originalFieldData.content && originalFieldData = (originalFieldData && originalFieldData.content &&
new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || ''; this.timeUtils.toDatetimeFormat(originalFieldData.content * 1000).substr(0, 10)) || '';
return input != originalFieldData; return input != originalFieldData;
} }

View File

@ -1389,6 +1389,7 @@
"core.deleteduser": "Deleted user", "core.deleteduser": "Deleted user",
"core.deleting": "Deleting", "core.deleting": "Deleting",
"core.description": "Description", "core.description": "Description",
"core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS",
"core.dfdaymonthyear": "MM-DD-YYYY", "core.dfdaymonthyear": "MM-DD-YYYY",
"core.dfdayweekmonth": "ddd, D MMM", "core.dfdayweekmonth": "ddd, D MMM",
"core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A",

View File

@ -64,6 +64,7 @@
"deleteduser": "Deleted user", "deleteduser": "Deleted user",
"deleting": "Deleting", "deleting": "Deleting",
"description": "Description", "description": "Description",
"dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS",
"dfdaymonthyear": "MM-DD-YYYY", "dfdaymonthyear": "MM-DD-YYYY",
"dfdayweekmonth": "ddd, D MMM", "dfdayweekmonth": "ddd, D MMM",
"dffulldate": "dddd, D MMMM YYYY h[:]mm A", "dffulldate": "dddd, D MMMM YYYY h[:]mm A",

View File

@ -299,6 +299,18 @@ export class CoreTimeUtilsProvider {
return moment(timestamp).format(format); return moment(timestamp).format(format);
} }
/**
* Convert a timestamp to the format to set to a datetime input.
*
* @param {number} [timestamp] Timestamp to convert (in ms). If not provided, current time.
* @return {string} Formatted time.
*/
toDatetimeFormat(timestamp?: number): string {
timestamp = timestamp || Date.now();
return this.userDate(timestamp, 'core.dfdatetimeinput', false);
}
/** /**
* Convert a text into user timezone timestamp. * Convert a text into user timezone timestamp.
* *