From 7ccfade21e2c0c118b56928f1213a4158030b8a3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 21 Jun 2019 09:17:46 +0200 Subject: [PATCH] MOBILE-1927 calendar: Allow creating events in offline --- src/addon/calendar/calendar.module.ts | 3 + .../calendar/pages/edit-event/edit-event.html | 4 +- .../calendar/pages/edit-event/edit-event.ts | 75 ++++-- src/addon/calendar/pages/list/list.html | 21 +- src/addon/calendar/pages/list/list.ts | 69 ++++-- .../calendar/providers/calendar-offline.ts | 215 ++++++++++++++++++ src/addon/calendar/providers/calendar.ts | 56 ++++- src/addon/calendar/providers/helper.ts | 15 ++ .../mod/data/fields/date/component/date.ts | 4 +- .../mod/data/fields/date/providers/handler.ts | 5 +- src/assets/lang/en.json | 1 + src/lang/en.json | 1 + src/providers/utils/time.ts | 12 + 13 files changed, 437 insertions(+), 44 deletions(-) create mode 100644 src/addon/calendar/providers/calendar-offline.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 3146e6e69..c8131878a 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { AddonCalendarProvider } from './providers/calendar'; +import { AddonCalendarOfflineProvider } from './providers/calendar-offline'; import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; @@ -25,6 +26,7 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager'; // List of providers (without handlers). export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarProvider, + AddonCalendarOfflineProvider, AddonCalendarHelperProvider ]; @@ -35,6 +37,7 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [ ], providers: [ AddonCalendarProvider, + AddonCalendarOfflineProvider, AddonCalendarHelperProvider, AddonCalendarMainMenuHandler ] diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html index bfddc7633..0cf401536 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.html +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -44,7 +44,7 @@

{{ 'core.course' | translate }}

- {{ course.fullname }} +
@@ -54,7 +54,7 @@

{{ 'core.course' | translate }}

- {{ course.fullname }} +
diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index abb8f8b75..d6a6ec488 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -26,6 +26,7 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { CoreSite } from '@classes/site'; @@ -79,6 +80,7 @@ export class AddonCalendarEditEventPage implements OnInit { private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, private fb: FormBuilder, @Optional() private svComponent: CoreSplitViewComponent) { @@ -102,8 +104,10 @@ export class AddonCalendarEditEventPage implements OnInit { this.groupControl = 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('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('categoryid', this.fb.control('')); 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('location', this.fb.control('')); 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('repeat', this.fb.control(false)); 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. * + * @param {boolean} [refresh] Whether it's refreshing data. * @return {Promise} Promise resolved when done. */ - protected fetchData(): Promise { + protected fetchData(refresh?: boolean): Promise { let accessInfo; // Get access info. @@ -151,6 +156,33 @@ export class AddonCalendarEditEventPage implements OnInit { 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) { // Get the categories. promises.push(this.coursesProvider.getCategories(0, true).then((cats) => { @@ -185,11 +217,13 @@ export class AddonCalendarEditEventPage implements OnInit { } return Promise.all(promises).then(() => { - // Set event types. If course is allowed, select it first. - if (types.course) { - this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); - } else { - this.eventTypeControl.setValue(eventTypes[0].value); + if (!this.eventTypeControl.value) { + // Initialize event type value. If course is allowed, select it first. + if (types.course) { + this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); + } else { + this.eventTypeControl.setValue(eventTypes[0].value); + } } this.eventTypes = eventTypes; @@ -227,7 +261,7 @@ export class AddonCalendarEditEventPage implements OnInit { } Promise.all(promises).finally(() => { - this.fetchData().finally(() => { + this.fetchData(true).finally(() => { refresher.complete(); }); }); @@ -333,8 +367,8 @@ export class AddonCalendarEditEventPage implements OnInit { // Send the data. const modal = this.domUtils.showModalLoading('core.sending'); - this.calendarProvider.submitEvent(this.eventId, data).then((event) => { - this.returnToList(event); + this.calendarProvider.submitEvent(this.eventId, data).then((result) => { + this.returnToList(result.event); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'Error sending data.'); }).finally(() => { @@ -348,10 +382,14 @@ export class AddonCalendarEditEventPage implements OnInit { * @param {number} [event] Event. */ protected returnToList(event?: any): void { - const data: any = { - event: event - }; - this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + if (event) { + const data: any = { + event: event + }; + 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()) { // Empty form. @@ -369,7 +407,12 @@ export class AddonCalendarEditEventPage implements OnInit { */ discard(): void { 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(() => { // Cancelled. }); diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 97ae57d40..50a57e777 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -17,9 +17,28 @@ + + + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} + + + + + {{ 'core.notsent' | translate }} + + +

+

+ {{ event.timestart * 1000 | coreFormatDate: "strftimedatetimeshort" }} + - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }} +

+
+
+
+ @@ -43,7 +62,7 @@ - diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 237ad2457..d799f64c5 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -16,6 +16,7 @@ import { Component, ViewChild, OnDestroy } from '@angular/core'; import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -55,10 +56,12 @@ export class AddonCalendarListPage implements OnDestroy { protected eventId: number; protected preSelectedCourseId: number; protected newEventObserver: any; + protected discardedObserver: any; courses: any[]; eventsLoaded = false; events = []; + offlineEvents = []; notificationsEnabled = false; filteredEvents = []; canLoadMore = false; @@ -67,12 +70,14 @@ export class AddonCalendarListPage implements OnDestroy { course: this.allCourses }; canCreate = false; + hasOffline = false; constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, 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.notificationsEnabled = localNotificationsProvider.isAvailable(); @@ -87,7 +92,7 @@ export class AddonCalendarListPage implements OnDestroy { this.eventId = navParams.get('eventId') || false; 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) => { if (data && data.event) { if (this.splitviewCtrl.isOn()) { @@ -97,19 +102,25 @@ export class AddonCalendarListPage implements OnDestroy { this.eventsLoaded = false; this.refreshEvents(false).finally(() => { - this.eventsLoaded = true; - // In tablet mode try to open the event. - if (this.splitviewCtrl.isOn()) { - if (data.event.id) { - this.gotoEvent(data.event.id); - } else { - // It's an offline event. - } + // In tablet mode try to open the event (only if it's an online event). + if (this.splitviewCtrl.isOn() && data.event.id > 0) { + this.gotoEvent(data.event.id); } }); } }, 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. this.gotoEvent(this.events[0].id); } - }).finally(() => { - this.eventsLoaded = true; }); } @@ -167,7 +176,18 @@ export class AddonCalendarListPage implements OnDestroy { 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(); } } else { + events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Sort the events by timestart, they're ordered by id. events.sort((a, b) => { if (a.timestart == b.timestart) { @@ -203,7 +225,6 @@ export class AddonCalendarListPage implements OnDestroy { return a.timestart - b.timestart; }); - events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); this.getCategories = this.shouldLoadCategories(events); 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 = {}; + + if (eventId) { + params.eventId = eventId; + } if (this.filter.course.id != this.allCourses.id) { params.courseId = this.filter.course.id; } @@ -452,7 +479,15 @@ export class AddonCalendarListPage implements OnDestroy { */ gotoEvent(eventId: number): void { 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 + }); + } } /** diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts new file mode 100644 index 000000000..c720215bc --- /dev/null +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -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} Promise resolved if deleted, rejected if failure. + */ + deleteEvent(eventId: number, siteId?: string): Promise { + 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} Promise resolved with events. + */ + getAllEvents(siteId?: string): Promise { + 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} Promise resolved with the event. + */ + getEvent(eventId: number, siteId?: string): Promise { + 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} Promise resolved with boolean: true if has offline events, false otherwise. + */ + hasEvents(siteId?: string): Promise { + 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} Promise resolved with the stored event. + */ + saveEvent(eventId: number, data: any, timeCreated?: number, siteId?: string): Promise { + 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; + }); + }); + } +} diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 23f664371..6e5bc965b 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreAppProvider } from '@providers/app'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; @@ -25,6 +26,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; +import { AddonCalendarOfflineProvider } from './calendar-offline'; /** * Service to handle calendar events. @@ -37,6 +39,7 @@ export class AddonCalendarProvider { static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME = 60; static NEW_EVENT_EVENT = 'addon_calendar_new_event'; + static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; static TYPE_CATEGORY = 'category'; static TYPE_COURSE = 'course'; static TYPE_GROUP = 'group'; @@ -214,7 +217,8 @@ export class AddonCalendarProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, - private utils: CoreUtilsProvider) { + private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider, + private appProvider: CoreAppProvider) { this.logger = logger.getInstance('AddonCalendarProvider'); 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 {any} formData Form data. * @param {string} [siteId] Site ID. If not provided, current site. * @return {Promise} Promise resolved when done. */ - submitEvent(eventId: number, formData: any, siteId?: string): Promise { + submitEventOnline(eventId: number, formData: any, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { // Add data that is "hidden" in web. formData.id = eventId || 0; @@ -940,7 +988,7 @@ export class AddonCalendarProvider { return site.write('core_calendar_submit_create_update_form', params).then((result) => { if (result.validationerror) { - return Promise.reject(null); + return Promise.reject(this.utils.createFakeWSError('')); } return result.event; diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 818feadf1..30d4b9521 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; +import { CoreConstants } from '@core/constants'; /** * 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.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; + } + } } /** diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts index ff0ab6ded..c846032a8 100644 --- a/src/addon/mod/data/fields/date/component/date.ts +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -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'] + '-' + 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 { 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); diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts index f36d166b7..bf9d65079 100644 --- a/src/addon/mod/data/fields/date/providers/handler.ts +++ b/src/addon/mod/data/fields/date/providers/handler.ts @@ -15,6 +15,7 @@ import { Injector, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; import { AddonModDataFieldDateComponent } from '../component/date'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; /** * Handler for date data field plugin. @@ -24,7 +25,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { name = 'AddonModDataFieldDateHandler'; type = 'date'; - constructor(private translate: TranslateService) { } + constructor(private translate: TranslateService, private timeUtils: CoreTimeUtilsProvider) { } /** * 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) || ''; 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; } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 2ed0480e9..14730d64b 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1389,6 +1389,7 @@ "core.deleteduser": "Deleted user", "core.deleting": "Deleting", "core.description": "Description", + "core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "core.dfdaymonthyear": "MM-DD-YYYY", "core.dfdayweekmonth": "ddd, D MMM", "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/lang/en.json b/src/lang/en.json index b5c79cb94..714b64a15 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -64,6 +64,7 @@ "deleteduser": "Deleted user", "deleting": "Deleting", "description": "Description", + "dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "dfdaymonthyear": "MM-DD-YYYY", "dfdayweekmonth": "ddd, D MMM", "dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index f7c2de9f6..020860908 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -299,6 +299,18 @@ export class CoreTimeUtilsProvider { 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. *