From fa837532d4557ef9f3c2a4886be1419fa6a850ca Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 1 Jul 2019 10:24:14 +0200 Subject: [PATCH 01/12] MOBILE-3021 calendar: Go to new page if monthly view supported --- src/addon/calendar/calendar.module.ts | 8 +- src/addon/calendar/pages/index/index.html | 28 +++ .../calendar/pages/index/index.module.ts | 35 +++ src/addon/calendar/pages/index/index.ts | 38 ++++ src/addon/calendar/providers/calendar.ts | 202 ++++++++++++++++++ .../calendar/providers/mainmenu-handler.ts | 2 +- 6 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 src/addon/calendar/pages/index/index.html create mode 100644 src/addon/calendar/pages/index/index.module.ts create mode 100644 src/addon/calendar/pages/index/index.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index c8f9cef82..66ab9f237 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -70,7 +70,13 @@ export class AddonCalendarModule { return; } - loginHelper.redirect('AddonCalendarListPage', {eventId: data.eventid}, data.siteId); + // Check which page we should load. + calendarProvider.canViewMonth(data.siteId).then((canView) => { + const pageName = canView ? 'AddonCalendarIndexPage' : 'AddonCalendarListPage'; + + loginHelper.redirect(pageName, {eventId: data.eventid}, data.siteId); + }); + }); }); } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html new file mode 100644 index 000000000..122436442 --- /dev/null +++ b/src/addon/calendar/pages/index/index.html @@ -0,0 +1,28 @@ + + + {{ 'addon.calendar.calendarevents' | translate }} + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addon/calendar/pages/index/index.module.ts b/src/addon/calendar/pages/index/index.module.ts new file mode 100644 index 000000000..0c3486dd9 --- /dev/null +++ b/src/addon/calendar/pages/index/index.module.ts @@ -0,0 +1,35 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonCalendarIndexPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonCalendarIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarIndexPageModule {} diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts new file mode 100644 index 000000000..3c1be5ed2 --- /dev/null +++ b/src/addon/calendar/pages/index/index.ts @@ -0,0 +1,38 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; + +/** + * Page that displays the calendar events. + */ +@IonicPage({ segment: 'addon-calendar-index' }) +@Component({ + selector: 'page-addon-calendar-index', + templateUrl: 'index.html', +}) +export class AddonCalendarIndexPage implements OnInit { + + constructor() { + // @todo + } + + /** + * View loaded. + */ + ngOnInit(): void { + // @todo + } +} diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 9e34520a1..fdc4905d3 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -237,6 +237,8 @@ export class AddonCalendarProvider { canDeleteEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return this.canDeleteEventsInSite(site); + }).catch(() => { + return false; }); } @@ -263,6 +265,8 @@ export class AddonCalendarProvider { canEditEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return this.canEditEventsInSite(site); + }).catch(() => { + return false; }); } @@ -280,6 +284,34 @@ export class AddonCalendarProvider { return site.isVersionGreaterEqualThan('3.7.1'); } + /** + * Check if a certain site allows viewing events in monthly view. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if monthly view is supported. + * @since 3.4 + */ + canViewMonth(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canViewMonthInSite(site); + }).catch(() => { + return false; + }); + } + + /** + * Check if a certain site allows viewing events in monthly view. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether monthly view is supported. + * @since 3.4 + */ + canViewMonthInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_calendar_get_calendar_monthly_view'); + } + /** * Removes expired events from local DB. * @@ -723,6 +755,126 @@ export class AddonCalendarProvider { return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval; } + /** + * Get monthly calendar events. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = { + year: year, + month: month, + mini: 1 // Set mini to 1 to prevent returning the course selector HTML. + }; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getMonthlyEventsCacheKey(year, month, courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_monthly_view', data, preSets); + }); + } + + /** + * Get prefix cache key for monthly events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getMonthlyEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'monthly:'; + } + + /** + * Get prefix cache key for a certain month for monthly events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @return {string} Prefix Cache key. + */ + protected getMonthlyEventsMonthPrefixCacheKey(year: number, month: number): string { + return this.getMonthlyEventsPrefixCacheKey() + year + ':' + month + ':'; + } + + /** + * Get cache key for monthly events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getMonthlyEventsCacheKey(year: number, month: number, courseId?: number, categoryId?: number): string { + return this.getMonthlyEventsMonthPrefixCacheKey(year, month) + (courseId ? courseId : '') + ':' + + (categoryId ? categoryId : ''); + } + + /** + * Get upcoming calendar events. + * + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = {}; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getUpcomingEventsCacheKey(courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_upcoming_view', data, preSets); + }); + } + + /** + * Get prefix cache key for upcoming events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getUpcomingEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'upcoming:'; + } + + /** + * Get cache key for upcoming events WS calls. + * + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getUpcomingEventsCacheKey(courseId?: number, categoryId?: number): string { + return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : ''); + } + /** * Invalidates access information. * @@ -782,6 +934,56 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates monthly events for all months. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllMonthlyEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getMonthlyEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates monthly events for a certain months. + * + * @param {number} year Year. + * @param {number} month Month. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateMonthlyEvents(year: number, month: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getMonthlyEventsMonthPrefixCacheKey(year, month)); + }); + } + + /** + * Invalidates upcoming events for all courses and categories. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllUpcomingEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getUpcomingEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates upcoming events for a certain course or category. + * + * @param {number} [courseId] Course ID. + * @param {number} [categoryId] Category ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getUpcomingEventsCacheKey(courseId, categoryId)); + }); + } + /** * Check if Calendar is disabled in a certain site. * diff --git a/src/addon/calendar/providers/mainmenu-handler.ts b/src/addon/calendar/providers/mainmenu-handler.ts index 2769f0e0b..0568eeb01 100644 --- a/src/addon/calendar/providers/mainmenu-handler.ts +++ b/src/addon/calendar/providers/mainmenu-handler.ts @@ -44,7 +44,7 @@ export class AddonCalendarMainMenuHandler implements CoreMainMenuHandler { return { icon: 'calendar', title: 'addon.calendar.calendar', - page: 'AddonCalendarListPage', + page: this.calendarProvider.canViewMonthInSite() ? 'AddonCalendarIndexPage' : 'AddonCalendarListPage', class: 'addon-calendar-handler' }; } From 661709acb47339b5dc11c6eb7c794b73ed62167f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 3 Jul 2019 08:13:02 +0200 Subject: [PATCH 02/12] MOBILE-3021 calendar: Support monthly view --- scripts/langindex.json | 15 ++ .../calendar/addon-calendar-calendar.html | 52 +++++ .../components/calendar/calendar.scss | 42 ++++ .../calendar/components/calendar/calendar.ts | 221 ++++++++++++++++++ .../calendar/components/components.module.ts | 40 ++++ src/addon/calendar/lang/en.json | 16 +- src/addon/calendar/pages/index/index.html | 3 +- .../calendar/pages/index/index.module.ts | 2 + src/addon/calendar/pages/index/index.ts | 159 ++++++++++++- src/addon/calendar/pages/list/list.ts | 48 +--- src/addon/calendar/providers/calendar.ts | 43 ++++ src/addon/calendar/providers/helper.ts | 74 +++++- src/assets/lang/en.json | 15 ++ src/lang/en.json | 1 + src/theme/variables.scss | 1 + 15 files changed, 678 insertions(+), 54 deletions(-) create mode 100644 src/addon/calendar/components/calendar/addon-calendar-calendar.html create mode 100644 src/addon/calendar/components/calendar/calendar.scss create mode 100644 src/addon/calendar/components/calendar/calendar.ts create mode 100644 src/addon/calendar/components/components.module.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 40cfc905d..2f79cd0a0 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -106,9 +106,13 @@ "addon.calendar.eventname": "calendar", "addon.calendar.eventstarttime": "calendar", "addon.calendar.eventtype": "calendar", + "addon.calendar.fri": "calendar", + "addon.calendar.friday": "calendar", "addon.calendar.gotoactivity": "calendar", "addon.calendar.invalidtimedurationminutes": "calendar", "addon.calendar.invalidtimedurationuntil": "calendar", + "addon.calendar.mon": "calendar", + "addon.calendar.monday": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", @@ -118,7 +122,15 @@ "addon.calendar.repeateditthis": "calendar", "addon.calendar.repeatevent": "calendar", "addon.calendar.repeatweeksl": "calendar", + "addon.calendar.sat": "calendar", + "addon.calendar.saturday": "calendar", "addon.calendar.setnewreminder": "local_moodlemobileapp", + "addon.calendar.sun": "calendar", + "addon.calendar.sunday": "calendar", + "addon.calendar.thu": "calendar", + "addon.calendar.thursday": "calendar", + "addon.calendar.tue": "calendar", + "addon.calendar.tuesday": "calendar", "addon.calendar.typecategory": "calendar", "addon.calendar.typeclose": "calendar", "addon.calendar.typecourse": "calendar", @@ -128,6 +140,8 @@ "addon.calendar.typeopen": "calendar", "addon.calendar.typesite": "calendar", "addon.calendar.typeuser": "calendar", + "addon.calendar.wed": "calendar", + "addon.calendar.wednesday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", "addon.competency.competenciesmostoftennotproficientincourse": "tool_lp", @@ -1657,6 +1671,7 @@ "core.notingroup": "moodle", "core.notsent": "local_moodlemobileapp", "core.now": "moodle", + "core.nummore": "local_moodlemobileapp", "core.numwords": "moodle", "core.offline": "message", "core.ok": "moodle", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html new file mode 100644 index 000000000..04d35dfda --- /dev/null +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -0,0 +1,52 @@ + + + + + + + + + + +

{{ periodName }}

+
+ + + + + +
+
+ + + + + + +

{{ day.shortname | translate }}

+
+
+ + + + + +

{{ day.mday }}

+ + +

+ + +
+

+ + {{event.name}} +

+

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

+
+
+ +
+
+ +
diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss new file mode 100644 index 000000000..853cff23a --- /dev/null +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -0,0 +1,42 @@ + +$calendar-event-category-color: $purple !default; // Purple. +$calendar-event-course-color: $red !default; // Red. +$calendar-event-group-color: $yellow !default; // Yellow. +$calendar-event-user-color: $blue !default; // Blue. +$calendar-event-site-color: $green !default; // Green. + +ion-app.app-root addon-calendar-calendar { + + .addon-calendar-weekdays { + opacity: 0.4; + } + + .addon-calendar-day-events { + @include text-align('start'); + } + + .calendar_event_type { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + border: 1px solid white; + @include margin-horizontal(1px, 1px); + + &.calendar_event_category { + background-color: $calendar-event-category-color; + } + &.calendar_event_course { + background-color: $calendar-event-course-color; + } + &.calendar_event_group { + background-color: $calendar-event-group-color; + } + &.calendar_event_user { + background-color: $calendar-event-user-color; + } + &.calendar_event_site { + background-color: $calendar-event-site-color; + } + } +} \ No newline at end of file diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts new file mode 100644 index 000000000..9bb41721e --- /dev/null +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -0,0 +1,221 @@ +// (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 { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; + +/** + * Component that displays a calendar. + */ +@Component({ + selector: 'addon-calendar-calendar', + templateUrl: 'addon-calendar-calendar.html', +}) +export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDestroy { + @Input() initialYear: number | string; // Initial year to load. + @Input() initialMonth: number | string; // Initial month to load. + @Input() courseId: number | string; + @Input() categoryId: number | string; // Category ID the course belongs to. + @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. + + periodName: string; + weekDays: any[]; + weeks: any[]; + loaded = false; + + protected year: number; + protected month: number; + protected categoriesRetrieved = false; + protected categories = {}; + + constructor(eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private utils: CoreUtilsProvider, + private coursesProvider: CoreCoursesProvider) { + + } + + /** + * Component loaded. + */ + ngOnInit(): void { + const now = new Date(); + + this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear(); + this.month = this.initialYear ? Number(this.initialYear) : now.getMonth() + 1; + this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); + + this.fetchData(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + + if ((changes.courseId || changes.categoryId) && this.weeks) { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; + + this.filterEvents(courseId, categoryId); + } + } + + /** + * Fetch contacts. + * + * @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined, + promises = []; + + promises.push(this.loadCategories()); + + promises.push(this.calendarProvider.getMonthlyEvents(this.year, this.month, courseId, categoryId).then((result) => { + + // Calculate the period name. We don't use the one in result because it's in server's language. + this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear'); + + this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); + this.weeks = result.weeks; + + this.filterEvents(courseId, categoryId); + })); + + return Promise.all(promises).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + if (this.categoriesRetrieved) { + // Already retrieved, stop. + return Promise.resolve(); + } + + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categoriesRetrieved = true; + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * 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 { + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + if (!courseId || courseId < 0) { + day.filteredEvents = day.events; + } else { + day.filteredEvents = day.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories); + }); + } + + // Re-calculate some properties. + this.calendarHelper.calculateDayData(day, day.filteredEvents); + }); + }); + } + + /** + * Refresh events. + * + * @return {Promise} Promise resolved when done. + */ + refreshData(): Promise { + const promises = []; + + promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + + this.categoriesRetrieved = false; // Get categories again. + + return Promise.all(promises).then(() => { + return this.fetchData(true); + }); + } + + /** + * Load next month. + */ + loadNext(): void { + if (this.month === 12) { + this.month = 1; + this.year++; + } else { + this.month++; + } + + this.loaded = false; + + this.fetchData(); + } + + /** + * Load previous month. + */ + loadPrevious(): void { + if (this.month === 1) { + this.month = 12; + this.year--; + } else { + this.month--; + } + + this.loaded = false; + + this.fetchData(); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + // @todo + } +} diff --git a/src/addon/calendar/components/components.module.ts b/src/addon/calendar/components/components.module.ts new file mode 100644 index 000000000..a6d5afe22 --- /dev/null +++ b/src/addon/calendar/components/components.module.ts @@ -0,0 +1,40 @@ +// (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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonCalendarCalendarComponent } from '../components/calendar/calendar'; + +@NgModule({ + declarations: [ + AddonCalendarCalendarComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + ], + exports: [ + AddonCalendarCalendarComponent + ] +}) +export class AddonCalendarComponentsModule {} diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 8792b48c5..412c81742 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -22,9 +22,13 @@ "eventname": "Event title", "eventstarttime": "Start time", "eventtype": "Event type", + "fri": "Fri", + "friday": "Friday", "gotoactivity": "Go to activity", "invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "mon": "Mon", + "monday": "Monday", "newevent": "New event", "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -34,7 +38,15 @@ "repeateditthis": "Apply changes to this event only", "repeatevent": "Repeat this event", "repeatweeksl": "Repeat weekly, creating altogether", + "sat": "Sat", + "saturday": "Saturday", "setnewreminder": "Set a new reminder", + "sun": "Sun", + "sunday": "Sunday", + "thu": "Thu", + "thursday": "Thursday", + "tue": "Tue", + "tuesday": "Tuesday", "typeclose": "Close event", "typecourse": "Course event", "typecategory": "Category event", @@ -43,5 +55,7 @@ "typegroup": "Group event", "typeopen": "Open event", "typesite": "Site event", - "typeuser": "User event" + "typeuser": "User event", + "wed": "Wed", + "wednesday": "Wednesday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 122436442..42c3cd4b9 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -15,9 +15,8 @@ - - + diff --git a/src/addon/calendar/pages/index/index.module.ts b/src/addon/calendar/pages/index/index.module.ts index 0c3486dd9..bf925e799 100644 --- a/src/addon/calendar/pages/index/index.module.ts +++ b/src/addon/calendar/pages/index/index.module.ts @@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarComponentsModule } from '../../components/components.module'; import { AddonCalendarIndexPage } from './index'; @NgModule({ @@ -28,6 +29,7 @@ import { AddonCalendarIndexPage } from './index'; CoreComponentsModule, CoreDirectivesModule, CorePipesModule, + AddonCalendarComponentsModule, IonicPageModule.forChild(AddonCalendarIndexPage), TranslateModule.forChild() ], diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 3c1be5ed2..9ab19cd7e 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -8,12 +8,20 @@ // // 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. +// WITHOUT WARRANTIES OR CONDITIONS OFx ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; -import { IonicPage } from 'ionic-angular'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; +import { TranslateService } from '@ngx-translate/core'; /** * Page that displays the calendar events. @@ -24,15 +32,154 @@ import { IonicPage } from 'ionic-angular'; templateUrl: 'index.html', }) export class AddonCalendarIndexPage implements OnInit { + @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; - constructor() { - // @todo + protected allCourses = { + id: -1, + fullname: this.translate.instant('core.fulllistofcourses'), + category: -1 + }; + + courseId: number; + categoryId: number; + canCreate = false; + courses: any[]; + notificationsEnabled = false; + loaded = false; + + constructor(localNotificationsProvider: CoreLocalNotificationsProvider, + navParams: NavParams, + private navCtrl: NavController, + private domUtils: CoreDomUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private translate: TranslateService, + private coursesProvider: CoreCoursesProvider, + private popoverCtrl: PopoverController) { + + this.courseId = navParams.get('courseId'); + this.notificationsEnabled = localNotificationsProvider.isAvailable(); } /** * View loaded. */ ngOnInit(): void { - // @todo + this.fetchData(); + } + + /** + * Fetch all the data required for the view. + * + * @return {Promise} Promise resolved when done. + */ + fetchData(): Promise { + const promises = []; + + // 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; + })); + + return Promise.all(promises).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any): void { + if (!this.loaded) { + return; + } + + const promises = []; + + promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { + return this.fetchData(); + })); + + // Refresh the sub-component. + promises.push(this.calendarComponent.refreshData()); + + Promise.all(promises).finally(() => { + refresher && refresher.complete(); + }); + } + + /** + * Show the context menu. + * + * @param {MouseEvent} event Event. + */ + openCourseFilter(event: MouseEvent): void { + const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { + courses: this.courses, + courseId: this.courseId + }); + + popover.onDidDismiss((course) => { + if (course) { + this.courseId = course.id > 0 ? course.id : undefined; + this.categoryId = course.id > 0 ? course.category : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); + } + }); + popover.present({ + ev: event + }); + } + + /** + * Open page to create/edit an event. + * + * @param {number} [eventId] Event ID to edit. + */ + openEdit(eventId?: number): void { + const params: any = {}; + + if (eventId) { + params.eventId = eventId; + } + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarEditEventPage', params); + } + + /** + * Open calendar events settings. + */ + openSettings(): void { + this.navCtrl.push('AddonCalendarSettingsPage'); } } diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 0dac7f74b..00c6657d4 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -423,50 +423,10 @@ export class AddonCalendarListPage implements OnDestroy { return this.events; } - return this.events.filter(this.shouldDisplayEvent.bind(this)); - } - - /** - * Check if an event should be displayed based on the filter. - * - * @param {any} event Event object. - * @return {boolean} Whether it should be displayed. - */ - protected shouldDisplayEvent(event: any): boolean { - if (event.eventtype == 'user' || event.eventtype == 'site') { - // User or site event, display it. - return true; - } - - if (event.eventtype == 'category') { - if (!event.categoryid || !Object.keys(this.categories).length) { - // We can't tell if the course belongs to the category, display them all. - return true; - } - if (event.categoryid == this.filter.course.category) { - // The event is in the same category as the course, display it. - return true; - } - - // Check parent categories. - let category = this.categories[this.filter.course.category]; - while (category) { - if (!category.parent) { - // Category doesn't have parent, stop. - break; - } - - if (event.categoryid == category.parent) { - return true; - } - category = this.categories[category.parent]; - } - - return false; - } - - // Show the event if it is from site home or if it matches the selected course. - return event.courseid === this.siteHomeId || event.courseid == this.filter.course.id; + return this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, this.filter.course.id, this.filter.course.category, + this.categories); + }); } /** diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index fdc4905d3..4705a8ad8 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -51,6 +51,37 @@ export class AddonCalendarProvider { static TYPE_USER = 'user'; protected ROOT_CACHE_KEY = 'mmaCalendar:'; + protected weekDays = [ + { + shortname: 'addon.calendar.sun', + fullname: 'addon.calendar.sunday' + }, + { + shortname: 'addon.calendar.mon', + fullname: 'addon.calendar.monday' + }, + { + shortname: 'addon.calendar.tue', + fullname: 'addon.calendar.tuesday' + }, + { + shortname: 'addon.calendar.wed', + fullname: 'addon.calendar.wednesday' + }, + { + shortname: 'addon.calendar.thu', + fullname: 'addon.calendar.thursday' + }, + { + shortname: 'addon.calendar.fri', + fullname: 'addon.calendar.friday' + }, + { + shortname: 'addon.calendar.sat', + fullname: 'addon.calendar.saturday' + } + ]; + // Variables for database. static EVENTS_TABLE = 'addon_calendar_events_2'; static REMINDERS_TABLE = 'addon_calendar_reminders'; @@ -875,6 +906,18 @@ export class AddonCalendarProvider { return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : ''); } + /** + * Get the week days, already ordered according to a specified starting day. + * + * @param {number} [startingDay=0] Starting day. 0=Sunday, 1=Monday, ... + * @return {any[]} Week days. + */ + getWeekDays(startingDay?: number): any[] { + startingDay = startingDay || 0; + + return this.weekDays.slice(startingDay).concat(this.weekDays.slice(0, startingDay)); + } + /** * Invalidates access information. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index ae857225d..94cfa1e93 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; @@ -33,11 +34,35 @@ export class AddonCalendarHelperProvider { category: 'fa-cubes' }; - constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider, + constructor(logger: CoreLoggerProvider, + private courseProvider: CoreCourseProvider, + private sitesProvider: CoreSitesProvider, private calendarProvider: AddonCalendarProvider) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } + /** + * Calculate some day data based on a list of events for that day. + * + * @param {any} day Day. + * @param {any[]} events Events. + */ + calculateDayData(day: any, events: any[]): void { + day.hasevents = events.length > 0; + day.haslastdayofevent = false; + + const types = {}; + events.forEach((event) => { + types[event.eventtype] = true; + + if (event.islastday) { + day.haslastdayofevent = true; + } + }); + + day.calendareventtypes = Object.keys(types); + } + /** * Check if current user can create/edit events. * @@ -155,4 +180,51 @@ export class AddonCalendarHelperProvider { return false; } + + /** + * Check if an event should be displayed based on the filter. + * + * @param {any} event Event object. + * @param {number} courseId Course ID to filter. + * @param {number} categoryId Category ID the course belongs to. + * @param {any} categories Categories indexed by ID. + * @return {boolean} Whether it should be displayed. + */ + shouldDisplayEvent(event: any, courseId: number, categoryId: number, categories: any): boolean { + if (event.eventtype == 'user' || event.eventtype == 'site') { + // User or site event, display it. + return true; + } + + if (event.eventtype == 'category') { + if (!event.categoryid || !Object.keys(categories).length) { + // We can't tell if the course belongs to the category, display them all. + return true; + } + + if (event.categoryid == categoryId) { + // The event is in the same category as the course, display it. + return true; + } + + // Check parent categories. + let category = categories[categoryId]; + while (category) { + if (!category.parent) { + // Category doesn't have parent, stop. + break; + } + + if (event.categoryid == category.parent) { + return true; + } + category = categories[category.parent]; + } + + return false; + } + + // Show the event if it is from site home or if it matches the selected course. + return event.courseid === this.sitesProvider.getSiteHomeId() || event.courseid == courseId; + } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 19cb0008d..3e4a0a984 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -106,9 +106,13 @@ "addon.calendar.eventname": "Event title", "addon.calendar.eventstarttime": "Start time", "addon.calendar.eventtype": "Event type", + "addon.calendar.fri": "Fri", + "addon.calendar.friday": "Friday", "addon.calendar.gotoactivity": "Go to activity", "addon.calendar.invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "addon.calendar.mon": "Mon", + "addon.calendar.monday": "Monday", "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -118,7 +122,15 @@ "addon.calendar.repeateditthis": "Apply changes to this event only", "addon.calendar.repeatevent": "Repeat this event", "addon.calendar.repeatweeksl": "Repeat weekly, creating altogether", + "addon.calendar.sat": "Sat", + "addon.calendar.saturday": "Saturday", "addon.calendar.setnewreminder": "Set a new reminder", + "addon.calendar.sun": "Sun", + "addon.calendar.sunday": "Sunday", + "addon.calendar.thu": "Thu", + "addon.calendar.thursday": "Thursday", + "addon.calendar.tue": "Tue", + "addon.calendar.tuesday": "Tuesday", "addon.calendar.typecategory": "Category event", "addon.calendar.typeclose": "Close event", "addon.calendar.typecourse": "Course event", @@ -128,6 +140,8 @@ "addon.calendar.typeopen": "Open event", "addon.calendar.typesite": "Site event", "addon.calendar.typeuser": "User event", + "addon.calendar.wed": "Wed", + "addon.calendar.wednesday": "Wednesday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", "addon.competency.competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course", @@ -1658,6 +1672,7 @@ "core.notingroup": "Sorry, but you need to be part of a group to see this page.", "core.notsent": "Not sent", "core.now": "now", + "core.nummore": "{{$a}} more", "core.numwords": "{{$a}} words", "core.offline": "Offline", "core.ok": "OK", diff --git a/src/lang/en.json b/src/lang/en.json index 4ec6a60a4..4f1d092cf 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -184,6 +184,7 @@ "notingroup": "Sorry, but you need to be part of a group to see this page.", "notsent": "Not sent", "now": "now", + "nummore": "{{$a}} more", "numwords": "{{$a}} words", "offline": "Offline", "ok": "OK", diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 919afa355..0062ebe09 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -28,6 +28,7 @@ $green: #5e8100; // Accent. $red: #cb3d4d; $orange: #f98012; // Accent (never text). $yellow: #fbad1a; // Accent (never text). +$purple: #8e24aa; // Accent (never text). $core-color: $orange; // Branded apps customization From b202d92dc35ea79c3930c3c1465d3202b1529405 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 3 Jul 2019 15:59:27 +0200 Subject: [PATCH 03/12] MOBILE-3021 calendar: Display offline events too --- .../calendar/addon-calendar-calendar.html | 16 +- .../components/calendar/calendar.scss | 9 + .../calendar/components/calendar/calendar.ts | 229 ++++++++++++++--- src/addon/calendar/pages/index/index.html | 8 +- src/addon/calendar/pages/index/index.ts | 240 +++++++++++++++--- .../calendar/providers/calendar-offline.ts | 12 + src/addon/calendar/providers/helper.ts | 49 +++- 7 files changed, 494 insertions(+), 69 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 04d35dfda..a866c4be2 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -19,7 +19,7 @@ - + @@ -38,11 +38,15 @@
-

- - {{event.name}} -

-

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

+ +

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

+
+

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

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index 853cff23a..c3a20bf9e 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -13,6 +13,15 @@ ion-app.app-root addon-calendar-calendar { .addon-calendar-day-events { @include text-align('start'); + + ion-icon { + @include margin-horizontal(null, 2px); + font-size: 1em; + } + } + + .addon-calendar-event { + cursor: pointer; } .calendar_event_type { diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 9bb41721e..053863483 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange } from '@angular/core'; +import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -20,6 +20,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; /** @@ -35,6 +36,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() courseId: number | string; @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. + @Output() onEventClicked = new EventEmitter(); periodName: string; weekDays: any[]; @@ -45,16 +47,39 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest protected month: number; protected categoriesRetrieved = false; protected categories = {}; + protected currentSiteId: string; + protected offlineEvents: {[monthId: string]: {[day: number]: any[]}} = {}; // Offline events classified in month & day. + protected offlineEditedEventsIds = []; // IDs of events edited in offline. + protected deletedEvents = []; // Events deleted in offline. + + // Observers. + protected undeleteEventObserver: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarOffline: AddonCalendarOfflineProvider, private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private coursesProvider: CoreCoursesProvider) { + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + this.undeleteEvent(data.eventId); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + } + }, this.currentSiteId); } /** @@ -76,27 +101,63 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest ngOnChanges(changes: {[name: string]: SimpleChange}): void { if ((changes.courseId || changes.categoryId) && this.weeks) { - const courseId = this.courseId ? Number(this.courseId) : undefined, - categoryId = this.categoryId ? Number(this.categoryId) : undefined; - - this.filterEvents(courseId, categoryId); + this.filterEvents(); } } /** * Fetch contacts. * - * @param {boolean} [refresh=false] True if we are refreshing contacts, false if we are loading more. + * @param {boolean} [refresh=false] True if we are refreshing events. * @return {Promise} Promise resolved when done. */ fetchData(refresh: boolean = false): Promise { - const courseId = this.courseId ? Number(this.courseId) : undefined, - categoryId = this.categoryId ? Number(this.categoryId) : undefined, - promises = []; + const promises = []; promises.push(this.loadCategories()); - promises.push(this.calendarProvider.getMonthlyEvents(this.year, this.month, courseId, categoryId).then((result) => { + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + + // Classify them by month. + this.offlineEvents = this.calendarHelper.classifyIntoMonths(events); + + // Get the IDs of events edited in offline. + const filtered = events.filter((event) => { + return event.id > 0; + }); + this.offlineEditedEventsIds = filtered.map((event) => { + return event.id; + }); + })); + + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; + })); + + return Promise.all(promises).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch the events for current month. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getMonthlyEvents(this.year, this.month).then((result) => { // Calculate the period name. We don't use the one in result because it's in server's language. this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear'); @@ -104,13 +165,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno); this.weeks = result.weeks; - this.filterEvents(courseId, categoryId); - })); + // Merge the online events with offline data. + this.mergeEvents(); - return Promise.all(promises).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); - }).finally(() => { - this.loaded = true; + // Filter events by course. + this.filterEvents(); }); } @@ -140,11 +199,10 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Filter events to only display events belonging to a certain course. - * - * @param {number} courseId Course ID. - * @param {number} categoryId Category the course belongs to. */ - filterEvents(courseId: number, categoryId: number): void { + filterEvents(): void { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; this.weeks.forEach((week) => { week.days.forEach((day) => { @@ -165,9 +223,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest /** * Refresh events. * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - refreshData(): Promise { + refreshData(sync?: boolean, showErrors?: boolean): Promise { const promises = []; promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); @@ -184,38 +244,145 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest * Load next month. */ loadNext(): void { - if (this.month === 12) { - this.month = 1; - this.year++; - } else { - this.month++; - } + this.increaseMonth(); this.loaded = false; - this.fetchData(); + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.decreaseMonth(); + }).finally(() => { + this.loaded = true; + }); } /** * Load previous month. */ loadPrevious(): void { + this.decreaseMonth(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.increaseMonth(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * An event was clicked. + * + * @param {any} event Event. + */ + eventClicked(event: any): void { + this.onEventClicked.emit(event.id); + } + + /** + * Decrease the current month. + */ + protected decreaseMonth(): void { if (this.month === 1) { this.month = 12; this.year--; } else { this.month--; } + } - this.loaded = false; + /** + * Increase the current month. + */ + protected increaseMonth(): void { + if (this.month === 12) { + this.month = 1; + this.year++; + } else { + this.month++; + } + } - this.fetchData(); + /** + * Merge online events with the offline events of that period. + */ + protected mergeEvents(): void { + const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)]; + + if (!monthOfflineEvents && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return; + } + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + day.events.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + day.events = day.events.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + } + + if (monthOfflineEvents && monthOfflineEvents[day.mday]) { + // Add the offline events (either new or edited). + day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + } + }); + }); + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + + /** + * Undelete a certain event. + * + * @param {number} eventId Event ID. + */ + protected undeleteEvent(eventId: number): void { + if (!this.weeks) { + return; + } + + this.weeks.forEach((week) => { + week.days.forEach((day) => { + const event = day.events.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = false; + } + }); + }); } /** * Component destroyed. */ ngOnDestroy(): void { - // @todo + this.undeleteEventObserver && this.undeleteEventObserver.off(); } } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 42c3cd4b9..fa0877b46 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -7,6 +7,7 @@ + @@ -16,7 +17,12 @@ - + + + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} + + + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 9ab19cd7e..8f134ed90 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -12,16 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core'; import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; import { TranslateService } from '@ngx-translate/core'; +import { Network } from '@ionic-native/network'; /** * Page that displays the calendar events. @@ -31,7 +37,7 @@ import { TranslateService } from '@ngx-translate/core'; selector: 'page-addon-calendar-index', templateUrl: 'index.html', }) -export class AddonCalendarIndexPage implements OnInit { +export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; protected allCourses = { @@ -39,6 +45,18 @@ export class AddonCalendarIndexPage implements OnInit { fullname: this.translate.instant('core.fulllistofcourses'), category: -1 }; + protected eventId: number; + protected currentSiteId: string; + + // Observers. + protected newEventObserver: any; + protected discardedObserver: any; + protected editEventObserver: any; + protected deleteEventObserver: any; + protected undeleteEventObserver: any; + protected syncObserver: any; + protected manualSyncObserver: any; + protected onlineObserver: any; courseId: number; categoryId: number; @@ -46,63 +64,177 @@ export class AddonCalendarIndexPage implements OnInit { courses: any[]; notificationsEnabled = false; loaded = false; + hasOffline = false; + isOnline = false; + syncIcon: string; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, + network: Network, + zone: NgZone, + sitesProvider: CoreSitesProvider, private navCtrl: NavController, private domUtils: CoreDomUtilsProvider, private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, private translate: TranslateService, + private eventsProvider: CoreEventsProvider, private coursesProvider: CoreCoursesProvider, - private popoverCtrl: PopoverController) { + private popoverCtrl: PopoverController, + private appProvider: CoreAppProvider) { this.courseId = navParams.get('courseId'); + this.eventId = navParams.get('eventId') || false; this.notificationsEnabled = localNotificationsProvider.isAvailable(); + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // Listen for events added. When an event is added, reload the data. + this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Listen for new event discarded event. When it does, reload the data. + this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { + this.loaded = false; + this.refreshData(true, false); + }, this.currentSiteId); + + // Listen for events edited. When an event is edited, reload the data. + this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => { + if (data && data.event) { + this.loaded = false; + this.refreshData(true, false); + } + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Refresh data if calendar events are synchronized manually but not by this page. + this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { + if (data && data.source != 'index') { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Update the events when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + this.loaded = false; + this.refreshData(); + }, this.currentSiteId); + + // Update the "hasOffline" property if an event deleted in offline is restored. + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + this.calendarOffline.hasOfflineData().then((hasOffline) => { + this.hasOffline = hasOffline; + }); + }, this.currentSiteId); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = this.appProvider.isOnline(); + }); + }); } /** * View loaded. */ ngOnInit(): void { - this.fetchData(); + if (this.eventId) { + // There is an event to load, open the event in a new state. + this.gotoEvent(this.eventId); + } + + this.fetchData(true, false); } /** * Fetch all the data required for the view. * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - fetchData(): Promise { - const promises = []; + fetchData(sync?: boolean, showErrors?: boolean): Promise { - // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + this.syncIcon = 'spinner'; + this.isOnline = this.appProvider.isOnline(); - if (this.courseId) { - // Search the course to get the category. - const course = this.courses.find((course) => { - return course.id == this.courseId; - }); + let promise; - if (course) { - this.categoryId = course.category; + if (sync) { + // Try to synchronize offline events. + promise = this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); } - } - })); - // Check if user can create events. - promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { - this.canCreate = canEdit; - })); + if (result.updated) { + // Trigger a manual sync event. + result.source = 'index'; - return Promise.all(promises).catch((error) => { + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + const promises = []; + + this.hasOffline = false; + + // Load courses for the popover. + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift(this.allCourses); + this.courses = courses; + + if (this.courseId) { + // Search the course to get the category. + const course = this.courses.find((course) => { + return course.id == this.courseId; + }); + + if (course) { + this.categoryId = course.category; + } + } + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + // Check if there is offline data. + promises.push(this.calendarOffline.hasOfflineData().then((hasOffline) => { + this.hasOffline = hasOffline; + })); + + return Promise.all(promises); + }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); }).finally(() => { this.loaded = true; + this.syncIcon = 'sync'; }); } @@ -110,13 +242,31 @@ export class AddonCalendarIndexPage implements OnInit { * Refresh the data. * * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - doRefresh(refresher?: any): void { - if (!this.loaded) { - return; + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.loaded) { + return this.refreshData(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); } + return Promise.resolve(); + } + + /** + * Refresh the data. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + const promises = []; promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { @@ -126,11 +276,27 @@ export class AddonCalendarIndexPage implements OnInit { // Refresh the sub-component. promises.push(this.calendarComponent.refreshData()); - Promise.all(promises).finally(() => { - refresher && refresher.complete(); + return Promise.all(promises).finally(() => { + return this.fetchData(sync, showErrors); }); } + /** + * Navigate to a particular event. + * + * @param {number} eventId Event to load. + */ + gotoEvent(eventId: number): void { + if (eventId < 0) { + // It's an offline event, go to the edit page. + this.openEdit(eventId); + } else { + this.navCtrl.push('AddonCalendarEventPage', { + id: eventId + }); + } + } + /** * Show the context menu. * @@ -182,4 +348,18 @@ export class AddonCalendarIndexPage implements OnInit { openSettings(): void { this.navCtrl.push('AddonCalendarSettingsPage'); } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); + } } diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index 3d48baa38..ee06f75c9 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -280,6 +280,18 @@ export class AddonCalendarOfflineProvider { }); } + /** + * Check whether there's offline data for a site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline data, false otherwise. + */ + hasOfflineData(siteId?: string): Promise { + return this.getAllEventsIds(siteId).then((ids) => { + return ids.length > 0; + }); + } + /** * Check if an event is deleted. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 94cfa1e93..225ec7ffb 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; import { AddonCalendarProvider } from './calendar'; import { CoreConstants } from '@core/constants'; +import * as moment from 'moment'; /** * Service that provides some features regarding lists of courses and categories. @@ -85,6 +86,41 @@ export class AddonCalendarHelperProvider { }); } + /** + * Classify events into their respective months and days. If an event duration covers more than one day, + * it will be included in all the days it lasts. + * + * @param {any[]} events Events to classify. + * @return {{[monthId: string]: {[day: number]: any[]}}} Object with the classified events. + */ + classifyIntoMonths(events: any[]): {[monthId: string]: {[day: number]: any[]}} { + + const result = {}; + + events.forEach((event) => { + const treatedDay = moment(new Date(event.timestart * 1000)), + endDay = moment(new Date((event.timestart + (event.timeduration || 0)) * 1000)); + + // Add the event to all the days it lasts. + while (!treatedDay.isAfter(endDay, 'day')) { + const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1), + day = treatedDay.date(); + + if (!result[monthId]) { + result[monthId] = {}; + } + if (!result[monthId][day]) { + result[monthId][day] = []; + } + result[monthId][day].push(event); + + treatedDay.add(1, 'day'); // Treat next day. + } + }); + + return result; + } + /** * Convenience function to format some event data to be rendered. * @@ -97,7 +133,7 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } - if (e.id < 0) { + if (typeof e.duration != 'undefined') { // It's an offline event, add some calculated data. e.format = 1; e.visible = 1; @@ -140,6 +176,17 @@ export class AddonCalendarHelperProvider { return options; } + /** + * Get the month "id" (year + month). + * + * @param {number} year Year. + * @param {number} month Month. + * @return {string} The "id". + */ + getMonthId(year: number, month: number): string { + return year + '#' + month; + } + /** * Check if the data of an event has changed. * From 8dd01089070148a60eda4621e20be61740004341 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 4 Jul 2019 13:16:54 +0200 Subject: [PATCH 04/12] MOBILE-3021 calendar: Support upcoming events --- scripts/langindex.json | 6 + .../calendar/addon-calendar-calendar.html | 3 +- .../calendar/components/calendar/calendar.ts | 7 + .../calendar/components/components.module.ts | 11 +- .../addon-calendar-upcoming-events.html | 24 ++ .../upcoming-events/upcoming-events.ts | 317 ++++++++++++++++++ src/addon/calendar/lang/en.json | 8 +- src/addon/calendar/pages/index/index.html | 10 +- src/addon/calendar/pages/index/index.ts | 21 +- src/addon/calendar/providers/calendar.ts | 207 +++++++++++- src/app/app.scss | 5 + src/assets/lang/en.json | 6 + 12 files changed, 613 insertions(+), 12 deletions(-) create mode 100644 src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html create mode 100644 src/addon/calendar/components/upcoming-events/upcoming-events.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 2f79cd0a0..1d2d82d13 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -83,6 +83,7 @@ "addon.blog.publishtoworld": "blog", "addon.blog.showonlyyourentries": "local_moodlemobileapp", "addon.blog.siteblogheading": "blog", + "addon.calendar.allday": "calendar", "addon.calendar.calendar": "calendar", "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", @@ -113,6 +114,7 @@ "addon.calendar.invalidtimedurationuntil": "calendar", "addon.calendar.mon": "calendar", "addon.calendar.monday": "calendar", + "addon.calendar.monthlyview": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", @@ -129,6 +131,8 @@ "addon.calendar.sunday": "calendar", "addon.calendar.thu": "calendar", "addon.calendar.thursday": "calendar", + "addon.calendar.today": "calendar", + "addon.calendar.tomorrow": "calendar", "addon.calendar.tue": "calendar", "addon.calendar.tuesday": "calendar", "addon.calendar.typecategory": "calendar", @@ -140,8 +144,10 @@ "addon.calendar.typeopen": "calendar", "addon.calendar.typesite": "calendar", "addon.calendar.typeuser": "calendar", + "addon.calendar.upcomingevents": "calendar", "addon.calendar.wed": "calendar", "addon.calendar.wednesday": "calendar", + "addon.calendar.yesterday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", "addon.competency.competenciesmostoftennotproficientincourse": "tool_lp", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index a866c4be2..007100baf 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -37,12 +37,13 @@

-
+

+ {{ event.timestart * 1000 | coreFormatDate: timeFormat }} {{event.name}}

diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 053863483..203db79e8 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -42,6 +42,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest weekDays: any[]; weeks: any[]; loaded = false; + timeFormat: string; protected year: number; protected month: number; @@ -141,6 +142,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.deletedEvents = ids; })); + // Get time format to use. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + return Promise.all(promises).then(() => { return this.fetchEvents(); }).catch((error) => { @@ -232,6 +238,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateTimeFormat()); this.categoriesRetrieved = false; // Get categories again. diff --git a/src/addon/calendar/components/components.module.ts b/src/addon/calendar/components/components.module.ts index a6d5afe22..3928f6dd9 100644 --- a/src/addon/calendar/components/components.module.ts +++ b/src/addon/calendar/components/components.module.ts @@ -18,23 +18,28 @@ import { IonicModule } from 'ionic-angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { AddonCalendarCalendarComponent } from '../components/calendar/calendar'; +import { AddonCalendarUpcomingEventsComponent } from '../components/upcoming-events/upcoming-events'; @NgModule({ declarations: [ - AddonCalendarCalendarComponent + AddonCalendarCalendarComponent, + AddonCalendarUpcomingEventsComponent ], imports: [ CommonModule, IonicModule, TranslateModule.forChild(), CoreComponentsModule, - CoreDirectivesModule + CoreDirectivesModule, + CorePipesModule ], providers: [ ], exports: [ - AddonCalendarCalendarComponent + AddonCalendarCalendarComponent, + AddonCalendarUpcomingEventsComponent ] }) export class AddonCalendarComponentsModule {} diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html new file mode 100644 index 000000000..b338f0eca --- /dev/null +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -0,0 +1,24 @@ + + + + + + + + + +

+

+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+
+
+ +
diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts new file mode 100644 index 000000000..41751c0d5 --- /dev/null +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -0,0 +1,317 @@ +// (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 { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreConstants } from '@core/constants'; + +/** + * Component that displays upcoming events. + */ +@Component({ + selector: 'addon-calendar-upcoming-events', + templateUrl: 'addon-calendar-upcoming-events.html', +}) +export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, OnDestroy { + @Input() courseId: number | string; + @Input() categoryId: number | string; // Category ID the course belongs to. + @Output() onEventClicked = new EventEmitter(); + + events = []; // Events (both online and offline). + filteredEvents = []; + loaded = false; + + protected year: number; + protected month: number; + protected categoriesRetrieved = false; + protected categories = {}; + protected currentSiteId: string; + protected onlineEvents = []; + protected offlineEvents = []; // Offline events. + protected deletedEvents = []; // Events deleted in offline. + protected lookAhead: number; + protected timeFormat: string; + + // Observers. + protected undeleteEventObserver: any; + + constructor(eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private domUtils: CoreDomUtilsProvider, + 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); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + this.fetchData(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.courseId || changes.categoryId) { + this.filterEvents(); + } + } + + /** + * Fetch data. + * + * @param {boolean} [refresh=false] True if we are refreshing events. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + const promises = []; + + promises.push(this.loadCategories()); + + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + + this.offlineEvents = this.sortEvents(events); + })); + + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; + })); + + // Get user preferences. + promises.push(this.calendarProvider.getCalendarLookAhead().then((value) => { + this.lookAhead = value; + })); + + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + + return Promise.all(promises).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch upcoming events. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getUpcomingEvents().then((result) => { + this.onlineEvents = result.events; + + this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + // Merge the online events with offline data. + this.events = this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + + // Re-calculate the formatted time so it uses the device date. + this.events.forEach((event) => { + event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + }); + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + if (this.categoriesRetrieved) { + // Already retrieved, stop. + return Promise.resolve(); + } + + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categoriesRetrieved = true; + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Filter events to only display events belonging to a certain course. + */ + filterEvents(): void { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; + + if (!courseId || courseId < 0) { + this.filteredEvents = this.events; + } else { + this.filteredEvents = this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories); + }); + } + } + + /** + * Refresh events. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + const promises = []; + + promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateLookAhead()); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + this.categoriesRetrieved = false; // Get categories again. + + return Promise.all(promises).then(() => { + return this.fetchData(true); + }); + } + + /** + * An event was clicked. + * + * @param {any} event Event. + */ + eventClicked(event: any): void { + this.onEventClicked.emit(event.id); + } + + /** + * Merge online events with the offline events of that period. + * + * @return {any[]} Merged events. + */ + protected mergeEvents(): any[] { + if (!this.offlineEvents.length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return this.onlineEvents; + } + + const start = Date.now(), + end = start + (CoreConstants.SECONDS_DAY * this.lookAhead); + let result = this.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEvents.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + const offlineEvent = this.offlineEvents.find((ev) => { + return ev.id == event.id; + }); + + return !offlineEvent; + }); + } + + // Now get the offline events that belong to this period. + const periodOfflineEvents = this.offlineEvents.filter((event) => { + return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end; + }); + + // Merge both arrays and sort them. + result = result.concat(periodOfflineEvents); + + return this.sortEvents(result); + } + + /** + * 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 { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = false; + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.undeleteEventObserver && this.undeleteEventObserver.off(); + } +} diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 412c81742..ba81a9a89 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -1,4 +1,5 @@ { + "allday": "All day", "calendar": "Calendar", "calendarevent": "Calendar event", "calendarevents": "Calendar events", @@ -29,6 +30,7 @@ "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", "mon": "Mon", "monday": "Monday", + "monthlyview": "Monthly view", "newevent": "New event", "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -45,6 +47,8 @@ "sunday": "Sunday", "thu": "Thu", "thursday": "Thursday", + "today": "Today", + "tomorrow": "Tomorrow", "tue": "Tue", "tuesday": "Tuesday", "typeclose": "Close event", @@ -56,6 +60,8 @@ "typeopen": "Open event", "typesite": "Site event", "typeuser": "User event", + "upcomingevents": "Upcoming events", "wed": "Wed", - "wednesday": "Wednesday" + "wednesday": "Wednesday", + "yesterday": "Yesterday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index fa0877b46..120c9dc2c 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -2,6 +2,12 @@ {{ 'addon.calendar.calendarevents' | translate }} + + @@ -22,7 +28,9 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + + + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 8f134ed90..396690427 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -23,6 +23,7 @@ import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; @@ -39,6 +40,7 @@ import { Network } from '@ionic-native/network'; }) export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; + @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent; protected allCourses = { id: -1, @@ -67,6 +69,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { hasOffline = false; isOnline = false; syncIcon: string; + showCalendar = true; + loadUpcoming = false; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, @@ -274,7 +278,11 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { })); // Refresh the sub-component. - promises.push(this.calendarComponent.refreshData()); + if (this.showCalendar && this.calendarComponent) { + promises.push(this.calendarComponent.refreshData()); + } else if (!this.showCalendar && this.upcomingEventsComponent) { + promises.push(this.upcomingEventsComponent.refreshData()); + } return Promise.all(promises).finally(() => { return this.fetchData(sync, showErrors); @@ -349,6 +357,17 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.navCtrl.push('AddonCalendarSettingsPage'); } + /** + * Toogle display: monthly view or upcoming events. + */ + toggleDisplay(): void { + this.showCalendar = !this.showCalendar; + + if (!this.showCalendar) { + this.loadUpcoming = true; + } + } + /** * Page destroyed. */ diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 4705a8ad8..c759210db 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -27,7 +27,9 @@ import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; import { AddonCalendarOfflineProvider } from './calendar-offline'; +import { CoreUserProvider } from '@core/user/providers/user'; import { TranslateService } from '@ngx-translate/core'; +import * as moment from 'moment'; /** * Service to handle calendar events. @@ -49,6 +51,10 @@ export class AddonCalendarProvider { static TYPE_GROUP = 'group'; static TYPE_SITE = 'site'; static TYPE_USER = 'user'; + + static CALENDAR_TF_24 = '%H:%M'; // Calendar time in 24 hours format. + static CALENDAR_TF_12 = '%I:%M %p'; // Calendar time in 12 hours format. + protected ROOT_CACHE_KEY = 'mmaCalendar:'; protected weekDays = [ @@ -249,11 +255,19 @@ export class AddonCalendarProvider { protected logger; - 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 calendarOffline: AddonCalendarOfflineProvider, - private appProvider: CoreAppProvider, private translate: TranslateService) { + 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 calendarOffline: AddonCalendarOfflineProvider, + private appProvider: CoreAppProvider, + private translate: TranslateService, + private userProvider: CoreUserProvider) { + this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -456,6 +470,90 @@ export class AddonCalendarProvider { }); } + /** + * Check if event ends the same day or not. + * + * @param {any} event Event info. + * @return {boolean} If the . + */ + endsSameDay(event: any): boolean { + if (!event.timeduration) { + // No duration. + return true; + } + + // Check if day has changed. + return moment(event.timestart * 1000).isSame((event.timestart + event.timeduration) * 1000, 'day'); + } + + /** + * Format event time. Equivalent to calendar_format_event_time. + * + * @param {any} event Event to format. + * @param {string} format Calendar time format (from getCalendarTimeFormat). + * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @param {number} [showTime=0] Determine the show time GMT timestamp. + * @return {string} Formatted event time. + */ + formatEventTime(event: any, format: string, useCommonWords: boolean = true, showTime: number = 0): string { + const start = event.timestart * 1000, + end = (event.timestart + event.timeduration) * 1000; + let time; + + if (event.timeduration) { + + if (moment(start).isSame(end, 'day')) { + // Event starts and ends the same day. + if (event.timeduration == CoreConstants.SECONDS_DAY) { + time = this.translate.instant('addon.calendar.allday'); + } else { + time = this.timeUtils.userDate(start, format) + ' » ' + + this.timeUtils.userDate(end, format); + } + + if (!showTime) { + return this.getDayRepresentation(start, useCommonWords) + ', ' + time; + } else { + return time; + } + + } else { + // Event lasts more than one day. + const midnightStart = moment(start).startOf('day').unix() * 1000, + midnightEnd = moment(end).startOf('day').unix() * 1000, + timeStart = this.timeUtils.userDate(start, format), + timeEnd = this.timeUtils.userDate(end, format); + let dayStart = this.getDayRepresentation(start, useCommonWords) + ', ', + dayEnd = this.getDayRepresentation(end, useCommonWords) + ', '; + + if (showTime == midnightStart) { + dayStart = ''; + } + + if (showTime == midnightEnd) { + dayEnd = ''; + } + + // Set printable representation. + if (moment().isSame(start, 'day')) { + // Event starts today, don't display the day. + return timeStart + ' » ' + dayEnd + timeEnd; + } else { + // The event starts in the future, print both days. + return dayStart + timeStart + ' » ' + dayEnd + timeEnd; + } + } + } else { // There is no time duration. + const time = this.timeUtils.userDate(start, format); + + if (!showTime) { + return this.getDayRepresentation(start, useCommonWords) + ', ' + time.trim(); + } else { + return time; + } + } + } + /** * Get access information for a calendar (either course calendar or site calendar). * @@ -545,6 +643,84 @@ export class AddonCalendarProvider { return this.ROOT_CACHE_KEY + 'allowedEventTypes:' + (courseId || 0); } + /** + * Get the "look ahead" for a certain user. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the look ahead (number of days). + */ + getCalendarLookAhead(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.userProvider.getUserPreference('calendar_lookahead').catch((error) => { + // Ignore errors. + }).then((value): any => { + if (typeof value != 'undefined') { + return value; + } + + return site.getStoredConfig('calendar_lookahead'); + }); + }); + } + + /** + * Get the time format to use in calendar. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the format. + */ + getCalendarTimeFormat(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.userProvider.getUserPreference('calendar_timeformat').catch((error) => { + // Ignore errors. + }).then((format) => { + + if (!format || format === '0') { + format = site.getStoredConfig('calendar_site_timeformat'); + } + + if (format === AddonCalendarProvider.CALENDAR_TF_12) { + format = this.translate.instant('core.strftimetime12'); + } else if (format === AddonCalendarProvider.CALENDAR_TF_24) { + format = this.translate.instant('core.strftimetime24'); + } + + return format && format !== '0' ? format : this.translate.instant('core.strftimetime'); + }); + }); + } + + /** + * Return the representation day. Equivalent to Moodle's calendar_day_representation. + * + * @param {number} time Timestamp to get the day from. + * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @return {string} The formatted date/time. + */ + getDayRepresentation(time: number, useCommonWords: boolean = true): string { + + if (!useCommonWords) { + // We don't want words, just a date. + return this.timeUtils.userDate(time, 'core.strftimedayshort'); + } + + const date = moment(time), + today = moment(); + + if (date.isSame(today, 'day')) { + return this.translate.instant('addon.calendar.today'); + + } else if (date.isSame(today.clone().subtract(1, 'days'), 'day')) { + return this.translate.instant('addon.calendar.yesterday'); + + } else if (date.isSame(today.clone().add(1, 'days'), 'day')) { + return this.translate.instant('addon.calendar.tomorrow'); + + } else { + return this.timeUtils.userDate(time, 'core.strftimedayshort'); + } + } + /** * Get the configured default notification time. * @@ -1019,6 +1195,7 @@ export class AddonCalendarProvider { * * @param {number} [courseId] Course ID. * @param {number} [categoryId] Category ID. + * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { @@ -1027,6 +1204,26 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates look ahead setting. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateLookAhead(siteId?: string): Promise { + return this.userProvider.invalidateUserPreference('calendar_lookahead', siteId); + } + + /** + * Invalidates time format setting. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateTimeFormat(siteId?: string): Promise { + return this.userProvider.invalidateUserPreference('calendar_timeformat', siteId); + } + /** * Check if Calendar is disabled in a certain site. * diff --git a/src/app/app.scss b/src/app/app.scss index 41759e8ef..7123b9909 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1152,3 +1152,8 @@ ion-app.platform-desktop { min-height: $button-ios-small-height; } } + +// Make funnel icon have iOS look. +.ion-md-funnel::before { + content: "\f182"; +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 3e4a0a984..7dae77a70 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -83,6 +83,7 @@ "addon.blog.publishtoworld": "Anyone in the world", "addon.blog.showonlyyourentries": "Show only your entries", "addon.blog.siteblogheading": "Site blog", + "addon.calendar.allday": "All day", "addon.calendar.calendar": "Calendar", "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", @@ -113,6 +114,7 @@ "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", "addon.calendar.mon": "Mon", "addon.calendar.monday": "Monday", + "addon.calendar.monthlyview": "Monthly view", "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -129,6 +131,8 @@ "addon.calendar.sunday": "Sunday", "addon.calendar.thu": "Thu", "addon.calendar.thursday": "Thursday", + "addon.calendar.today": "Today", + "addon.calendar.tomorrow": "Tomorrow", "addon.calendar.tue": "Tue", "addon.calendar.tuesday": "Tuesday", "addon.calendar.typecategory": "Category event", @@ -140,8 +144,10 @@ "addon.calendar.typeopen": "Open event", "addon.calendar.typesite": "Site event", "addon.calendar.typeuser": "User event", + "addon.calendar.upcomingevents": "Upcoming events", "addon.calendar.wed": "Wed", "addon.calendar.wednesday": "Wednesday", + "addon.calendar.yesterday": "Yesterday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", "addon.competency.competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course", From 083ca7cd7ee9b3228443dbe32f71d649820389a8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 5 Jul 2019 10:30:38 +0200 Subject: [PATCH 05/12] MOBILE-3021 calendar: Implement day view --- scripts/langindex.json | 2 + .../calendar/addon-calendar-calendar.html | 4 +- .../components/calendar/calendar.scss | 2 +- .../calendar/components/calendar/calendar.ts | 10 + .../upcoming-events/upcoming-events.ts | 2 +- src/addon/calendar/lang/en.json | 2 + src/addon/calendar/pages/day/day.html | 71 ++ src/addon/calendar/pages/day/day.module.ts | 35 + src/addon/calendar/pages/day/day.ts | 607 ++++++++++++++++++ .../calendar/pages/edit-event/edit-event.ts | 4 +- src/addon/calendar/pages/event/event.ts | 1 - src/addon/calendar/pages/index/index.html | 2 +- src/addon/calendar/pages/index/index.ts | 72 +-- src/addon/calendar/pages/list/list.ts | 66 +- src/addon/calendar/providers/calendar.ts | 99 +++ src/assets/lang/en.json | 2 + src/core/courses/providers/helper.ts | 74 ++- 17 files changed, 963 insertions(+), 92 deletions(-) create mode 100644 src/addon/calendar/pages/day/day.html create mode 100644 src/addon/calendar/pages/day/day.module.ts create mode 100644 src/addon/calendar/pages/day/day.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 1d2d82d13..d80355727 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -90,6 +90,8 @@ "addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.confirmeventdelete": "calendar", "addon.calendar.confirmeventseriesdelete": "calendar", + "addon.calendar.daynext": "calendar", + "addon.calendar.dayprev": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.deleteallevents": "calendar", "addon.calendar.deleteevent": "calendar", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 007100baf..7c607ca63 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -31,7 +31,7 @@ -

{{ day.mday }}

+

{{ day.mday }}

@@ -47,7 +47,7 @@ {{event.name}}

-

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

+

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

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index c3a20bf9e..af7182c32 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -20,7 +20,7 @@ ion-app.app-root addon-calendar-calendar { } } - .addon-calendar-event { + .addon-calendar-event, .addon-calendar-day-number, .addon-calendar-day-more { cursor: pointer; } diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 203db79e8..2b40b8b3f 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -37,6 +37,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. @Output() onEventClicked = new EventEmitter(); + @Output() onDayClicked = new EventEmitter<{day: number, month: number, year: number}>(); periodName: string; weekDays: any[]; @@ -288,6 +289,15 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.onEventClicked.emit(event.id); } + /** + * A day was clicked. + * + * @param {number} day Day. + */ + dayClicked(day: number): void { + this.onDayClicked.emit({day: day, month: this.month, year: this.year}); + } + /** * Decrease the current month. */ diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index 41751c0d5..f358b5785 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -34,7 +34,6 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, @Input() categoryId: number | string; // Category ID the course belongs to. @Output() onEventClicked = new EventEmitter(); - events = []; // Events (both online and offline). filteredEvents = []; loaded = false; @@ -43,6 +42,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, protected categoriesRetrieved = false; protected categories = {}; protected currentSiteId: string; + protected events = []; // Events (both online and offline). protected onlineEvents = []; protected offlineEvents = []; // Offline events. protected deletedEvents = []; // Events deleted in offline. diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index ba81a9a89..8d19b4559 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -6,6 +6,8 @@ "calendarreminders": "Calendar reminders", "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "daynext": "Next day", + "dayprev": "Previous day", "defaultnotificationtime": "Default notification time", "deleteallevents": "Delete all events", "deleteevent": "Delete event", diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html new file mode 100644 index 000000000..e9b48bd52 --- /dev/null +++ b/src/addon/calendar/pages/day/day.html @@ -0,0 +1,71 @@ + + + {{ 'addon.calendar.calendarevents' | translate }} + + + + + + + + + + + + + + + + + + + + + + +

{{ periodName }}

+
+ + + + + +
+
+ + + + {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }} + + + + + + + + + + +

+

+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+
+
+ + + + + +
diff --git a/src/addon/calendar/pages/day/day.module.ts b/src/addon/calendar/pages/day/day.module.ts new file mode 100644 index 000000000..c4e33dbb9 --- /dev/null +++ b/src/addon/calendar/pages/day/day.module.ts @@ -0,0 +1,35 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonCalendarDayPage } from './day'; + +@NgModule({ + declarations: [ + AddonCalendarDayPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonCalendarDayPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarDayPageModule {} diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts new file mode 100644 index 000000000..924bfa0e7 --- /dev/null +++ b/src/addon/calendar/pages/day/day.ts @@ -0,0 +1,607 @@ +// (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 OFx ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, NgZone } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; +import { Network } from '@ionic-native/network'; +import * as moment from 'moment'; + +/** + * Page that displays the calendar events for a certain day. + */ +@IonicPage({ segment: 'addon-calendar-day' }) +@Component({ + selector: 'page-addon-calendar-day', + templateUrl: 'day.html', +}) +export class AddonCalendarDayPage implements OnInit, OnDestroy { + + protected currentSiteId: string; + protected year: number; + protected month: number; + protected day: number; + protected categories = {}; + protected events = []; // Events (both online and offline). + protected onlineEvents = []; + protected offlineEvents = {}; // Offline events. + protected offlineEditedEventsIds = []; // IDs of events edited in offline. + protected deletedEvents = []; // Events deleted in offline. + protected timeFormat: string; + protected currentMoment: moment.Moment; + + // 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; + + periodName: string; + filteredEvents = []; + courseId: number; + categoryId: number; + canCreate = false; + courses: any[]; + loaded = false; + hasOffline = false; + isOnline = false; + syncIcon: string; + + constructor(localNotificationsProvider: CoreLocalNotificationsProvider, + navParams: NavParams, + network: Network, + zone: NgZone, + sitesProvider: CoreSitesProvider, + private navCtrl: NavController, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, + private eventsProvider: CoreEventsProvider, + private coursesProvider: CoreCoursesProvider, + private coursesHelper: CoreCoursesHelperProvider, + private appProvider: CoreAppProvider) { + + const now = new Date(); + + this.year = navParams.get('year') || now.getFullYear(); + this.month = navParams.get('month') || (now.getMonth() + 1); + this.day = navParams.get('day') || now.getDate(); + this.courseId = navParams.get('courseId'); + 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 != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // Update the events when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + if (data && !data.sent) { + // Event was deleted in offline. Just mark it as deleted, no need to refresh. + this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline; + this.deletedEvents.push(data.eventId); + } else { + this.loaded = false; + this.refreshData(); + } + }, this.currentSiteId); + + // 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. + const found = this.markAsDeleted(data.eventId, false); + + // 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); + } + + if (found) { + // The deleted event belongs to current list. Re-calculate "hasOffline". + this.hasOffline = false; + + if (this.events.length != this.onlineEvents.length) { + this.hasOffline = true; + } else { + const event = this.events.find((event) => { + return event.deleted || event.offline; + }); + + this.hasOffline = !!event; + } + } + } + }, this.currentSiteId); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = this.appProvider.isOnline(); + }); + }); + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.currentMoment = moment().year(this.year).month(this.month - 1).day(this.day); + + this.fetchData(true, false); + } + + /** + * Fetch all the data required for the view. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + fetchData(sync?: boolean, showErrors?: boolean): Promise { + + this.syncIcon = 'spinner'; + this.isOnline = this.appProvider.isOnline(); + + const promise = sync ? this.sync() : Promise.resolve(); + + return promise.then(() => { + const promises = []; + + // Load courses for the popover. + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => { + this.courses = data.courses; + this.categoryId = data.categoryId; + })); + + // Get categories. + promises.push(this.loadCategories()); + + // 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 & day. + 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; + })); + + // Check if user can create events. + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + })); + + // Get user preferences. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + + return Promise.all(promises); + }).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + this.syncIcon = 'sync'; + }); + } + + /** + * Fetch the events for current day. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getDayEvents(this.year, this.month, this.day).then((result) => { + + // Calculate the period name. We don't use the one in result because it's in server's language. + this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1, this.day).getTime(), + 'core.strftimedaydate'); + + this.onlineEvents = result.events; + this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + // Merge the online events with offline data. + this.events = this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + + // Re-calculate the formatted time so it uses the device date. + this.events.forEach((event) => { + event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + }); + }); + } + + /** + * Merge online events with the offline events of that period. + * + * @return {any[]} Merged events. + */ + protected mergeEvents(): any[] { + this.hasOffline = false; + + if (!this.offlineEditedEventsIds.length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return this.onlineEvents; + } + + const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)], + dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day]; + let result = this.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + + if (event.deleted) { + this.hasOffline = true; + } + }); + } + + if (this.offlineEditedEventsIds.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + return this.offlineEditedEventsIds.indexOf(event.id) == -1; + }); + + if (result.length != this.onlineEvents.length) { + this.hasOffline = true; + } + } + + if (dayOfflineEvents && dayOfflineEvents.length) { + // Add the offline events (either new or edited). + this.hasOffline = true; + result = this.sortEvents(result.concat(dayOfflineEvents)); + } + + return result; + } + + /** + * Filter events to only display events belonging to a certain course. + */ + protected filterEvents(): void { + if (!this.courseId || this.courseId < 0) { + this.filteredEvents = this.events; + } else { + this.filteredEvents = this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories); + }); + } + } + + /** + * 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; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.loaded) { + return this.refreshData(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + + /** + * Refresh the data. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + + const promises = []; + + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); + promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day)); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + return Promise.all(promises).finally(() => { + return this.fetchData(sync, showErrors); + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categories = {}; + + // Index categories by ID. + cats.forEach((category) => { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Try to synchronize offline events. + * + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + protected sync(showErrors?: boolean): Promise { + return this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + if (result.updated) { + // Trigger a manual sync event. + result.source = 'day'; + result.day = this.day; + result.month = this.month; + result.year = this.year; + + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } + + /** + * 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. + * + * @param {MouseEvent} event Event. + */ + openCourseFilter(event: MouseEvent): void { + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); + + this.filterEvents(); + } + }); + } + + /** + * Open page to create/edit an event. + * + * @param {number} [eventId] Event ID to edit. + */ + openEdit(eventId?: number): void { + const params: any = {}; + + if (eventId) { + params.eventId = eventId; + } else { + // It's a new event, set the time. + params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000; + } + + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarEditEventPage', params); + } + + /** + * Load next month. + */ + loadNext(): void { + this.increaseDay(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.decreaseDay(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Load previous month. + */ + loadPrevious(): void { + this.decreaseDay(); + + this.loaded = false; + + this.fetchEvents().catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.increaseDay(); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Decrease the current day. + */ + protected decreaseDay(): void { + this.currentMoment.subtract(1, 'day'); + + this.year = this.currentMoment.year(); + this.month = this.currentMoment.month() + 1; + this.day = this.currentMoment.date(); + } + + /** + * Increase the current day. + */ + protected increaseDay(): void { + this.currentMoment.add(1, 'day'); + + this.year = this.currentMoment.year(); + this.month = this.currentMoment.month() + 1; + this.day = this.currentMoment.date(); + } + + /** + * Find an event and mark it as deleted. + * + * @param {number} eventId Event ID. + * @param {boolean} deleted Whether to mark it as deleted or not. + * @return {boolean} Whether the event was found. + */ + protected markAsDeleted(eventId: number, deleted: boolean): boolean { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = deleted; + + return true; + } + + return false; + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.manualSyncObserver && this.manualSyncObserver.off(); + this.onlineObserver && this.onlineObserver.unsubscribe(); + } +} diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index edda98102..93f8b3499 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -99,6 +99,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent'; + const timestamp = navParams.get('timestamp'); + this.currentSite = sitesProvider.getCurrentSite(); this.errors = { required: this.translate.instant('core.required') @@ -114,7 +116,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { this.groupControl = this.fb.control(''); this.descriptionControl = this.fb.control(''); - const currentDate = this.timeUtils.toDatetimeFormat(); + const currentDate = this.timeUtils.toDatetimeFormat(timestamp); this.eventForm.addControl('name', this.fb.control('', Validators.required)); this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required)); diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 9f312a3e8..bfb45499c 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -493,7 +493,6 @@ export class AddonCalendarEventPage implements OnDestroy { eventId: this.eventId }, this.sitesProvider.getCurrentSiteId()); - this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); this.event.deleted = false; }).catch((error) => { diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 120c9dc2c..099892df8 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -28,7 +28,7 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 396690427..59088156b 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core'; -import { IonicPage, NavParams, NavController, PopoverController } from 'ionic-angular'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -25,9 +25,7 @@ import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; -import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; -import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { Network } from '@ionic-native/network'; /** @@ -42,11 +40,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent; - protected allCourses = { - id: -1, - fullname: this.translate.instant('core.fulllistofcourses'), - category: -1 - }; protected eventId: number; protected currentSiteId: string; @@ -83,10 +76,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarSync: AddonCalendarSyncProvider, - private translate: TranslateService, private eventsProvider: CoreEventsProvider, - private coursesProvider: CoreCoursesProvider, - private popoverCtrl: PopoverController, + private coursesHelper: CoreCoursesHelperProvider, private appProvider: CoreAppProvider) { this.courseId = navParams.get('courseId'); @@ -206,21 +197,9 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { 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; - } - } + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => { + this.courses = data.courses; + this.categoryId = data.categoryId; })); // Check if user can create events. @@ -273,9 +252,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { const promises = []; - promises.push(this.calendarProvider.invalidateAllowedEventTypes().then(() => { - return this.fetchData(); - })); + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); // Refresh the sub-component. if (this.showCalendar && this.calendarComponent) { @@ -305,21 +282,35 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { } } + /** + * View a certain day. + * + * @param {any} data Data with the year, month and day. + */ + gotoDay(data: any): void { + const params: any = { + day: data.day, + month: data.month, + year: data.year + }; + + if (this.courseId) { + params.courseId = this.courseId; + } + + this.navCtrl.push('AddonCalendarDayPage', params); + } + /** * Show the context menu. * * @param {MouseEvent} event Event. */ openCourseFilter(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { - courses: this.courses, - courseId: this.courseId - }); - - popover.onDidDismiss((course) => { - if (course) { - this.courseId = course.id > 0 ? course.id : undefined; - this.categoryId = course.id > 0 ? course.category : undefined; + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; // Course viewed has changed, check if the user can create events for this course calendar. this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { @@ -327,9 +318,6 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { }); } }); - popover.present({ - ev: event - }); } /** diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 00c6657d4..1c7572e12 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -13,19 +13,18 @@ // limitations under the License. import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core'; -import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; -import { TranslateService } from '@ngx-translate/core'; +import { IonicPage, Content, NavParams, NavController } from 'ionic-angular'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitesProvider } from '@providers/sites'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; -import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; import { CoreEventsProvider } from '@providers/events'; import { CoreAppProvider } from '@providers/app'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -50,11 +49,6 @@ export class AddonCalendarListPage implements OnDestroy { protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events. protected categoriesRetrieved = false; protected getCategories = false; - protected allCourses = { - id: -1, - fullname: this.translate.instant('core.fulllistofcourses'), - category: -1 - }; protected categories = {}; protected siteHomeId: number; protected obsDefaultTimeChange: any; @@ -80,18 +74,17 @@ export class AddonCalendarListPage implements OnDestroy { filteredEvents = []; canLoadMore = false; loadMoreError = false; - filter = { - course: this.allCourses - }; + courseId: number; + categoryId: number; canCreate = false; hasOffline = false; isOnline = false; syncIcon: string; // Sync icon. - constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, + constructor(private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, zone: NgZone, - localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, + localNotificationsProvider: CoreLocalNotificationsProvider, private coursesHelper: CoreCoursesHelperProvider, private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, network: Network, private timeUtils: CoreTimeUtilsProvider) { @@ -177,6 +170,7 @@ export class AddonCalendarListPage implements OnDestroy { if (data && !data.sent) { // Event was deleted in offline. Just mark it as deleted, no need to refresh. this.markAsDeleted(data.eventId, true); + this.deletedEvents.push(data.eventId); this.hasOffline = true; } else { // Event deleted, clear the details if needed and refresh the view. @@ -278,19 +272,17 @@ export class AddonCalendarListPage implements OnDestroy { return promise.then(() => { const promises = []; - const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; this.hasOffline = false; - promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { this.canCreate = canEdit; })); // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((result) => { + this.courses = result.courses; + this.categoryId = result.categoryId; if (this.preSelectedCourseId) { this.filter.course = courses.find((course) => { @@ -418,14 +410,13 @@ export class AddonCalendarListPage implements OnDestroy { * @return {any[]} Filtered events. */ protected getFilteredEvents(): any[] { - if (this.filter.course.id == -1) { + if (!this.courseId) { // No filter, display everything. return this.events; } return this.events.filter((event) => { - return this.calendarHelper.shouldDisplayEvent(event, this.filter.course.id, this.filter.course.category, - this.categories); + return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories); }); } @@ -613,28 +604,21 @@ export class AddonCalendarListPage implements OnDestroy { * @param {MouseEvent} event Event. */ openCourseFilter(event: MouseEvent): void { - const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { - courses: this.courses, - courseId: this.filter.course.id - }); - popover.onDidDismiss((course) => { - if (course) { - this.filter.course = course; - this.domUtils.scrollToTop(this.content); + this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => { + if (typeof result.courseId != 'undefined') { + this.courseId = result.courseId > 0 ? result.courseId : undefined; + this.categoryId = result.courseId > 0 ? result.categoryId : undefined; + + // Course viewed has changed, check if the user can create events for this course calendar. + this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => { + this.canCreate = canEdit; + }); this.filteredEvents = this.getFilteredEvents(); - // Course viewed has changed, check if the user can create events for this course calendar. - const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; - - this.calendarHelper.canEditEvents(courseId).then((canEdit) => { - this.canCreate = canEdit; - }); + this.domUtils.scrollToTop(this.content); } }); - popover.present({ - ev: event - }); } /** @@ -650,8 +634,8 @@ export class AddonCalendarListPage implements OnDestroy { if (eventId) { params.eventId = eventId; } - if (this.filter.course.id != this.allCourses.id) { - params.courseId = this.filter.course.id; + if (this.courseId) { + params.courseId = this.courseId; } this.splitviewCtrl.push('AddonCalendarEditEventPage', params); diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index c759210db..3bb868034 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -858,6 +858,79 @@ export class AddonCalendarProvider { }); } + /** + * Get calendar events for a certain day. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + const data: any = { + year: year, + month: month, + day: day + }; + + if (courseId) { + data.courseid = courseId; + } + if (categoryId) { + data.categoryid = categoryId; + } + + const preSets = { + cacheKey: this.getDayEventsCacheKey(year, month, day, courseId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES + }; + + return site.read('core_calendar_get_calendar_day_view', data, preSets); + }); + } + + /** + * Get prefix cache key for day events WS calls. + * + * @return {string} Prefix Cache key. + */ + protected getDayEventsPrefixCacheKey(): string { + return this.ROOT_CACHE_KEY + 'day:'; + } + + /** + * Get prefix cache key for a certain day for day events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @return {string} Prefix Cache key. + */ + protected getDayEventsDayPrefixCacheKey(year: number, month: number, day: number): string { + return this.getDayEventsPrefixCacheKey() + year + ':' + month + ':' + day + ':'; + } + + /** + * Get cache key for day events WS calls. + * + * @param {number} year Year to get. + * @param {number} month Month to get. + * @param {number} day Day to get. + * @param {number} [courseId] Course to get. + * @param {number} [categoryId] Category to get. + * @return {string} Cache key. + */ + protected getDayEventsCacheKey(year: number, month: number, day: number, courseId?: number, categoryId?: number): string { + return this.getDayEventsDayPrefixCacheKey(year, month, day) + (courseId ? courseId : '') + ':' + + (categoryId ? categoryId : ''); + } + /** * Get a calendar reminders from local Db. * @@ -1120,6 +1193,32 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates day events for all days. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllDayEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsPrefixCacheKey()); + }); + } + + /** + * Invalidates day events for a certain day. + * + * @param {number} year Year. + * @param {number} month Month. + * @param {number} day Day. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDayEvents(year: number, month: number, day: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsDayPrefixCacheKey(year, month, day)); + }); + } + /** * Invalidates events list and all the single events and related info. * diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7dae77a70..b78160d84 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -90,6 +90,8 @@ "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", + "addon.calendar.daynext": "Next day", + "addon.calendar.dayprev": "Previous day", "addon.calendar.defaultnotificationtime": "Default notification time", "addon.calendar.deleteallevents": "Delete all events", "addon.calendar.deleteevent": "Delete event", diff --git a/src/core/courses/providers/helper.ts b/src/core/courses/providers/helper.ts index 6ff3ba5cf..abd9634fe 100644 --- a/src/core/courses/providers/helper.ts +++ b/src/core/courses/providers/helper.ts @@ -13,9 +13,12 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { PopoverController } from 'ionic-angular'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCoursesProvider } from './courses'; import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; /** * Helper to gather some common courses functions. @@ -23,8 +26,46 @@ import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers @Injectable() export class CoreCoursesHelperProvider { - constructor(private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, - private courseCompletionProvider: AddonCourseCompletionProvider) { } + constructor(private coursesProvider: CoreCoursesProvider, + private utils: CoreUtilsProvider, + private courseCompletionProvider: AddonCourseCompletionProvider, + private translate: TranslateService, + private popoverCtrl: PopoverController) { } + + /** + * Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId. + * + * @param {number} [courseId] Course ID to get the category. + * @return {Promise<{courses: any[], categoryId: number}>} Promise resolved with the list of courses and the category. + */ + getCoursesForPopover(courseId?: number): Promise<{courses: any[], categoryId: number}> { + return this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift({ + id: -1, + fullname: this.translate.instant('core.fulllistofcourses'), + category: -1 + }); + + let categoryId; + + if (courseId) { + // Search the course to get the category. + const course = courses.find((course) => { + return course.id == courseId; + }); + + if (course) { + categoryId = course.category; + } + } + + return { + courses: courses, + categoryId: categoryId + }; + }); + } /** * Given a course object returned by core_enrol_get_users_courses and another one returned by core_course_get_courses_by_field, @@ -174,4 +215,33 @@ export class CoreCoursesHelperProvider { }); }); } + + /** + * Show a context menu to select a course, and return the courseId and categoryId of the selected course (-1 for all courses). + * Returns an empty object if popover closed without picking a course. + * + * @param {MouseEvent} event Click event. + * @param {any[]} courses List of courses, from CoreCoursesHelperProvider.getCoursesForPopover. + * @param {number} courseId The course to select at start. + * @return {Promise<{courseId?: number, categoryId?: number}>} Promise resolved with the course ID and category ID. + */ + selectCourse(event: MouseEvent, courses: any[], courseId: number): Promise<{courseId?: number, categoryId?: number}> { + return new Promise((resolve, reject): any => { + const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, { + courses: courses, + courseId: courseId + }); + + popover.onDidDismiss((course) => { + if (course) { + resolve({courseId: course.id, categoryId: course.category}); + } else { + resolve({}); + } + }); + popover.present({ + ev: event + }); + }); + } } From f07d6e1df7233a3b7dda4f5583fcccd6d310e901 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Jul 2019 12:09:32 +0200 Subject: [PATCH 06/12] MOBILE-3021 calendar: Support links to calendar --- src/addon/calendar/calendar.module.ts | 9 +- .../calendar/addon-calendar-calendar.html | 2 +- .../calendar/components/calendar/calendar.ts | 44 +++---- .../addon-calendar-upcoming-events.html | 2 +- .../upcoming-events/upcoming-events.ts | 8 +- src/addon/calendar/pages/day/day.html | 2 +- src/addon/calendar/pages/day/day.ts | 10 +- src/addon/calendar/pages/event/event.html | 9 +- src/addon/calendar/pages/event/event.ts | 7 ++ src/addon/calendar/pages/index/index.html | 2 +- src/addon/calendar/pages/index/index.ts | 6 + src/addon/calendar/pages/list/list.ts | 9 +- src/addon/calendar/providers/calendar.ts | 118 +++++++++++++----- src/addon/calendar/providers/helper.ts | 4 +- .../calendar/providers/view-link-handler.ts | 113 +++++++++++++++++ src/providers/utils/url.ts | 11 ++ 16 files changed, 276 insertions(+), 80 deletions(-) create mode 100644 src/addon/calendar/providers/view-link-handler.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 66ab9f237..28765fad6 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -19,12 +19,14 @@ import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarSyncProvider } from './providers/calendar-sync'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; import { AddonCalendarSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonCalendarViewLinkHandler } from './providers/view-link-handler'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreCronDelegate } from '@providers/cron'; import { CoreInitDelegate } from '@providers/init'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreUpdateManagerProvider } from '@providers/update-manager'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; // List of providers (without handlers). export const ADDON_CALENDAR_PROVIDERS: any[] = [ @@ -45,17 +47,20 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarHelperProvider, AddonCalendarSyncProvider, AddonCalendarMainMenuHandler, - AddonCalendarSyncCronHandler + AddonCalendarSyncCronHandler, + AddonCalendarViewLinkHandler ] }) export class AddonCalendarModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler, initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider, localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider, - cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler) { + cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler, + contentLinksDelegate: CoreContentLinksDelegate, viewLinkHandler: AddonCalendarViewLinkHandler) { mainMenuDelegate.registerHandler(calendarHandler); cronDelegate.register(syncHandler); + contentLinksDelegate.registerHandler(viewLinkHandler); initDelegate.ready().then(() => { calendarProvider.scheduleAllSitesEventsNotifications(); diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 7c607ca63..ea373d297 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -40,7 +40,7 @@

- + {{ event.timestart * 1000 | coreFormatDate: timeFormat }} diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 2b40b8b3f..2a94768f8 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -91,7 +91,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest const now = new Date(); this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear(); - this.month = this.initialYear ? Number(this.initialYear) : now.getMonth() + 1; + this.month = this.initialMonth ? Number(this.initialMonth) : now.getMonth() + 1; this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); this.fetchData(); @@ -328,31 +328,33 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest 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; - }); - } + // Format online events. + day.events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); - 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 || this.deletedEvents.length) { + // There is offline data, merge it. - if (monthOfflineEvents && monthOfflineEvents[day.mday]) { - // Add the offline events (either new or edited). - day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday])); + 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])); + } } }); }); diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html index b338f0eca..0a4b7bb1e 100644 --- a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -8,7 +8,7 @@

-

+

{{ 'core.notsent' | translate }} diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index f358b5785..d362b6d6e 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -146,6 +146,8 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, fetchEvents(): Promise { // Don't pass courseId and categoryId, we'll filter them locally. return this.calendarProvider.getUpcomingEvents().then((result) => { + const promises = []; + this.onlineEvents = result.events; this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); @@ -158,8 +160,12 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, // Re-calculate the formatted time so it uses the device date. this.events.forEach((event) => { - event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat).then((time) => { + event.formattedtime = time; + })); }); + + return Promise.all(promises); }); } diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index e9b48bd52..134def736 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -49,7 +49,7 @@

-

+

{{ 'core.notsent' | translate }} diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index 924bfa0e7..43be54b5b 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -188,7 +188,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * View loaded. */ ngOnInit(): void { - this.currentMoment = moment().year(this.year).month(this.month - 1).day(this.day); + this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); this.fetchData(true, false); } @@ -273,6 +273,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { fetchEvents(): Promise { // Don't pass courseId and categoryId, we'll filter them locally. return this.calendarProvider.getDayEvents(this.year, this.month, this.day).then((result) => { + const promises = []; // 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, this.day).getTime(), @@ -288,9 +289,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.filterEvents(); // Re-calculate the formatted time so it uses the device date. + const dayTime = this.currentMoment.unix() * 1000; this.events.forEach((event) => { - event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => { + event.formattedtime = time; + })); }); + + return Promise.all(promises); }); } diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 23e55aac7..6e64b6fe0 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -29,18 +29,11 @@

+

{{ 'core.deletedoffline' | translate }}
- -

{{ 'addon.calendar.eventstarttime' | translate}}

-

{{ event.timestart * 1000 | coreFormatDate }}

-
- -

{{ 'addon.calendar.eventendtime' | translate}}

-

{{ (event.timestart + event.timeduration) * 1000 | coreFormatDate }}

-

{{ 'core.course' | translate}}

diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index bfb45499c..7af265152 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -311,6 +311,13 @@ export class AddonCalendarEventPage implements OnDestroy { event.deleted = deleted; })); + // Re-calculate the formatted time so it uses the device date. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((timeFormat) => { + this.calendarProvider.formatEventTime(event, timeFormat).then((time) => { + event.formattedtime = time; + }); + })); + return Promise.all(promises); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index 099892df8..ca8e832f3 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -28,7 +28,7 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 59088156b..341cf36b3 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -53,6 +53,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { protected manualSyncObserver: any; protected onlineObserver: any; + year: number; + month: number; courseId: number; categoryId: number; canCreate = false; @@ -82,8 +84,12 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.eventId = navParams.get('eventId') || false; + this.year = navParams.get('year'); + this.month = navParams.get('month'); this.notificationsEnabled = localNotificationsProvider.isAvailable(); this.currentSiteId = sitesProvider.getCurrentSiteId(); + this.loadUpcoming = !!navParams.get('upcoming'); + this.showCalendar = !this.loadUpcoming; // Listen for events added. When an event is added, reload the data. this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 1c7572e12..0b864a78a 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -53,7 +53,6 @@ export class AddonCalendarListPage implements OnDestroy { protected siteHomeId: number; protected obsDefaultTimeChange: any; protected eventId: number; - protected preSelectedCourseId: number; protected newEventObserver: any; protected discardedObserver: any; protected editEventObserver: any; @@ -101,7 +100,7 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventId = navParams.get('eventId') || false; - this.preSelectedCourseId = navParams.get('courseId') || null; + this.courseId = navParams.get('courseId'); // Listen for events added. When an event is added, reload the data. this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { @@ -284,12 +283,6 @@ export class AddonCalendarListPage implements OnDestroy { this.courses = result.courses; this.categoryId = result.categoryId; - if (this.preSelectedCourseId) { - this.filter.course = courses.find((course) => { - return course.id == this.preSelectedCourseId; - }); - } - return this.fetchEvents(refresh); })); diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 3bb868034..e7c0800f3 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -18,7 +18,9 @@ import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreConstants } from '@core/constants'; @@ -259,7 +261,9 @@ export class AddonCalendarProvider { private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, + private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, + private urlUtils: CoreUrlUtilsProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, private utils: CoreUtilsProvider, @@ -487,15 +491,19 @@ export class AddonCalendarProvider { } /** - * Format event time. Equivalent to calendar_format_event_time. + * Format event time. Similar to calendar_format_event_time. * * @param {any} event Event to format. * @param {string} format Calendar time format (from getCalendarTimeFormat). * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @param {number} [seenDay] Timestamp of day currently seen. If set, the function will not add links to this day. * @param {number} [showTime=0] Determine the show time GMT timestamp. - * @return {string} Formatted event time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the formatted event time. */ - formatEventTime(event: any, format: string, useCommonWords: boolean = true, showTime: number = 0): string { + formatEventTime(event: any, format: string, useCommonWords: boolean = true, seenDay?: number, showTime: number = 0, + siteId?: string): Promise { + const start = event.timestart * 1000, end = (event.timestart + event.timeduration) * 1000; let time; @@ -511,46 +519,50 @@ export class AddonCalendarProvider { this.timeUtils.userDate(end, format); } - if (!showTime) { - return this.getDayRepresentation(start, useCommonWords) + ', ' + time; - } else { - return time; - } - } else { // Event lasts more than one day. - const midnightStart = moment(start).startOf('day').unix() * 1000, - midnightEnd = moment(end).startOf('day').unix() * 1000, - timeStart = this.timeUtils.userDate(start, format), - timeEnd = this.timeUtils.userDate(end, format); - let dayStart = this.getDayRepresentation(start, useCommonWords) + ', ', - dayEnd = this.getDayRepresentation(end, useCommonWords) + ', '; + const timeStart = this.timeUtils.userDate(start, format), + timeEnd = this.timeUtils.userDate(end, format), + promises = []; - if (showTime == midnightStart) { - dayStart = ''; + // Don't use common words when the event lasts more than one day. + let dayStart = this.getDayRepresentation(start, false) + ', ', + dayEnd = this.getDayRepresentation(end, false) + ', '; + + // Add links to the days if needed. + if (dayStart && (!seenDay || !moment(seenDay).isSame(start, 'day'))) { + promises.push(this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => { + dayStart = this.urlUtils.buildLink(url, dayStart); + })); + } + if (dayEnd && (!seenDay || !moment(seenDay).isSame(end, 'day'))) { + promises.push(this.getViewUrl('day', end / 1000, undefined, siteId).then((url) => { + dayEnd = this.urlUtils.buildLink(url, dayEnd); + })); } - if (showTime == midnightEnd) { - dayEnd = ''; - } - - // Set printable representation. - if (moment().isSame(start, 'day')) { - // Event starts today, don't display the day. - return timeStart + ' » ' + dayEnd + timeEnd; - } else { - // The event starts in the future, print both days. + return Promise.all(promises).then(() => { return dayStart + timeStart + ' » ' + dayEnd + timeEnd; - } + }); } - } else { // There is no time duration. - const time = this.timeUtils.userDate(start, format); + } else { + // There is no time duration. + time = this.timeUtils.userDate(start, format); + } - if (!showTime) { - return this.getDayRepresentation(start, useCommonWords) + ', ' + time.trim(); + if (!showTime) { + // Display day + time. + if (seenDay && moment(seenDay).isSame(start, 'day')) { + // This day is currently being displayed, don't add an link. + return Promise.resolve(this.getDayRepresentation(start, useCommonWords) + ', ' + time); } else { - return time; + // Add link to view the day. + return this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => { + return this.urlUtils.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time; + }); } + } else { + return Promise.resolve(time); } } @@ -841,6 +853,21 @@ export class AddonCalendarProvider { }); } + /** + * Return the normalised event type. + * Activity events are normalised to be course events. + * + * @param {any} event The event to get its type. + * @return {string} Event type. + */ + getEventType(event: any): string { + if (event.modulename) { + return 'course'; + } + + return event.eventtype; + } + /** * Remove an event reminder and cancel the notification. * @@ -1155,6 +1182,31 @@ export class AddonCalendarProvider { return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : ''); } + /** + * Get URL to view a calendar. + * + * @param {string} view The view to load: 'month', 'day', 'upcoming', etc. + * @param {number} [time] Time to load. If not defined, current time. + * @param {string} [courseId] Course to load. If not defined, all courses. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the URL.x + */ + getViewUrl(view: string, time?: number, courseId?: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + let url = this.textUtils.concatenatePaths(site.getURL(), 'calendar/view.php?view=' + view); + + if (time) { + url += '&time=' + time; + } + + if (courseId) { + url += '&course=' + courseId; + } + + return url; + }); + } + /** * Get the week days, already ordered according to a specified starting day. * diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 225ec7ffb..575500c16 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -54,7 +54,7 @@ export class AddonCalendarHelperProvider { const types = {}; events.forEach((event) => { - types[event.eventtype] = true; + types[event.formattedType || event.eventtype] = true; if (event.islastday) { day.haslastdayofevent = true; @@ -133,6 +133,8 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } + e.formattedType = this.calendarProvider.getEventType(e); + if (typeof e.duration != 'undefined') { // It's an offline event, add some calculated data. e.format = 1; diff --git a/src/addon/calendar/providers/view-link-handler.ts b/src/addon/calendar/providers/view-link-handler.ts new file mode 100644 index 000000000..e0ba641c0 --- /dev/null +++ b/src/addon/calendar/providers/view-link-handler.ts @@ -0,0 +1,113 @@ +// (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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonCalendarProvider } from './calendar'; + +/** + * Content links handler for calendar view page. + */ +@Injectable() +export class AddonCalendarViewLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonCalendarViewLinkHandler'; + pattern = /\/calendar\/view\.php/; + + protected SUPPORTED_VIEWS = ['month', 'mini', 'minithree', 'day', 'upcoming', 'upcoming_mini']; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private calendarProvider: AddonCalendarProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + if (!params.view || params.view == 'month' || params.view == 'mini' || params.view == 'minithree') { + // Monthly view, open the calendar tab. + const stateParams: any = { + courseId: params.course + }, + timestamp = params.time ? params.time * 1000 : Date.now(); + + const date = new Date(timestamp); + stateParams.year = date.getFullYear(); + stateParams.month = date.getMonth() + 1; + + this.linkHelper.goInSite(navCtrl, 'AddonCalendarIndexPage', stateParams, siteId); // @todo: Add checkMenu param. + + } else if (params.view == 'day') { + // Daily view, open the page. + const stateParams: any = { + courseId: params.course + }, + timestamp = params.time ? params.time * 1000 : Date.now(); + + const date = new Date(timestamp); + stateParams.year = date.getFullYear(); + stateParams.month = date.getMonth() + 1; + stateParams.day = date.getDate(); + + this.linkHelper.goInSite(navCtrl, 'AddonCalendarDayPage', stateParams, siteId); + + } else if (params.view == 'upcoming' || params.view == 'upcoming_mini') { + // Upcoming view, open the calendar tab. + const stateParams: any = { + courseId: params.course, + upcoming: true, + }; + + this.linkHelper.goInSite(navCtrl, 'AddonCalendarIndexPage', stateParams, siteId); // @todo: Add checkMenu param. + + } + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (params.view && this.SUPPORTED_VIEWS.indexOf(params.view) == -1) { + // This type of view isn't supported in the app. + return false; + } + + return this.calendarProvider.isDisabled(siteId).then((disabled) => { + if (disabled) { + return false; + } + + return this.calendarProvider.canViewMonth(siteId); + }); + } +} diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts index 3035f53c8..70bd15d55 100644 --- a/src/providers/utils/url.ts +++ b/src/providers/utils/url.ts @@ -44,6 +44,17 @@ export class CoreUrlUtilsProvider { return url; } + /** + * Given a URL and a text, return an HTML link. + * + * @param {string} url URL. + * @param {string} text Text of the link. + * @return {string} Link. + */ + buildLink(url: string, text: string): string { + return '
' + text + ''; + } + /** * Extracts the parameters from a URL and stores them in an object. * From 6d94c961f17634aba51327c815c20942557db9b3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Jul 2019 12:56:55 +0200 Subject: [PATCH 07/12] MOBILE-3021 calendar: Improve event page display --- scripts/langindex.json | 6 ++-- .../calendar/addon-calendar-calendar.html | 1 + .../components/calendar/calendar.scss | 7 +++++ src/addon/calendar/lang/en.json | 1 + src/addon/calendar/pages/event/event.html | 28 ++++++++++++++----- src/addon/calendar/pages/event/event.ts | 25 ++++------------- src/assets/lang/en.json | 1 + 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index d80355727..aa47c8821 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -30,6 +30,7 @@ "addon.block_badges.pluginname": "block_badges", "addon.block_blogmenu.pluginname": "block_blog_menu", "addon.block_blogrecent.nocourses": "block_blog_recent", + "addon.block_blogrecent.pluginname": "block_blog_recent", "addon.block_blogtags.pluginname": "block_blog_tags", "addon.block_calendarmonth.pluginname": "block_calendar_month", "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming", @@ -51,12 +52,12 @@ "addon.block_newsitems.pluginname": "block_news_items", "addon.block_onlineusers.pluginname": "block_online_users", "addon.block_privatefiles.pluginname": "block_private_files", + "addon.block_recentactivity.pluginname": "block_recent_activity", "addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses", "addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses", "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", - "addon.block_recentactivity.pluginname": "block_recent_activity", + "addon.block_recentlyaccesseditems.pluginname": "block_recentlyaccesseditems", "addon.block_rssclient.pluginname": "block_rss_client", - "addon.block_glossaryrandom.pluginname": "block_glossary_random", "addon.block_selfcompletion.pluginname": "block_selfcompletion", "addon.block_sitemainmenu.pluginname": "block_site_main_menu", "addon.block_starredcourses.nocourses": "block_starredcourses", @@ -149,6 +150,7 @@ "addon.calendar.upcomingevents": "calendar", "addon.calendar.wed": "calendar", "addon.calendar.wednesday": "calendar", + "addon.calendar.when": "calendar", "addon.calendar.yesterday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index ea373d297..1cc977fdf 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -43,6 +43,7 @@ + {{ event.timestart * 1000 | coreFormatDate: timeFormat }} {{event.name}}

diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index af7182c32..b764187e4 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -48,4 +48,11 @@ ion-app.app-root addon-calendar-calendar { background-color: $calendar-event-site-color; } } + + .core-module-icon { + @include margin-horizontal(1px, 1px); + width: 16px; + height: 16px; + display: inline; + } } \ No newline at end of file diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 8d19b4559..be2bab15a 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -65,5 +65,6 @@ "upcomingevents": "Upcoming events", "wed": "Wed", "wednesday": "Wednesday", + "when": "When", "yesterday": "Yesterday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 6e64b6fe0..366327f66 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -1,6 +1,10 @@ - + + + + + @@ -26,14 +30,26 @@ - + + -

-

+

{{ 'addon.calendar.eventname' | translate }}

+

{{ 'core.deletedoffline' | translate }}
+ +

{{ 'addon.calendar.when' | translate }}

+

+ + {{ 'core.deletedoffline' | translate }} + +
+ +

{{ 'addon.calendar.eventtype' | translate }}

+

{{ 'addon.calendar.type' + event.formattedType | translate }}

+

{{ 'core.course' | translate}}

@@ -46,10 +62,8 @@

{{ 'core.category' | translate}}

- - {{event.moduleName}} - +

{{ 'core.description' | translate}}

diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 7af265152..fb9354748 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -57,7 +57,6 @@ export class AddonCalendarEventPage implements OnDestroy { notificationMax: string; notificationTimeText: string; event: any = {}; - title: string; courseName: string; groupName: string; courseUrl = ''; @@ -236,8 +235,6 @@ export class AddonCalendarEventPage implements OnDestroy { this.courseUrl = ''; this.moduleUrl = ''; - // Guess event title. - let title = this.translate.instant('addon.calendar.type' + event.eventtype); if (event.moduleIcon) { // It's a module event, translate the module name to the current language. const name = this.courseProvider.translateModuleName(event.modulename); @@ -245,27 +242,12 @@ export class AddonCalendarEventPage implements OnDestroy { event.moduleName = name; } - // Calculate the title of the page; - if (title == 'addon.calendar.type' + event.eventtype) { - title = this.translate.instant('core.mod_' + event.modulename + '.' + event.eventtype); - - if (title == 'core.mod_' + event.modulename + '.' + event.eventtype) { - title = name; - } - } - // Get the module URL. if (canGetById) { this.moduleUrl = event.url; } - } else { - if (title == 'addon.calendar.type' + event.eventtype) { - title = event.name; - } } - this.title = title; - // If the event belongs to a course, get the course name and the URL to view it. if (canGetById && event.course && event.course.id != this.siteHomeId) { this.courseName = event.course.fullname; @@ -403,7 +385,12 @@ export class AddonCalendarEventPage implements OnDestroy { refreshEvent(sync?: boolean, showErrors?: boolean): Promise { this.syncIcon = 'spinner'; - return this.calendarProvider.invalidateEvent(this.eventId).catch(() => { + const promises = []; + + promises.push(this.calendarProvider.invalidateEvent(this.eventId)); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + return Promise.all(promises).catch(() => { // Ignore errors. }).then(() => { return this.fetchEvent(sync, showErrors); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index b78160d84..a8564ee92 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -149,6 +149,7 @@ "addon.calendar.upcomingevents": "Upcoming events", "addon.calendar.wed": "Wed", "addon.calendar.wednesday": "Wednesday", + "addon.calendar.when": "When", "addon.calendar.yesterday": "Yesterday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", From 31c92398b3e884a25d16d06c3ae26ffc959b29d6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 18 Jul 2019 17:22:51 +0200 Subject: [PATCH 08/12] MOBILE-3021 calendar: Schedule event notifications --- .../calendar/components/calendar/calendar.ts | 18 ++++++++++++++++++ .../upcoming-events/upcoming-events.ts | 14 ++++++++++++++ src/addon/calendar/pages/day/day.ts | 12 ++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 2a94768f8..cbeeb6379 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -14,6 +14,7 @@ import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -56,9 +57,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest // Observers. protected undeleteEventObserver: any; + protected obsDefaultTimeChange: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + localNotificationsProvider: CoreLocalNotificationsProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarOffline: AddonCalendarOfflineProvider, @@ -69,6 +72,17 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.currentSiteId = sitesProvider.getCurrentSiteId(); + if (localNotificationsProvider.isAvailable()) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + this.weeks.forEach((week) => { + week.days.forEach((day) => { + calendarProvider.scheduleEventsNotifications(day.events); + }); + }); + }, this.currentSiteId); + } + // Listen for events "undeleted" (offline). this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { if (data && data.eventId) { @@ -334,6 +348,9 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest // Format online events. day.events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Schedule notifications for the events retrieved (only future events will be scheduled). + this.calendarProvider.scheduleEventsNotifications(day.events); + if (monthOfflineEvents || this.deletedEvents.length) { // There is offline data, merge it. @@ -403,5 +420,6 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest */ ngOnDestroy(): void { this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); } } diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts index d362b6d6e..d540dae6e 100644 --- a/src/addon/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -14,6 +14,7 @@ import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { AddonCalendarProvider } from '../../providers/calendar'; @@ -51,9 +52,11 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, // Observers. protected undeleteEventObserver: any; + protected obsDefaultTimeChange: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, + localNotificationsProvider: CoreLocalNotificationsProvider, private calendarProvider: AddonCalendarProvider, private calendarHelper: AddonCalendarHelperProvider, private calendarOffline: AddonCalendarOfflineProvider, @@ -62,6 +65,13 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, this.currentSiteId = sitesProvider.getCurrentSiteId(); + if (localNotificationsProvider.isAvailable()) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + calendarProvider.scheduleEventsNotifications(this.onlineEvents); + }, this.currentSiteId); + } + // Listen for events "undeleted" (offline). this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { if (data && data.eventId) { @@ -152,6 +162,9 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Schedule notifications for the events retrieved. + this.calendarProvider.scheduleEventsNotifications(this.onlineEvents); + // Merge the online events with offline data. this.events = this.mergeEvents(); @@ -319,5 +332,6 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, */ ngOnDestroy(): void { this.undeleteEventObserver && this.undeleteEventObserver.off(); + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); } } diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index 43be54b5b..d18e25d75 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -61,6 +61,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected syncObserver: any; protected manualSyncObserver: any; protected onlineObserver: any; + protected obsDefaultTimeChange: any; periodName: string; filteredEvents = []; @@ -98,6 +99,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.courseId = navParams.get('courseId'); this.currentSiteId = sitesProvider.getCurrentSiteId(); + if (localNotificationsProvider.isAvailable()) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + calendarProvider.scheduleEventsNotifications(this.onlineEvents); + }, this.currentSiteId); + } + // 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) { @@ -282,6 +290,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.onlineEvents = result.events; this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + // Schedule notifications for the events retrieved (only future events will be scheduled). + this.calendarProvider.scheduleEventsNotifications(this.onlineEvents); + // Merge the online events with offline data. this.events = this.mergeEvents(); @@ -609,5 +620,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.syncObserver && this.syncObserver.off(); this.manualSyncObserver && this.manualSyncObserver.off(); this.onlineObserver && this.onlineObserver.unsubscribe(); + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); } } From d7d565086b5ff96d6c019c31b43467a17c109764 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jul 2019 09:07:53 +0200 Subject: [PATCH 09/12] MOBILE-3021 calendar: Fix calendar block links --- .../calendarmonth/providers/block-handler.ts | 14 +++++++++++--- .../calendarupcoming/providers/block-handler.ts | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/addon/block/calendarmonth/providers/block-handler.ts b/src/addon/block/calendarmonth/providers/block-handler.ts index 9dc4ce2ab..80e85edf8 100644 --- a/src/addon/block/calendarmonth/providers/block-handler.ts +++ b/src/addon/block/calendarmonth/providers/block-handler.ts @@ -16,6 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; +import { AddonCalendarProvider } from '@addon/calendar/providers/calendar'; /** * Block handler. @@ -25,7 +26,7 @@ export class AddonBlockCalendarMonthHandler extends CoreBlockBaseHandler { name = 'AddonBlockCalendarMonth'; blockName = 'calendar_month'; - constructor() { + constructor(private calendarProvider: AddonCalendarProvider) { super(); } @@ -41,12 +42,19 @@ export class AddonBlockCalendarMonthHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { + let link = 'AddonCalendarListPage'; + const linkParams: any = contextLevel == 'course' ? { courseId: instanceId } : {}; + + if (this.calendarProvider.canViewMonthInSite()) { + link = 'AddonCalendarIndexPage'; + } + return { title: 'addon.block_calendarmonth.pluginname', class: 'addon-block-calendar-month', component: CoreBlockOnlyTitleComponent, - link: 'AddonCalendarListPage', - linkParams: contextLevel == 'course' ? { courseId: instanceId } : null + link: link, + linkParams: linkParams }; } } diff --git a/src/addon/block/calendarupcoming/providers/block-handler.ts b/src/addon/block/calendarupcoming/providers/block-handler.ts index b7f4e8acd..c58a0d080 100644 --- a/src/addon/block/calendarupcoming/providers/block-handler.ts +++ b/src/addon/block/calendarupcoming/providers/block-handler.ts @@ -16,6 +16,7 @@ import { Injectable, Injector } from '@angular/core'; import { CoreBlockHandlerData } from '@core/block/providers/delegate'; import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block'; import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; +import { AddonCalendarProvider } from '@addon/calendar/providers/calendar'; /** * Block handler. @@ -25,7 +26,7 @@ export class AddonBlockCalendarUpcomingHandler extends CoreBlockBaseHandler { name = 'AddonBlockCalendarUpcoming'; blockName = 'calendar_upcoming'; - constructor() { + constructor(private calendarProvider: AddonCalendarProvider) { super(); } @@ -41,12 +42,20 @@ export class AddonBlockCalendarUpcomingHandler extends CoreBlockBaseHandler { getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) : CoreBlockHandlerData | Promise { + let link = 'AddonCalendarListPage'; + const linkParams: any = contextLevel == 'course' ? { courseId: instanceId } : {}; + + if (this.calendarProvider.canViewMonthInSite()) { + link = 'AddonCalendarIndexPage'; + linkParams.upcoming = true; + } + return { title: 'addon.block_calendarupcoming.pluginname', class: 'addon-block-calendar-upcoming', component: CoreBlockOnlyTitleComponent, - link: 'AddonCalendarListPage', - linkParams: contextLevel == 'course' ? { courseId: instanceId } : null + link: link, + linkParams: linkParams }; } } From 762dc4b0f686107a6d608027a270e7bdaee6f777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2019 11:42:22 +0200 Subject: [PATCH 10/12] MOBILE-3021 calendar: Add styling to monthly view --- .../calendar/addon-calendar-calendar.html | 22 +++--- .../components/calendar/calendar.scss | 76 ++++++++++++++++++- src/addon/calendar/pages/day/day.html | 4 +- src/addon/calendar/pages/day/day.scss | 9 +++ 4 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 src/addon/calendar/pages/day/day.scss diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 1cc977fdf..679b0996e 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -7,8 +7,8 @@ - -

{{ periodName }}

+ +

{{ periodName }}

@@ -19,22 +19,22 @@ - + - +

{{ day.shortname | translate }}

- - - + + +

{{ day.mday }}

-

+

@@ -44,14 +44,14 @@ - {{ event.timestart * 1000 | coreFormatDate: timeFormat }} - {{event.name}} + {{ event.timestart * 1000 | coreFormatDate: timeFormat }} + {{event.name}}

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

- +
diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss index b764187e4..f96cba14d 100644 --- a/src/addon/calendar/components/calendar/calendar.scss +++ b/src/addon/calendar/components/calendar/calendar.scss @@ -4,11 +4,81 @@ $calendar-event-course-color: $red !default; // Red. $calendar-event-group-color: $yellow !default; // Yellow. $calendar-event-user-color: $blue !default; // Blue. $calendar-event-site-color: $green !default; // Green. +$calendar-today-bgcolor: $core-color !default; +$calendar-today-color: $white !default; +$calendar-border-color: $gray !default; ion-app.app-root addon-calendar-calendar { - .addon-calendar-weekdays { - opacity: 0.4; + .addon-calendar-months { + background-color: white; + padding: 0; + } + + .addon-calendar-day { + border-bottom: 1px solid $calendar-border-color; + @include border-end(1px, solid, $calendar-border-color); + overflow: hidden; + min-height: 70px; + + &:first-child { + @include padding(null, null, null, 10px); + } + &:last-child { + @include border-end(0, null, null); + @include padding(null, 8px, null, null); + } + .addon-calendar-day-number { + height: 24px; + line-height: 24px; + width: max-content; + min-width: 24px; + text-align: center; + font-weight: 500; + display: inline-block; + margin: 3px; + } + &.today .addon-calendar-day-number { + background-color: $calendar-today-bgcolor; + color: $calendar-today-color; + + border-radius: 50%; + } + &.dayblank { + background-color: $gray-lighter; + } + + .addon-calendar-event { + margin-top: 0.6em; + margin-bottom: 0.6em; + overflow: hidden; + white-space: nowrap; + + .addon-calendar-event-name { + font-weight: 500; + } + } + + .addon-calendar-day-more { + @include margin(0.6em, null, 0.6em, 4px); + } + + .addon-calendar-dot-types { + @include margin(0.6em, null, 0.6em, null); + } + } + + .addon-calendar-period { + flex-grow: 3; + h3 { + margin-top: 10px; + font-size: 1.6rem; + } + } + + .addon-calendar-weekday { + color: $gray-dark; + border-bottom: 1px solid $list-md-border-color; } .addon-calendar-day-events { @@ -32,6 +102,8 @@ ion-app.app-root addon-calendar-calendar { border: 1px solid white; @include margin-horizontal(1px, 1px); + + &.calendar_event_category { background-color: $calendar-event-category-color; } diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index 134def736..e6db57d36 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -24,8 +24,8 @@
- -

{{ periodName }}

+ +

{{ periodName }}

diff --git a/src/addon/calendar/pages/day/day.scss b/src/addon/calendar/pages/day/day.scss new file mode 100644 index 000000000..4918c1312 --- /dev/null +++ b/src/addon/calendar/pages/day/day.scss @@ -0,0 +1,9 @@ +page-addon-calendar-day { + .addon-calendar-period { + flex-grow: 3; + h3 { + margin-top: 10px; + font-size: 1.6rem; + } + } +} \ No newline at end of file From 26157100813deb2a4a5c78961e6c67f6332a2ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2019 12:41:16 +0200 Subject: [PATCH 11/12] MOBILE-3021 styles: Make it easy to change ionic colors --- src/theme/variables.scss | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 0062ebe09..9b5f9ae66 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -35,7 +35,6 @@ $core-color: $orange; // -------------------------------------------------- @import "bmma"; - $blue-light: mix($blue, white, 20%) !default; // Background. $blue-dark: darken($blue, 10%) !default; @@ -76,19 +75,31 @@ $content-padding: 10px; // colors so you can add, rename and remove colors as needed. // The "primary" color is the only required color in the map. +$primary: $core-color !default; +$secondary: $turquoise !default; +$danger: $red !default; +$light: $gray-lighter !default; +$color-gray: $gray-dark !default; +$dark: $black !default; +$warning: $yellow !default; +$success: $green !default; +$info: $blue !default; +$inverted-base: $white !default; +$inverted-contrast: $primary !default; + $colors: ( - primary: $core-color, - secondary: $turquoise, - danger: $red, - light: $gray-lighter, - gray: $gray-dark, - dark: $black, - warning: $yellow, - success: $green, - info: $blue, + primary: $primary, + secondary: $secondary, + danger: $danger, + light: $light, + gray: $color-gray, + dark: $dark, + warning: $warning, + success: $success, + info: $info, inverted: ( - base: $white, - contrast: $core-color + base: $inverted-base, + contrast: $inverted-contrast ) ); From 12e61f1f5887aeb1234dec23ac418e6a03200a5a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 19 Jul 2019 15:52:43 +0200 Subject: [PATCH 12/12] MOBILE-3021 calendar: Add a button to view current month or day --- .../calendar/addon-calendar-calendar.html | 8 +++ .../calendar/components/calendar/calendar.ts | 43 ++++++++++++++- src/addon/calendar/pages/day/day.html | 3 ++ src/addon/calendar/pages/day/day.ts | 52 ++++++++++++++++++- src/addon/calendar/pages/index/index.html | 4 +- 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index 679b0996e..5872e86d0 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -1,3 +1,11 @@ + + + + + + diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index cbeeb6379..dcdb412b1 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -37,6 +37,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest @Input() courseId: number | string; @Input() categoryId: number | string; // Category ID the course belongs to. @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true. + @Input() displayNavButtons?: string | boolean; // Whether to display nav buttons created by this component. Defaults to true. @Output() onEventClicked = new EventEmitter(); @Output() onDayClicked = new EventEmitter<{day: number, month: number, year: number}>(); @@ -45,6 +46,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest weeks: any[]; loaded = false; timeFormat: string; + isCurrentMonth: boolean; protected year: number; protected month: number; @@ -106,7 +108,8 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear(); this.month = this.initialMonth ? Number(this.initialMonth) : now.getMonth() + 1; - this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); + + this.calculateIsCurrentMonth(); this.fetchData(); } @@ -115,6 +118,9 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest * Detect changes on input properties. */ ngOnChanges(changes: {[name: string]: SimpleChange}): void { + this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate); + this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true : + this.utils.isTrueOrOne(this.displayNavButtons); if ((changes.courseId || changes.categoryId) && this.weeks) { this.filterEvents(); @@ -274,6 +280,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.decreaseMonth(); }).finally(() => { + this.calculateIsCurrentMonth(); this.loaded = true; }); } @@ -290,6 +297,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.increaseMonth(); }).finally(() => { + this.calculateIsCurrentMonth(); this.loaded = true; }); } @@ -312,6 +320,39 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.onDayClicked.emit({day: day, month: this.month, year: this.year}); } + /** + * Check if user is viewing the current month. + */ + calculateIsCurrentMonth(): void { + const now = new Date(); + + this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1; + } + + /** + * Go to current month. + */ + goToCurrentMonth(): void { + const now = new Date(), + initialMonth = this.month, + initialYear = this.year; + + this.month = now.getMonth() + 1; + this.year = now.getFullYear(); + + this.loaded = false; + + this.fetchEvents().then(() => { + this.isCurrentMonth = true; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.year = initialYear; + this.month = initialMonth; + }).finally(() => { + this.loaded = true; + }); + } + /** * Decrease the current month. */ diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html index e6db57d36..0ef39ef72 100644 --- a/src/addon/calendar/pages/day/day.html +++ b/src/addon/calendar/pages/day/day.html @@ -5,6 +5,9 @@ + diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts index d18e25d75..a8c1e90a1 100644 --- a/src/addon/calendar/pages/day/day.ts +++ b/src/addon/calendar/pages/day/day.ts @@ -73,6 +73,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { hasOffline = false; isOnline = false; syncIcon: string; + isCurrentDay: boolean; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, @@ -196,7 +197,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * View loaded. */ ngOnInit(): void { - this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); + this.calculateCurrentMoment(); + this.calculateIsCurrentDay(); this.fetchData(true, false); } @@ -533,6 +535,52 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.navCtrl.push('AddonCalendarEditEventPage', params); } + /** + * Calculate current moment. + */ + calculateCurrentMoment(): void { + this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day); + } + + /** + * Check if user is viewing the current day. + */ + calculateIsCurrentDay(): void { + const now = new Date(); + + this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate(); + } + + /** + * Go to current day. + */ + goToCurrentDay(): void { + const now = new Date(), + initialDay = this.day, + initialMonth = this.month, + initialYear = this.year; + + this.day = now.getDate(); + this.month = now.getMonth() + 1; + this.year = now.getFullYear(); + this.calculateCurrentMoment(); + + this.loaded = false; + + this.fetchEvents().then(() => { + this.isCurrentDay = true; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + + this.year = initialYear; + this.month = initialMonth; + this.day = initialDay; + this.calculateCurrentMoment(); + }).finally(() => { + this.loaded = true; + }); + } + /** * Load next month. */ @@ -545,6 +593,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.decreaseDay(); }).finally(() => { + this.calculateIsCurrentDay(); this.loaded = true; }); } @@ -561,6 +610,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); this.increaseDay(); }).finally(() => { + this.calculateIsCurrentDay(); this.loaded = true; }); } diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index ca8e832f3..adfbe89db 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -6,7 +6,7 @@