MOBILE-2667 blocks: Move my overview and timeline to blocks
This commit is contained in:
		
							parent
							
								
									cdb4e3078d
								
							
						
					
					
						commit
						7e2f2dfd4f
					
				| @ -10,6 +10,20 @@ | ||||
|   "addon.badges.issuername": "badges", | ||||
|   "addon.badges.nobadges": "badges", | ||||
|   "addon.badges.recipientdetails": "badges", | ||||
|   "addon.block_myoverview.future": "block_myoverview", | ||||
|   "addon.block_myoverview.inprogress": "block_myoverview", | ||||
|   "addon.block_myoverview.morecourses": "block_myoverview", | ||||
|   "addon.block_myoverview.nocoursesfuture": "block_myoverview", | ||||
|   "addon.block_myoverview.nocoursesinprogress": "block_myoverview", | ||||
|   "addon.block_myoverview.nocoursespast": "block_myoverview", | ||||
|   "addon.block_myoverview.past": "block_myoverview", | ||||
|   "addon.block_timeline.next30days": "block_timeline", | ||||
|   "addon.block_timeline.next7days": "block_timeline", | ||||
|   "addon.block_timeline.nocoursesinprogress": "block_timeline", | ||||
|   "addon.block_timeline.noevents": "block_timeline", | ||||
|   "addon.block_timeline.recentlyoverdue": "local_moodlemobileapp", | ||||
|   "addon.block_timeline.sortbycourses": "block_timeline", | ||||
|   "addon.block_timeline.sortbydates": "block_timeline", | ||||
|   "addon.calendar.calendar": "calendar", | ||||
|   "addon.calendar.calendarevents": "local_moodlemobileapp", | ||||
|   "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", | ||||
| @ -1135,34 +1149,20 @@ | ||||
|   "core.courses.errorselfenrol": "local_moodlemobileapp", | ||||
|   "core.courses.filtermycourses": "local_moodlemobileapp", | ||||
|   "core.courses.frontpage": "admin", | ||||
|   "core.courses.future": "block_myoverview", | ||||
|   "core.courses.inprogress": "block_myoverview", | ||||
|   "core.courses.morecourses": "block_myoverview", | ||||
|   "core.courses.mycourses": "moodle", | ||||
|   "core.courses.next30days": "block_timeline", | ||||
|   "core.courses.next7days": "block_timeline", | ||||
|   "core.courses.nocourses": "my", | ||||
|   "core.courses.nocoursesfuture": "block_myoverview", | ||||
|   "core.courses.nocoursesinprogress": "block_myoverview", | ||||
|   "core.courses.nocoursesoverview": "moodle/nocourses", | ||||
|   "core.courses.nocoursespast": "block_myoverview", | ||||
|   "core.courses.nocoursesyet": "moodle", | ||||
|   "core.courses.noevents": "block_timeline", | ||||
|   "core.courses.nosearchresults": "wiki", | ||||
|   "core.courses.notenroled": "completion", | ||||
|   "core.courses.notenrollable": "local_moodlemobileapp", | ||||
|   "core.courses.password": "local_moodlemobileapp", | ||||
|   "core.courses.past": "block_myoverview", | ||||
|   "core.courses.paymentrequired": "moodle", | ||||
|   "core.courses.paypalaccepted": "enrol_paypal", | ||||
|   "core.courses.recentlyoverdue": "local_moodlemobileapp", | ||||
|   "core.courses.search": "moodle", | ||||
|   "core.courses.searchcourses": "moodle", | ||||
|   "core.courses.searchcoursesadvice": "local_moodlemobileapp", | ||||
|   "core.courses.selfenrolment": "local_moodlemobileapp", | ||||
|   "core.courses.sendpaymentbutton": "enrol_paypal", | ||||
|   "core.courses.sortbycourses": "block_timeline", | ||||
|   "core.courses.sortbydates": "block_timeline", | ||||
|   "core.courses.timeline": "block_dashboard", | ||||
|   "core.courses.totalcoursesearchresults": "local_moodlemobileapp", | ||||
|   "core.currentdevice": "local_moodlemobileapp", | ||||
|  | ||||
							
								
								
									
										138
									
								
								src/addon/block/classes/block-component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/addon/block/classes/block-component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,138 @@ | ||||
| // (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 { Injector, OnInit } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| 
 | ||||
| /** | ||||
|  * Template class to easily create AddonBlockComponent of blocks. | ||||
|  */ | ||||
| export class AddonBlockComponent implements OnInit { | ||||
|     loaded: boolean; // If the component has been loaded.
 | ||||
|     protected fetchContentDefaultError: string; // Default error to show when loading contents.
 | ||||
| 
 | ||||
|     protected domUtils: CoreDomUtilsProvider; | ||||
|     protected logger; | ||||
| 
 | ||||
|     constructor(injector: Injector, loggerName: string = 'CoreCourseModuleMainResourceComponent') { | ||||
|         this.domUtils = injector.get(CoreDomUtilsProvider); | ||||
|         const loggerProvider = injector.get(CoreLoggerProvider); | ||||
|         this.logger = loggerProvider.getInstance(loggerName); | ||||
|     } | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.loaded = false; | ||||
|         this.loadContent(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param {any}       [refresher] Refresher. | ||||
|      * @param {Function}  [done] Function to call when done. | ||||
|      * @param {boolean}   [showErrors=false] If show errors to the user of hide them. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise<any> { | ||||
|         if (this.loaded) { | ||||
|             return this.invalidateContent().catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }).then(() => { | ||||
|                 return this.refreshContent(showErrors).finally(() => { | ||||
|                     refresher && refresher.complete(); | ||||
|                     done && done(); | ||||
|                 }); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the refresh content function. | ||||
|      * | ||||
|      * @param  {boolean}      [showErrors=false] Wether to show errors to the user or hide them. | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected refreshContent(showErrors: boolean = false): Promise<any> { | ||||
|         // Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs.
 | ||||
|         let promise; | ||||
| 
 | ||||
|         try { | ||||
|             promise = this.invalidateContent(); | ||||
|         } catch (ex) { | ||||
|             // An error ocurred in the function, log the error and just resolve the promise so the workflow continues.
 | ||||
|             this.logger.error(ex); | ||||
| 
 | ||||
|             promise = Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         return promise.catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }).then(() => { | ||||
|             return this.loadContent(true, showErrors); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected invalidateContent(): Promise<any> { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads the component contents and shows the corresponding error. | ||||
|      * | ||||
|      * @param {boolean}       [refresh=false] Whether we're refreshing data. | ||||
|      * @param  {boolean}      [showErrors=false] Wether to show errors to the user or hide them. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected loadContent(refresh?: boolean, showErrors: boolean = false): Promise<any> { | ||||
|         // Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs.
 | ||||
|         let promise; | ||||
| 
 | ||||
|         try { | ||||
|             promise = this.fetchContent(refresh); | ||||
|         } catch (ex) { | ||||
|             // An error ocurred in the function, log the error and just resolve the promise so the workflow continues.
 | ||||
|             this.logger.error(ex); | ||||
| 
 | ||||
|             promise = Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         return promise.catch((error) => { | ||||
|             // Error getting data, fail.
 | ||||
|             this.domUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true); | ||||
|         }).finally(() => { | ||||
|             this.loaded = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Download the component contents. | ||||
|      * | ||||
|      * @param {boolean} [refresh] Whether we're refreshing data. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchContent(refresh?: boolean): Promise<any> { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,42 @@ | ||||
| <!-- Buttons to add to the header. --> | ||||
| <core-navbar-buttons end> | ||||
|      <button [hidden]="!showFilterSwitchButton()" ion-button icon-only [attr.aria-label]="'core.courses.filtermycourses' | translate" (click)="switchFilter()"> | ||||
|         <ion-icon name="funnel"></ion-icon> | ||||
|     </button> | ||||
| </core-navbar-buttons> | ||||
| 
 | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
|     <!-- "Time" selector. --> | ||||
|     <div padding class="clearfix" [hidden]="showFilter" ion-row justify-content-between> | ||||
|         <ion-select float-start [title]="'core.show' | translate" [(ngModel)]="selectedFilter" ion-col (ngModelChange)="selectedChanged()" interface="popover" class="core-button-select"> | ||||
|             <ion-option value="inprogress">{{ 'addon.block_myoverview.inprogress' | translate }}</ion-option> | ||||
|             <ion-option value="future">{{ 'addon.block_myoverview.future' | translate }}</ion-option> | ||||
|             <ion-option value="past">{{ 'addon.block_myoverview.past' | translate }}</ion-option> | ||||
|         </ion-select> | ||||
|         <!-- Download all courses. --> | ||||
|         <div *ngIf="downloadAllCoursesEnabled && courses[selectedFilter] && courses[selectedFilter].length > 1" class="core-button-spinner"> | ||||
|             <button *ngIf="prefetchCoursesData[selectedFilter].icon && prefetchCoursesData[selectedFilter].icon != 'spinner'" ion-button icon-only clear color="dark" (click)="prefetchCourses()"> | ||||
|                 <core-icon [name]="prefetchCoursesData[selectedFilter].icon"></core-icon> | ||||
|             </button> | ||||
|             <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[selectedFilter].badge">{{prefetchCoursesData[selectedFilter].badge}}</ion-badge> | ||||
|             <ion-spinner *ngIf="!prefetchCoursesData[selectedFilter].icon || prefetchCoursesData[selectedFilter].icon == 'spinner'"></ion-spinner> | ||||
|         </div> | ||||
|     </div> | ||||
|     <core-empty-box *ngIf="courses[selectedFilter].length == 0 && selectedFilter == 'inprogress'" image="assets/img/icons/courses.svg" [message]="'addon.block_myoverview.nocoursesinprogress' | translate"></core-empty-box> | ||||
|     <core-empty-box *ngIf="courses[selectedFilter].length == 0 && selectedFilter == 'future'" image="assets/img/icons/courses.svg" [message]="'addon.block_myoverview.nocoursesfuture' | translate"></core-empty-box> | ||||
|     <core-empty-box *ngIf="courses[selectedFilter].length == 0 && selectedFilter == 'past'" image="assets/img/icons/courses.svg" [message]="'addon.block_myoverview.nocoursespast' | translate"></core-empty-box> | ||||
| 
 | ||||
|     <!-- Filter courses. --> | ||||
|     <ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate"> | ||||
|     </ion-searchbar> | ||||
|     <!-- List of courses. --> | ||||
|     <div> | ||||
|         <ion-grid no-padding> | ||||
|             <ion-row no-padding> | ||||
|                 <ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch> | ||||
|                     <core-courses-course-progress [course]="course" class="core-courseoverview"></core-courses-course-progress> | ||||
|                 </ion-col> | ||||
|             </ion-row> | ||||
|         </ion-grid> | ||||
|     </div> | ||||
| </core-loading> | ||||
							
								
								
									
										277
									
								
								src/addon/block/myoverview/component/myoverview.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								src/addon/block/myoverview/component/myoverview.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,277 @@ | ||||
| // (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, OnDestroy, ViewChild, Injector } from '@angular/core'; | ||||
| import { Searchbar } from 'ionic-angular'; | ||||
| import * as moment from 'moment'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCoursesProvider } from '@core/courses/providers/courses'; | ||||
| import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; | ||||
| import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; | ||||
| import { AddonBlockComponent } from '../../classes/block-component'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a my overview block. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-block-myoverview', | ||||
|     templateUrl: 'addon-block-myoverview.html' | ||||
| }) | ||||
| export class AddonBlockMyOverviewComponent extends AddonBlockComponent implements OnInit, OnDestroy { | ||||
|     @ViewChild('searchbar') searchbar: Searchbar; | ||||
| 
 | ||||
|     courses = { | ||||
|         filter: '', | ||||
|         past: [], | ||||
|         inprogress: [], | ||||
|         future: [] | ||||
|     }; | ||||
|     selectedFilter = 'inprogress'; | ||||
|     downloadAllCoursesEnabled: boolean; | ||||
|     filteredCourses: any[]; | ||||
|     prefetchCoursesData = { | ||||
|         inprogress: {}, | ||||
|         past: {}, | ||||
|         future: {} | ||||
|     }; | ||||
|     showFilter = false; | ||||
| 
 | ||||
|     protected prefetchIconsInitialized = false; | ||||
|     protected isDestroyed; | ||||
|     protected updateSiteObserver; | ||||
|     protected courseIds = []; | ||||
|     protected fetchContentDefaultError = 'Error getting my overview data.'; | ||||
| 
 | ||||
|     constructor(injector: Injector, private coursesProvider: CoreCoursesProvider, | ||||
|             private courseCompletionProvider: AddonCourseCompletionProvider, private eventsProvider: CoreEventsProvider, | ||||
|             private courseHelper: CoreCourseHelperProvider, private utils: CoreUtilsProvider, | ||||
|             private courseOptionsDelegate: CoreCourseOptionsDelegate, private coursesHelper: CoreCoursesHelperProvider) { | ||||
| 
 | ||||
|         super(injector, 'AddonBlockMyOverviewComponent'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|         // Refresh the enabled flags if site is updated.
 | ||||
|         this.updateSiteObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { | ||||
|             const wasEnabled = this.downloadAllCoursesEnabled; | ||||
| 
 | ||||
|             this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|             if (!wasEnabled && this.downloadAllCoursesEnabled && this.loaded) { | ||||
|                 // Download all courses is enabled now, initialize it.
 | ||||
|                 this.initPrefetchCoursesIcons(); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         super.ngOnInit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected invalidateContent(): Promise<any> { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.coursesProvider.invalidateUserCourses().finally(() => { | ||||
|             // Invalidate course completion data.
 | ||||
|             promises.push(this.coursesProvider.invalidateUserCourses().finally(() => { | ||||
|                 // Invalidate course completion data.
 | ||||
|                 return this.utils.allPromises(this.courseIds.map((courseId) => { | ||||
|                     return this.courseCompletionProvider.invalidateCourseCompletion(courseId); | ||||
|                  })); | ||||
|             })); | ||||
|         })); | ||||
| 
 | ||||
|         promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); | ||||
|         if (this.courseIds.length > 0) { | ||||
|             promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds.join(','))); | ||||
|         } | ||||
| 
 | ||||
|         return this.utils.allPromises(promises).finally(() => { | ||||
|             this.prefetchIconsInitialized = false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the courses for my overview. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchContent(): Promise<any> { | ||||
|         return this.coursesHelper.getUserCoursesWithOptions().then((courses) => { | ||||
|             // Fetch course completion status.
 | ||||
|             return Promise.all(courses.map((course) => { | ||||
|                 if (typeof course.enablecompletion != 'undefined' && course.enablecompletion == 0) { | ||||
|                     // Completion is disabled for this course, there is no need to fetch the completion status.
 | ||||
|                     return Promise.resolve(course); | ||||
|                 } | ||||
| 
 | ||||
|                 return this.courseCompletionProvider.getCompletion(course.id).catch(() => { | ||||
|                     // Ignore error, maybe course compleiton is disabled or user ha no permission.
 | ||||
|                 }).then((completion) => { | ||||
|                     course.completed = completion && completion.completed; | ||||
| 
 | ||||
|                     return course; | ||||
|                 }); | ||||
|             })); | ||||
|         }).then((courses) => { | ||||
|             const today = moment().unix(); | ||||
| 
 | ||||
|             this.courses.past = []; | ||||
|             this.courses.inprogress = []; | ||||
|             this.courses.future = []; | ||||
| 
 | ||||
|             courses.forEach((course) => { | ||||
|                 if ((course.enddate && course.enddate < today) || course.completed) { | ||||
|                     // Courses that have already ended.
 | ||||
|                     this.courses.past.push(course); | ||||
|                 } else if (course.startdate > today) { | ||||
|                     // Courses that have not started yet.
 | ||||
|                     this.courses.future.push(course); | ||||
|                 } else { | ||||
|                     // Courses still in progress.
 | ||||
|                     this.courses.inprogress.push(course); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             this.courses.filter = ''; | ||||
|             this.showFilter = false; | ||||
|             this.filteredCourses = this.courses[this.selectedFilter]; | ||||
| 
 | ||||
|             this.initPrefetchCoursesIcons(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The filter has changed. | ||||
|      * | ||||
|      * @param {any} Received Event. | ||||
|      */ | ||||
|     filterChanged(event: any): void { | ||||
|         const newValue = event.target.value && event.target.value.trim().toLowerCase(); | ||||
|         if (!newValue || !this.courses[this.selectedFilter]) { | ||||
|             this.filteredCourses = this.courses[this.selectedFilter]; | ||||
|         } else { | ||||
|             this.filteredCourses = this.courses[this.selectedFilter].filter((course) => { | ||||
|                 return course.fullname.toLowerCase().indexOf(newValue) > -1; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the prefetch icon for selected courses. | ||||
|      */ | ||||
|     protected initPrefetchCoursesIcons(): void { | ||||
|         if (this.prefetchIconsInitialized || !this.downloadAllCoursesEnabled) { | ||||
|             // Already initialized.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.prefetchIconsInitialized = true; | ||||
| 
 | ||||
|         Object.keys(this.prefetchCoursesData).forEach((filter) => { | ||||
|             if (!this.courses[filter] || this.courses[filter].length < 2) { | ||||
|                 // Not enough courses.
 | ||||
|                 this.prefetchCoursesData[filter].icon = ''; | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.courseHelper.determineCoursesStatus(this.courses[filter]).then((status) => { | ||||
|                 let icon = this.courseHelper.getCourseStatusIconAndTitleFromStatus(status).icon; | ||||
|                 if (icon == 'spinner') { | ||||
|                     // It seems all courses are being downloaded, show a download button instead.
 | ||||
|                     icon = 'cloud-download'; | ||||
|                 } | ||||
|                 this.prefetchCoursesData[filter].icon = icon; | ||||
|             }); | ||||
| 
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch all the shown courses. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     prefetchCourses(): Promise<any> { | ||||
|         const selected = this.selectedFilter, | ||||
|             selectedData = this.prefetchCoursesData[selected], | ||||
|             initialIcon = selectedData.icon; | ||||
| 
 | ||||
|         selectedData.icon = 'spinner'; | ||||
|         selectedData.badge = ''; | ||||
| 
 | ||||
|         return this.courseHelper.confirmAndPrefetchCourses(this.courses[this.selectedFilter], (progress) => { | ||||
|             selectedData.badge = progress.count + ' / ' + progress.total; | ||||
|         }).then(() => { | ||||
|             selectedData.icon = 'refresh'; | ||||
|         }).catch((error) => { | ||||
|             if (!this.isDestroyed) { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); | ||||
|                 selectedData.icon = initialIcon; | ||||
|             } | ||||
|         }).finally(() => { | ||||
|             selectedData.badge = ''; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The selected courses have changed. | ||||
|      */ | ||||
|     selectedChanged(): void { | ||||
|         this.filteredCourses = this.courses[this.selectedFilter]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show or hide the filter. | ||||
|      */ | ||||
|     switchFilter(): void { | ||||
|         this.showFilter = !this.showFilter; | ||||
|         this.courses.filter = ''; | ||||
|         this.filteredCourses = this.courses[this.selectedFilter]; | ||||
|         if (this.showFilter) { | ||||
|             setTimeout(() => { | ||||
|                 this.searchbar.setFocus(); | ||||
|             }, 500); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If switch button that enables the filter input is shown or not. | ||||
|      * | ||||
|      * @return {boolean} If switch button that enables the filter input is shown or not. | ||||
|      */ | ||||
|     showFilterSwitchButton(): boolean { | ||||
|         return this.loaded && this.courses[this.selectedFilter] && this.courses[this.selectedFilter].length > 5; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.updateSiteObserver && this.updateSiteObserver.off(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/addon/block/myoverview/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/addon/block/myoverview/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| { | ||||
|     "future": "Future", | ||||
|     "inprogress": "In progress", | ||||
|     "past": "Past", | ||||
|     "morecourses": "More courses", | ||||
|     "nocoursesfuture": "No future courses", | ||||
|     "nocoursesinprogress": "No in progress courses", | ||||
|     "nocoursespast": "No past courses" | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/addon/block/myoverview/myoverview.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/addon/block/myoverview/myoverview.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| // (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 { IonicModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreCoursesComponentsModule } from '@core/courses/components/components.module'; | ||||
| import { AddonBlockMyOverviewComponent } from './component/myoverview'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonBlockMyOverviewComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         IonicModule, | ||||
|         CoreComponentsModule, | ||||
|         CoreCoursesComponentsModule, | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonBlockMyOverviewComponent | ||||
|     ] | ||||
| }) | ||||
| export class AddonBlockMyOverviewModule {} | ||||
| @ -12,28 +12,28 @@ | ||||
| </ng-template> | ||||
| 
 | ||||
| <ion-item-group *ngIf="recentlyOverdue.length > 0"> | ||||
|     <ion-item-divider color="danger">{{ 'core.courses.recentlyoverdue' | translate }}</ion-item-divider> | ||||
|     <ion-item-divider color="danger">{{ 'addon.block_timeline.recentlyoverdue' | translate }}</ion-item-divider> | ||||
|     <ng-container *ngFor="let event of recentlyOverdue"> | ||||
|         <ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container> | ||||
|     </ng-container> | ||||
| </ion-item-group> | ||||
| 
 | ||||
| <ion-item-group *ngIf="next7Days.length > 0"> | ||||
|     <ion-item-divider color="light">{{ 'core.courses.next7days' | translate }}</ion-item-divider> | ||||
|     <ion-item-divider color="light">{{ 'addon.block_timeline.next7days' | translate }}</ion-item-divider> | ||||
|     <ng-container *ngFor="let event of next7Days"> | ||||
|         <ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container> | ||||
|     </ng-container> | ||||
| </ion-item-group> | ||||
| 
 | ||||
| <ion-item-group *ngIf="next30Days.length > 0"> | ||||
|     <ion-item-divider color="light">{{ 'core.courses.next30days' | translate }}</ion-item-divider> | ||||
|     <ion-item-divider color="light">{{ 'addon.block_timeline.next30days' | translate }}</ion-item-divider> | ||||
|     <ng-container *ngFor="let event of next30Days"> | ||||
|         <ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container> | ||||
|     </ng-container> | ||||
| </ion-item-group> | ||||
| 
 | ||||
| <ion-item-group *ngIf="future.length > 0"> | ||||
|     <ion-item-divider color="light">{{ 'core.courses.future' | translate }}</ion-item-divider> | ||||
|     <ion-item-divider color="light">{{ 'addon.block_myoverview.future' | translate }}</ion-item-divider> | ||||
|     <ng-container *ngFor="let event of future"> | ||||
|         <ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container> | ||||
|     </ng-container> | ||||
| @ -45,5 +45,5 @@ | ||||
|     <ion-spinner *ngIf="loadingMore"></ion-spinner> | ||||
| </div> | ||||
| 
 | ||||
| <core-empty-box *ngIf="empty && showCourse" image="assets/img/icons/activities.svg" [message]="'core.courses.noevents' | translate"></core-empty-box> | ||||
| <core-empty-box *ngIf="empty && !showCourse" [message]="'core.courses.noevents' | translate"></core-empty-box> | ||||
| <core-empty-box *ngIf="empty && showCourse" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"></core-empty-box> | ||||
| <core-empty-box *ngIf="empty && !showCourse" [message]="'addon.block_timeline.noevents' | translate"></core-empty-box> | ||||
| @ -26,10 +26,10 @@ import * as moment from 'moment'; | ||||
|  * Directive to render a list of events in course overview. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-courses-overview-events', | ||||
|     templateUrl: 'core-courses-overview-events.html' | ||||
|     selector: 'addon-block-timeline-events', | ||||
|     templateUrl: 'addon-block-timeline-events.html' | ||||
| }) | ||||
| export class CoreCoursesOverviewEventsComponent implements OnChanges { | ||||
| export class AddonBlockTimelineEventsComponent implements OnChanges { | ||||
|     @Input() events: any[]; // The events to render.
 | ||||
|     @Input() showCourse?: boolean | string; // Whether to show the course name.
 | ||||
|     @Input() canLoadMore?: boolean; // Whether more events can be loaded.
 | ||||
| @ -0,0 +1,21 @@ | ||||
| <div padding [hidden]="!loaded"> | ||||
|     <ion-select [(ngModel)]="sort" (ngModelChange)="switchSort()" interface="popover" class="core-button-select"> | ||||
|         <ion-option value="sortbydates">{{ 'addon.block_timeline.sortbydates' | translate }}</ion-option> | ||||
|         <ion-option value="sortbycourses">{{ 'addon.block_timeline.sortbycourses' | translate }}</ion-option> | ||||
|     </ion-select> | ||||
| </div> | ||||
| <core-loading [hideUntil]="loaded && timeline.loaded" [hidden]="sort != 'sortbydates'" class="core-loading-center"> | ||||
|     <addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMoreTimeline()"></addon-block-timeline-events> | ||||
| </core-loading> | ||||
| <core-loading [hideUntil]="loaded && timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" class="core-loading-center"> | ||||
|     <ion-grid no-padding> | ||||
|         <ion-row no-padding> | ||||
|             <ion-col *ngFor="let course of timelineCourses.courses" no-padding col-12 col-md-6> | ||||
|                 <core-courses-course-progress [course]="course"> | ||||
|                     <addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMoreCourse(course)"></addon-block-timeline-events> | ||||
|                 </core-courses-course-progress> | ||||
|             </ion-col> | ||||
|         </ion-row> | ||||
|     </ion-grid> | ||||
|     <core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" [message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box> | ||||
| </core-loading> | ||||
							
								
								
									
										172
									
								
								src/addon/block/timeline/components/timeline/timeline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/addon/block/timeline/components/timeline/timeline.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,172 @@ | ||||
| // (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, Injector } from '@angular/core'; | ||||
| import * as moment from 'moment'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCoursesProvider } from '@core/courses/providers/courses'; | ||||
| import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; | ||||
| import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; | ||||
| import { AddonBlockComponent } from '../../../classes/block-component'; | ||||
| import { AddonBlockTimelineProvider } from '../../providers/timeline'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a timeline block. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-block-timeline', | ||||
|     templateUrl: 'addon-block-timeline.html' | ||||
| }) | ||||
| export class AddonBlockTimelineComponent extends AddonBlockComponent implements OnInit { | ||||
|     sort = 'sortbydates'; | ||||
|     timeline = { | ||||
|         events: [], | ||||
|         loaded: false, | ||||
|         canLoadMore: undefined | ||||
|     }; | ||||
|     timelineCourses = { | ||||
|         courses: [], | ||||
|         loaded: false, | ||||
|         canLoadMore: false | ||||
|     }; | ||||
| 
 | ||||
|     protected courseIds = []; | ||||
|     protected fetchContentDefaultError = 'Error getting timeline data.'; | ||||
| 
 | ||||
|     constructor(injector: Injector, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, | ||||
|             private timelineProvider: AddonBlockTimelineProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, | ||||
|             private coursesHelper: CoreCoursesHelperProvider) { | ||||
| 
 | ||||
|         super(injector, 'AddonBlockTimelineComponent'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         super.ngOnInit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected invalidateContent(): Promise<any> { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         promises.push(this.timelineProvider.invalidateActionEventsByTimesort()); | ||||
|         promises.push(this.timelineProvider.invalidateActionEventsByCourses()); | ||||
|         promises.push(this.coursesProvider.invalidateUserCourses()); | ||||
|         promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); | ||||
|         if (this.courseIds.length > 0) { | ||||
|             promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds.join(','))); | ||||
|         } | ||||
| 
 | ||||
|         return this.utils.allPromises(promises); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the courses for my overview. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchContent(): Promise<any> { | ||||
|         if (this.sort == 'sortbydates') { | ||||
|             return this.fetchMyOverviewTimeline().finally(() => { | ||||
|                 this.timeline.loaded = true; | ||||
|             }); | ||||
|         } else if (this.sort == 'sortbycourses') { | ||||
|             return this.fetchMyOverviewTimelineByCourses().finally(() => { | ||||
|                 this.timelineCourses.loaded = true; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more events. | ||||
|      */ | ||||
|     loadMoreTimeline(): Promise<any> { | ||||
|         return this.fetchMyOverviewTimeline(this.timeline.canLoadMore).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, this.fetchContentDefaultError); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more events. | ||||
|      * | ||||
|      * @param {any} course Course. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     loadMoreCourse(course: any): Promise<any> { | ||||
|         return this.timelineProvider.getActionEventsByCourse(course.id, course.canLoadMore).then((courseEvents) => { | ||||
|             course.events = course.events.concat(courseEvents.events); | ||||
|             course.canLoadMore = courseEvents.canLoadMore; | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, this.fetchContentDefaultError); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the timeline. | ||||
|      * | ||||
|      * @param {number} [afterEventId] The last event id. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMyOverviewTimeline(afterEventId?: number): Promise<any> { | ||||
|         return this.timelineProvider.getActionEventsByTimesort(afterEventId).then((events) => { | ||||
|             this.timeline.events = events.events; | ||||
|             this.timeline.canLoadMore = events.canLoadMore; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the timeline by courses. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMyOverviewTimelineByCourses(): Promise<any> { | ||||
|         return this.coursesHelper.getUserCoursesWithOptions().then((courses) => { | ||||
|             const today = moment().unix(); | ||||
|             courses = courses.filter((course) => { | ||||
|                 return course.startdate <= today && (!course.enddate || course.enddate >= today); | ||||
|             }); | ||||
| 
 | ||||
|             this.timelineCourses.courses = courses; | ||||
|             if (courses.length > 0) { | ||||
|                 this.courseIds = courses.map((course) => { | ||||
|                     return course.id; | ||||
|                 }); | ||||
| 
 | ||||
|                 return this.timelineProvider.getActionEventsByCourses(this.courseIds).then((courseEvents) => { | ||||
|                     this.timelineCourses.courses.forEach((course) => { | ||||
|                         course.events = courseEvents[course.id].events; | ||||
|                         course.canLoadMore = courseEvents[course.id].canLoadMore; | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Change timeline sort being viewed. | ||||
|      */ | ||||
|     switchSort(): void { | ||||
|         if (!this.timeline.loaded && this.sort == 'sortbydates') { | ||||
|             this.fetchContent(); | ||||
|         } else if (!this.timelineCourses.loaded && this.sort == 'sortbycourses') { | ||||
|             this.fetchContent(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/addon/block/timeline/lang/en.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/addon/block/timeline/lang/en.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| { | ||||
|     "next30days": "Next 30 days", | ||||
|     "next7days": "Next 7 days", | ||||
|     "nocoursesinprogress": "No in progress courses", | ||||
|     "noevents": "No upcoming activities due", | ||||
|     "recentlyoverdue": "Recently overdue", | ||||
|     "sortbycourses": "Sort by courses", | ||||
|     "sortbydates": "Sort by dates" | ||||
| } | ||||
| @ -14,16 +14,16 @@ | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import * as moment from 'moment'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding course overview. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreCoursesMyOverviewProvider { | ||||
| export class AddonBlockTimelineProvider { | ||||
|     static EVENTS_LIMIT = 20; | ||||
|     static EVENTS_LIMIT_PER_COURSE = 10; | ||||
|     // Cache key was maintained when moving the functions to this file. It comes from core myoverview.
 | ||||
|     protected ROOT_CACHE_KEY = 'myoverview:'; | ||||
| 
 | ||||
|     constructor(private sitesProvider: CoreSitesProvider) { } | ||||
| @ -44,7 +44,7 @@ export class CoreCoursesMyOverviewProvider { | ||||
|                 data: any = { | ||||
|                     timesortfrom: time, | ||||
|                     courseid: courseId, | ||||
|                     limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE | ||||
|                     limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                     cacheKey: this.getActionEventsByCourseCacheKey(courseId) | ||||
| @ -88,7 +88,7 @@ export class CoreCoursesMyOverviewProvider { | ||||
|                 data = { | ||||
|                     timesortfrom: time, | ||||
|                     courseids: courseIds, | ||||
|                     limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE | ||||
|                     limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                     cacheKey: this.getActionEventsByCoursesCacheKey() | ||||
| @ -131,7 +131,7 @@ export class CoreCoursesMyOverviewProvider { | ||||
|             const time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
 | ||||
|                 data: any = { | ||||
|                     timesortfrom: time, | ||||
|                     limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT | ||||
|                     limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT | ||||
|                 }, | ||||
|                 preSets = { | ||||
|                     cacheKey: this.getActionEventsByTimesortCacheKey(afterEventId, data.limitnum), | ||||
| @ -222,33 +222,6 @@ export class CoreCoursesMyOverviewProvider { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if My Overview is disabled in a certain site. | ||||
|      * | ||||
|      * @param {CoreSite} [site] Site. If not defined, use current site. | ||||
|      * @return {boolean} Whether it's disabled. | ||||
|      */ | ||||
|     isDisabledInSite(site?: CoreSite): boolean { | ||||
|         site = site || this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.isFeatureDisabled('CoreMainMenuDelegate_CoreCourses'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if My Overview is available and not disabled. | ||||
|      * | ||||
|      * @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise. | ||||
|      */ | ||||
|     isEnabled(): Promise<boolean> { | ||||
|         if (!this.isDisabledInSite()) { | ||||
|             return this.isAvailable().catch(() => { | ||||
|                 return false; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(false); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles course events, filtering and treating if more can be loaded. | ||||
|      * | ||||
| @ -258,7 +231,7 @@ export class CoreCoursesMyOverviewProvider { | ||||
|      */ | ||||
|     protected treatCourseEvents(course: any, timeFrom: number): { events: any[], canLoadMore: number } { | ||||
|         const canLoadMore: number = | ||||
|             course.events.length >= CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined; | ||||
|             course.events.length >= AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined; | ||||
| 
 | ||||
|         // Filter events by time in case it uses cache.
 | ||||
|         course.events = course.events.filter((element) => { | ||||
							
								
								
									
										47
									
								
								src/addon/block/timeline/timeline.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/addon/block/timeline/timeline.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| // (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 { 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 { CoreCoursesComponentsModule } from '@core/courses/components/components.module'; | ||||
| import { AddonBlockTimelineComponent } from './components/timeline/timeline'; | ||||
| import { AddonBlockTimelineEventsComponent } from './components/events/events'; | ||||
| import { AddonBlockTimelineProvider } from './providers/timeline'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonBlockTimelineComponent, | ||||
|         AddonBlockTimelineEventsComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         IonicModule, | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CorePipesModule, | ||||
|         CoreCoursesComponentsModule, | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonBlockTimelineComponent, | ||||
|         AddonBlockTimelineEventsComponent | ||||
|     ], | ||||
|     providers: [ | ||||
|         AddonBlockTimelineProvider | ||||
|     ] | ||||
| }) | ||||
| export class AddonBlockTimelineModule {} | ||||
| @ -83,6 +83,8 @@ import { AddonCompetencyModule } from '@addon/competency/competency.module'; | ||||
| import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompletion.module'; | ||||
| import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module'; | ||||
| import { AddonFilesModule } from '@addon/files/files.module'; | ||||
| import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; | ||||
| import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; | ||||
| import { AddonModAssignModule } from '@addon/mod/assign/assign.module'; | ||||
| import { AddonModBookModule } from '@addon/mod/book/book.module'; | ||||
| import { AddonModChatModule } from '@addon/mod/chat/chat.module'; | ||||
| @ -192,6 +194,8 @@ export const CORE_PROVIDERS: any[] = [ | ||||
|         AddonCourseCompletionModule, | ||||
|         AddonUserProfileFieldModule, | ||||
|         AddonFilesModule, | ||||
|         AddonBlockMyOverviewModule, | ||||
|         AddonBlockTimelineModule, | ||||
|         AddonModAssignModule, | ||||
|         AddonModBookModule, | ||||
|         AddonModChatModule, | ||||
|  | ||||
| @ -10,6 +10,20 @@ | ||||
|     "addon.badges.issuername": "Issuer name", | ||||
|     "addon.badges.nobadges": "There are no badges available.", | ||||
|     "addon.badges.recipientdetails": "Recipient details", | ||||
|     "addon.block_myoverview.future": "Future", | ||||
|     "addon.block_myoverview.inprogress": "In progress", | ||||
|     "addon.block_myoverview.morecourses": "More courses", | ||||
|     "addon.block_myoverview.nocoursesfuture": "No future courses", | ||||
|     "addon.block_myoverview.nocoursesinprogress": "No in progress courses", | ||||
|     "addon.block_myoverview.nocoursespast": "No past courses", | ||||
|     "addon.block_myoverview.past": "Past", | ||||
|     "addon.block_timeline.next30days": "Next 30 days", | ||||
|     "addon.block_timeline.next7days": "Next 7 days", | ||||
|     "addon.block_timeline.nocoursesinprogress": "No in progress courses", | ||||
|     "addon.block_timeline.noevents": "No upcoming activities due", | ||||
|     "addon.block_timeline.recentlyoverdue": "Recently overdue", | ||||
|     "addon.block_timeline.sortbycourses": "Sort by courses", | ||||
|     "addon.block_timeline.sortbydates": "Sort by dates", | ||||
|     "addon.calendar.calendar": "Calendar", | ||||
|     "addon.calendar.calendarevents": "Calendar events", | ||||
|     "addon.calendar.defaultnotificationtime": "Default notification time", | ||||
| @ -1135,34 +1149,20 @@ | ||||
|     "core.courses.errorselfenrol": "An error occurred while self enrolling.", | ||||
|     "core.courses.filtermycourses": "Filter my courses", | ||||
|     "core.courses.frontpage": "Front page", | ||||
|     "core.courses.future": "Future", | ||||
|     "core.courses.inprogress": "In progress", | ||||
|     "core.courses.morecourses": "More courses", | ||||
|     "core.courses.mycourses": "My courses", | ||||
|     "core.courses.next30days": "Next 30 days", | ||||
|     "core.courses.next7days": "Next 7 days", | ||||
|     "core.courses.nocourses": "No course information to show.", | ||||
|     "core.courses.nocoursesfuture": "No future courses", | ||||
|     "core.courses.nocoursesinprogress": "No in progress courses", | ||||
|     "core.courses.nocoursesoverview": "No courses", | ||||
|     "core.courses.nocoursespast": "No past courses", | ||||
|     "core.courses.nocoursesyet": "No courses in this category", | ||||
|     "core.courses.noevents": "No upcoming activities due", | ||||
|     "core.courses.nosearchresults": "No results", | ||||
|     "core.courses.notenroled": "You are not enrolled in this course", | ||||
|     "core.courses.notenrollable": "You cannot enrol yourself in this course.", | ||||
|     "core.courses.password": "Enrolment key", | ||||
|     "core.courses.past": "Past", | ||||
|     "core.courses.paymentrequired": "This course requires a payment for entry.", | ||||
|     "core.courses.paypalaccepted": "PayPal payments accepted", | ||||
|     "core.courses.recentlyoverdue": "Recently overdue", | ||||
|     "core.courses.search": "Search", | ||||
|     "core.courses.searchcourses": "Search courses", | ||||
|     "core.courses.searchcoursesadvice": "You can use the search courses button to find courses to access as a  guest or enrol yourself in courses that allow it.", | ||||
|     "core.courses.selfenrolment": "Self enrolment", | ||||
|     "core.courses.sendpaymentbutton": "Send payment via PayPal", | ||||
|     "core.courses.sortbycourses": "Sort by courses", | ||||
|     "core.courses.sortbydates": "Sort by dates", | ||||
|     "core.courses.timeline": "Timeline", | ||||
|     "core.courses.totalcoursesearchresults": "Total courses: {{$a}}", | ||||
|     "core.currentdevice": "Current device", | ||||
|  | ||||
| @ -21,13 +21,11 @@ import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CorePipesModule } from '@pipes/pipes.module'; | ||||
| import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress'; | ||||
| import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item'; | ||||
| import { CoreCoursesOverviewEventsComponent } from '../components/overview-events/overview-events'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreCoursesCourseProgressComponent, | ||||
|         CoreCoursesCourseListItemComponent, | ||||
|         CoreCoursesOverviewEventsComponent | ||||
|         CoreCoursesCourseListItemComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
| @ -41,8 +39,7 @@ import { CoreCoursesOverviewEventsComponent } from '../components/overview-event | ||||
|     ], | ||||
|     exports: [ | ||||
|         CoreCoursesCourseProgressComponent, | ||||
|         CoreCoursesCourseListItemComponent, | ||||
|         CoreCoursesOverviewEventsComponent | ||||
|         CoreCoursesCourseListItemComponent | ||||
|     ] | ||||
| }) | ||||
| export class CoreCoursesComponentsModule {} | ||||
|  | ||||
| @ -16,17 +16,17 @@ import { NgModule } from '@angular/core'; | ||||
| import { CoreCoursesProvider } from './providers/courses'; | ||||
| import { CoreCoursesHelperProvider } from './providers/helper'; | ||||
| import { CoreCoursesMainMenuHandler } from './providers/mainmenu-handler'; | ||||
| import { CoreCoursesMyOverviewProvider } from './providers/my-overview'; | ||||
| import { CoreCoursesDashboardProvider } from './providers/dashboard'; | ||||
| import { CoreCoursesCourseLinkHandler } from './providers/course-link-handler'; | ||||
| import { CoreCoursesIndexLinkHandler } from './providers/courses-index-link-handler'; | ||||
| import { CoreCoursesMyOverviewLinkHandler } from './providers/my-overview-link-handler'; | ||||
| import { CoreCoursesDashboardLinkHandler } from './providers/dashboard-link-handler'; | ||||
| import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; | ||||
| import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; | ||||
| 
 | ||||
| // List of providers (without handlers).
 | ||||
| export const CORE_COURSES_PROVIDERS: any[] = [ | ||||
|     CoreCoursesProvider, | ||||
|     CoreCoursesMyOverviewProvider, | ||||
|     CoreCoursesDashboardProvider, | ||||
|     CoreCoursesHelperProvider | ||||
| ]; | ||||
| 
 | ||||
| @ -36,23 +36,23 @@ export const CORE_COURSES_PROVIDERS: any[] = [ | ||||
|     ], | ||||
|     providers: [ | ||||
|         CoreCoursesProvider, | ||||
|         CoreCoursesMyOverviewProvider, | ||||
|         CoreCoursesDashboardProvider, | ||||
|         CoreCoursesHelperProvider, | ||||
|         CoreCoursesMainMenuHandler, | ||||
|         CoreCoursesCourseLinkHandler, | ||||
|         CoreCoursesIndexLinkHandler, | ||||
|         CoreCoursesMyOverviewLinkHandler | ||||
|         CoreCoursesDashboardLinkHandler | ||||
|     ], | ||||
|     exports: [] | ||||
| }) | ||||
| export class CoreCoursesModule { | ||||
|     constructor(mainMenuDelegate: CoreMainMenuDelegate, contentLinksDelegate: CoreContentLinksDelegate, | ||||
|             mainMenuHandler: CoreCoursesMainMenuHandler, courseLinkHandler: CoreCoursesCourseLinkHandler, | ||||
|             indexLinkHandler: CoreCoursesIndexLinkHandler, myOverviewLinkHandler: CoreCoursesMyOverviewLinkHandler) { | ||||
|             indexLinkHandler: CoreCoursesIndexLinkHandler, dashboardLinkHandler: CoreCoursesDashboardLinkHandler) { | ||||
|         mainMenuDelegate.registerHandler(mainMenuHandler); | ||||
| 
 | ||||
|         contentLinksDelegate.registerHandler(courseLinkHandler); | ||||
|         contentLinksDelegate.registerHandler(indexLinkHandler); | ||||
|         contentLinksDelegate.registerHandler(myOverviewLinkHandler); | ||||
|         contentLinksDelegate.registerHandler(dashboardLinkHandler); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -14,34 +14,20 @@ | ||||
|     "errorselfenrol": "An error occurred while self enrolling.", | ||||
|     "filtermycourses": "Filter my courses", | ||||
|     "frontpage": "Front page", | ||||
|     "future": "Future", | ||||
|     "inprogress": "In progress", | ||||
|     "morecourses": "More courses", | ||||
|     "mycourses": "My courses", | ||||
|     "next30days": "Next 30 days", | ||||
|     "next7days": "Next 7 days", | ||||
|     "nocourses": "No course information to show.", | ||||
|     "nocoursesfuture": "No future courses", | ||||
|     "nocoursesinprogress": "No in progress courses", | ||||
|     "nocoursesoverview": "No courses", | ||||
|     "nocoursespast": "No past courses", | ||||
|     "nocoursesyet": "No courses in this category", | ||||
|     "noevents": "No upcoming activities due", | ||||
|     "nosearchresults": "No results", | ||||
|     "notenroled": "You are not enrolled in this course", | ||||
|     "notenrollable": "You cannot enrol yourself in this course.", | ||||
|     "password": "Enrolment key", | ||||
|     "past": "Past", | ||||
|     "paymentrequired": "This course requires a payment for entry.", | ||||
|     "paypalaccepted": "PayPal payments accepted", | ||||
|     "recentlyoverdue": "Recently overdue", | ||||
|     "search": "Search", | ||||
|     "searchcourses": "Search courses", | ||||
|     "searchcoursesadvice": "You can use the search courses button to find courses to access as a  guest or enrol yourself in courses that allow it.", | ||||
|     "selfenrolment": "Self enrolment", | ||||
|     "sendpaymentbutton": "Send payment via PayPal", | ||||
|     "sortbycourses": "Sort by courses", | ||||
|     "sortbydates": "Sort by dates", | ||||
|     "timeline": "Timeline", | ||||
|     "totalcoursesearchresults": "Total courses: {{$a}}" | ||||
| } | ||||
							
								
								
									
										50
									
								
								src/core/courses/pages/dashboard/dashboard.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/core/courses/pages/dashboard/dashboard.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title><core-format-text [text]="siteName"></core-format-text></ion-title> | ||||
| 
 | ||||
|         <ion-buttons end> | ||||
|             <button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate"> | ||||
|                 <ion-icon name="search"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-tabs [selectedIndex]="firstSelectedTab" [hideUntil]="tabsReady"> | ||||
|         <!-- Site home tab. --> | ||||
|         <core-tab [show]="siteHomeEnabled" [title]="'core.sitehome.sitehome' | translate" (ionSelect)="tabChanged('sitehome')"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <ion-refresher [enabled]="!!siteHomeComponent && siteHomeComponent.dataLoaded" (ionRefresh)="siteHomeComponent.doRefresh($event)"> | ||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|                     </ion-refresher> | ||||
|                     <core-sitehome-index></core-sitehome-index> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
| 
 | ||||
|         <!-- Courses tab. --> | ||||
|         <core-tab [title]="'core.courses.courses' | translate" (ionSelect)="tabChanged('courses')"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <ion-refresher [enabled]="!!blockMyOverview && blockMyOverview.loaded" (ionRefresh)="blockMyOverview.doRefresh($event)"> | ||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|                     </ion-refresher> | ||||
|                     <addon-block-myoverview></addon-block-myoverview> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
| 
 | ||||
|         <!-- Timeline tab. --> | ||||
|         <core-tab [title]="'core.courses.timeline' | translate" (ionSelect)="tabChanged('timeline')"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <ion-refresher [enabled]="!!blockTimeline && blockTimeline.loaded" (ionRefresh)="blockTimeline.doRefresh($event)"> | ||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|                     </ion-refresher> | ||||
|                     <addon-block-timeline></addon-block-timeline> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
|     </core-tabs> | ||||
| </ion-content> | ||||
| @ -15,23 +15,27 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreCoursesMyOverviewPage } from './my-overview'; | ||||
| import { CoreCoursesDashboardPage } from './dashboard'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreCoursesComponentsModule } from '../../components/components.module'; | ||||
| import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module'; | ||||
| import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module'; | ||||
| import { CoreSiteHomeComponentsModule } from '@core/sitehome/components/components.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreCoursesMyOverviewPage, | ||||
|         CoreCoursesDashboardPage, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CoreCoursesComponentsModule, | ||||
|         CoreSiteHomeComponentsModule, | ||||
|         IonicPageModule.forChild(CoreCoursesMyOverviewPage), | ||||
|         AddonBlockMyOverviewModule, | ||||
|         AddonBlockTimelineModule, | ||||
|         IonicPageModule.forChild(CoreCoursesDashboardPage), | ||||
|         TranslateModule.forChild() | ||||
|     ], | ||||
| }) | ||||
| export class CoreCoursesMyOverviewPageModule {} | ||||
| export class CoreCoursesDashboardPageModule {} | ||||
| @ -1,4 +1,4 @@ | ||||
| ion-app.app-root page-core-courses-my-overview { | ||||
| ion-app.app-root page-core-courses-dashboard { | ||||
|     ion-badge.core-course-download-courses-progress { | ||||
|         display: block; | ||||
|         @include float(start); | ||||
							
								
								
									
										125
									
								
								src/core/courses/pages/dashboard/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/core/courses/pages/dashboard/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | ||||
| // (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, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, NavController } from 'ionic-angular'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreCoursesProvider } from '../../providers/courses'; | ||||
| import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome'; | ||||
| import { AddonBlockMyOverviewComponent } from '@addon/block/myoverview/component/myoverview'; | ||||
| import { AddonBlockTimelineComponent } from '@addon/block/timeline/components/timeline/timeline'; | ||||
| import { CoreTabsComponent } from '@components/tabs/tabs'; | ||||
| import { CoreSiteHomeIndexComponent } from '@core/sitehome/components/index/index'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the dashboard. | ||||
|  */ | ||||
| @IonicPage({ segment: 'core-courses-dashboard' }) | ||||
| @Component({ | ||||
|     selector: 'page-core-courses-dashboard', | ||||
|     templateUrl: 'dashboard.html', | ||||
| }) | ||||
| export class CoreCoursesDashboardPage implements OnDestroy { | ||||
|     @ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent; | ||||
|     @ViewChild(CoreSiteHomeIndexComponent) siteHomeComponent: CoreSiteHomeIndexComponent; | ||||
|     @ViewChild(AddonBlockMyOverviewComponent) blockMyOverview: AddonBlockMyOverviewComponent; | ||||
|     @ViewChild(AddonBlockTimelineComponent) blockTimeline: AddonBlockTimelineComponent; | ||||
| 
 | ||||
|     firstSelectedTab: number; | ||||
|     siteHomeEnabled: boolean; | ||||
|     tabsReady = false; | ||||
|     tabShown = 'courses'; | ||||
|     searchEnabled: boolean; | ||||
|     tabs = []; | ||||
|     siteName: string; | ||||
| 
 | ||||
|     protected isDestroyed; | ||||
|     protected updateSiteObserver; | ||||
|     protected courseIds = ''; | ||||
| 
 | ||||
|     constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, | ||||
|             private sitesProvider: CoreSitesProvider, private siteHomeProvider: CoreSiteHomeProvider, | ||||
|             private eventsProvider: CoreEventsProvider) { | ||||
|         this.loadSiteName(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View loaded. | ||||
|      */ | ||||
|     ionViewDidLoad(): void { | ||||
|         this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); | ||||
| 
 | ||||
|         // Refresh the enabled flags if site is updated.
 | ||||
|         this.updateSiteObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { | ||||
|             this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); | ||||
|             this.loadSiteName(); | ||||
|         }); | ||||
| 
 | ||||
|         // Decide which tab to load first.
 | ||||
|         this.siteHomeProvider.isAvailable().then((enabled) => { | ||||
|             const site = this.sitesProvider.getCurrentSite(), | ||||
|                 displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; | ||||
| 
 | ||||
|             this.siteHomeEnabled = enabled; | ||||
|             this.firstSelectedTab = displaySiteHome ? 0 : 1; | ||||
|             this.tabsReady = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.tabsComponent && this.tabsComponent.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         this.tabsComponent && this.tabsComponent.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The tab has changed. | ||||
|      * | ||||
|      * @param {string} tab Name of the new tab. | ||||
|      */ | ||||
|     tabChanged(tab: string): void { | ||||
|         this.tabShown = tab; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to search courses. | ||||
|      */ | ||||
|     openSearch(): void { | ||||
|         this.navCtrl.push('CoreCoursesSearchPage'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the site name. | ||||
|      */ | ||||
|     protected loadSiteName(): void { | ||||
|         this.siteName = this.sitesProvider.getCurrentSite().getInfo().sitename; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.updateSiteObserver && this.updateSiteObserver.off(); | ||||
|     } | ||||
| } | ||||
| @ -1,109 +0,0 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title><core-format-text [text]="siteName"></core-format-text></ion-title> | ||||
| 
 | ||||
|         <ion-buttons end> | ||||
|             <button *ngIf="tabShown == 'courses' && courses[courses.selected] && courses[courses.selected].length > 5" ion-button icon-only [attr.aria-label]="'core.courses.filtermycourses' | translate" (click)="switchFilter()"> | ||||
|                 <ion-icon name="funnel"></ion-icon> | ||||
|             </button> | ||||
|             <button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate"> | ||||
|                 <ion-icon name="search"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-tabs [selectedIndex]="firstSelectedTab" [hideUntil]="tabsReady"> | ||||
|         <!-- Site home tab. --> | ||||
|         <core-tab [show]="siteHomeEnabled" [title]="'core.sitehome.sitehome' | translate" (ionSelect)="tabChanged('sitehome')"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <ion-refresher [enabled]="siteHomeComponent && siteHomeComponent.dataLoaded" (ionRefresh)="siteHomeComponent.doRefresh($event)"> | ||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|                     </ion-refresher> | ||||
|                     <core-sitehome-index></core-sitehome-index> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
| 
 | ||||
|         <!-- Courses tab. --> | ||||
|         <core-tab [title]="'core.courses.courses' | translate" (ionSelect)="tabChanged('courses')"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <ion-refresher [enabled]="timeline.loaded || timelineCourses.loaded || courses.loaded" (ionRefresh)="refreshMyOverview($event)"> | ||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|                     </ion-refresher> | ||||
| 
 | ||||
|                     <core-loading [hideUntil]="courses.loaded" class="core-loading-center"> | ||||
|                         <!-- "Time" selector. --> | ||||
|                         <div padding class="clearfix" [hidden]="showFilter" ion-row justify-content-between> | ||||
|                             <ion-select float-start [title]="'core.show' | translate" [(ngModel)]="courses.selected" ion-col (ngModelChange)="selectedChanged()" interface="popover" class="core-button-select"> | ||||
|                                 <ion-option value="inprogress">{{ 'core.courses.inprogress' | translate }}</ion-option> | ||||
|                                 <ion-option value="future">{{ 'core.courses.future' | translate }}</ion-option> | ||||
|                                 <ion-option value="past">{{ 'core.courses.past' | translate }}</ion-option> | ||||
|                             </ion-select> | ||||
|                             <!-- Download all courses. --> | ||||
|                             <div *ngIf="downloadAllCoursesEnabled && courses[courses.selected] && courses[courses.selected].length > 1" class="core-button-spinner"> | ||||
|                                 <button *ngIf="prefetchCoursesData[courses.selected].icon && prefetchCoursesData[courses.selected].icon != 'spinner'" ion-button icon-only clear color="dark" (click)="prefetchCourses()"> | ||||
|                                     <core-icon [name]="prefetchCoursesData[courses.selected].icon"></core-icon> | ||||
|                                 </button> | ||||
|                                 <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[courses.selected].badge">{{prefetchCoursesData[courses.selected].badge}}</ion-badge> | ||||
|                                 <ion-spinner *ngIf="!prefetchCoursesData[courses.selected].icon || prefetchCoursesData[courses.selected].icon == 'spinner'"></ion-spinner> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'inprogress'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesinprogress' | translate"></core-empty-box> | ||||
|                         <core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'future'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesfuture' | translate"></core-empty-box> | ||||
|                         <core-empty-box *ngIf="courses[courses.selected].length == 0 && courses.selected == 'past'" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursespast' | translate"></core-empty-box> | ||||
| 
 | ||||
|                         <!-- Filter courses. --> | ||||
|                         <ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate"> | ||||
|                         </ion-searchbar> | ||||
|                         <!-- List of courses. --> | ||||
|                         <div> | ||||
|                             <ion-grid no-padding> | ||||
|                                 <ion-row no-padding> | ||||
|                                     <ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch> | ||||
|                                         <core-courses-course-progress [course]="course" class="core-courseoverview"></core-courses-course-progress> | ||||
|                                     </ion-col> | ||||
|                                 </ion-row> | ||||
|                             </ion-grid> | ||||
|                         </div> | ||||
|                     </core-loading> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
| 
 | ||||
|         <!-- Timeline tab. --> | ||||
|         <core-tab [title]="'core.courses.timeline' | translate" (ionSelect)="tabChanged('timeline')"> | ||||
|             <ng-template> | ||||
|                 <ion-content> | ||||
|                     <ion-refresher [enabled]="timeline.loaded || timelineCourses.loaded || courses.loaded" (ionRefresh)="refreshMyOverview($event)"> | ||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|                     </ion-refresher> | ||||
| 
 | ||||
|                     <div padding [hidden]="!(timeline.loaded || timelineCourses.loaded)"> | ||||
|                         <ion-select [(ngModel)]="timeline.sort" (ngModelChange)="switchSort()" interface="popover" class="core-button-select"> | ||||
|                             <ion-option value="sortbydates">{{ 'core.courses.sortbydates' | translate }}</ion-option> | ||||
|                             <ion-option value="sortbycourses">{{ 'core.courses.sortbycourses' | translate }}</ion-option> | ||||
|                         </ion-select> | ||||
|                     </div> | ||||
|                     <core-loading [hideUntil]="timeline.loaded" [hidden]="timeline.sort != 'sortbydates'" class="core-loading-center"> | ||||
|                         <core-courses-overview-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMoreTimeline()"></core-courses-overview-events> | ||||
|                     </core-loading> | ||||
|                     <core-loading [hideUntil]="timelineCourses.loaded" [hidden]="timeline.sort != 'sortbycourses'" class="core-loading-center"> | ||||
|                         <ion-grid no-padding> | ||||
|                             <ion-row no-padding> | ||||
|                                 <ion-col *ngFor="let course of timelineCourses.courses" no-padding col-12 col-md-6> | ||||
|                                     <core-courses-course-progress [course]="course"> | ||||
|                                         <core-courses-overview-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMoreCourse(course)"></core-courses-overview-events> | ||||
|                                     </core-courses-course-progress> | ||||
|                                 </ion-col> | ||||
|                             </ion-row> | ||||
|                         </ion-grid> | ||||
|                         <core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" [message]="'core.courses.nocoursesoverview' | translate"></core-empty-box> | ||||
|                     </core-loading> | ||||
|                 </ion-content> | ||||
|             </ng-template> | ||||
|         </core-tab> | ||||
|     </core-tabs> | ||||
| </ion-content> | ||||
| @ -1,511 +0,0 @@ | ||||
| // (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, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, Searchbar, NavController } from 'ionic-angular'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreCoursesProvider } from '../../providers/courses'; | ||||
| import { CoreCoursesHelperProvider } from '../../providers/helper'; | ||||
| import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview'; | ||||
| import { CoreCourseHelperProvider } from '@core/course/providers/helper'; | ||||
| import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; | ||||
| import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; | ||||
| import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome'; | ||||
| import * as moment from 'moment'; | ||||
| import { CoreTabsComponent } from '@components/tabs/tabs'; | ||||
| import { CoreSiteHomeIndexComponent } from '@core/sitehome/components/index/index'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays My Overview. | ||||
|  */ | ||||
| @IonicPage({ segment: 'core-courses-my-overview' }) | ||||
| @Component({ | ||||
|     selector: 'page-core-courses-my-overview', | ||||
|     templateUrl: 'my-overview.html', | ||||
| }) | ||||
| export class CoreCoursesMyOverviewPage implements OnDestroy { | ||||
|     @ViewChild(CoreTabsComponent) tabsComponent: CoreTabsComponent; | ||||
|     @ViewChild('searchbar') searchbar: Searchbar; | ||||
|     @ViewChild(CoreSiteHomeIndexComponent) siteHomeComponent: CoreSiteHomeIndexComponent; | ||||
| 
 | ||||
|     firstSelectedTab: number; | ||||
|     siteHomeEnabled: boolean; | ||||
|     tabsReady = false; | ||||
|     tabShown = 'courses'; | ||||
|     timeline = { | ||||
|         sort: 'sortbydates', | ||||
|         events: [], | ||||
|         loaded: false, | ||||
|         canLoadMore: undefined | ||||
|     }; | ||||
|     timelineCourses = { | ||||
|         courses: [], | ||||
|         loaded: false, | ||||
|         canLoadMore: false | ||||
|     }; | ||||
|     courses = { | ||||
|         selected: 'inprogress', | ||||
|         loaded: false, | ||||
|         filter: '', | ||||
|         past: [], | ||||
|         inprogress: [], | ||||
|         future: [] | ||||
|     }; | ||||
|     showFilter = false; | ||||
|     searchEnabled: boolean; | ||||
|     filteredCourses: any[]; | ||||
|     tabs = []; | ||||
|     prefetchCoursesData = { | ||||
|         inprogress: {}, | ||||
|         past: {}, | ||||
|         future: {} | ||||
|     }; | ||||
|     downloadAllCoursesEnabled: boolean; | ||||
|     siteName: string; | ||||
| 
 | ||||
|     protected prefetchIconsInitialized = false; | ||||
|     protected isDestroyed; | ||||
|     protected updateSiteObserver; | ||||
|     protected courseIds = ''; | ||||
| 
 | ||||
|     constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider, | ||||
|             private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider, | ||||
|             private courseHelper: CoreCourseHelperProvider, private sitesProvider: CoreSitesProvider, | ||||
|             private siteHomeProvider: CoreSiteHomeProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, | ||||
|             private eventsProvider: CoreEventsProvider, private coursesHelper: CoreCoursesHelperProvider, | ||||
|             private utils: CoreUtilsProvider, private courseCompletionProvider: AddonCourseCompletionProvider) { | ||||
|         this.loadSiteName(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View loaded. | ||||
|      */ | ||||
|     ionViewDidLoad(): void { | ||||
|         this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); | ||||
|         this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|         // Refresh the enabled flags if site is updated.
 | ||||
|         this.updateSiteObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { | ||||
|             const wasEnabled = this.downloadAllCoursesEnabled; | ||||
| 
 | ||||
|             this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite(); | ||||
|             this.downloadAllCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|             if (!wasEnabled && this.downloadAllCoursesEnabled && this.courses.loaded) { | ||||
|                 // Download all courses is enabled now, initialize it.
 | ||||
|                 this.initPrefetchCoursesIcons(); | ||||
|             } | ||||
| 
 | ||||
|             this.loadSiteName(); | ||||
|         }); | ||||
| 
 | ||||
|         // Decide which tab to load first.
 | ||||
|         this.siteHomeProvider.isAvailable().then((enabled) => { | ||||
|             const site = this.sitesProvider.getCurrentSite(), | ||||
|                 displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; | ||||
| 
 | ||||
|             this.siteHomeEnabled = enabled; | ||||
|             this.firstSelectedTab = displaySiteHome ? 0 : 1; | ||||
|             this.tabsReady = true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User entered the page. | ||||
|      */ | ||||
|     ionViewDidEnter(): void { | ||||
|         this.tabsComponent && this.tabsComponent.ionViewDidEnter(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * User left the page. | ||||
|      */ | ||||
|     ionViewDidLeave(): void { | ||||
|         this.tabsComponent && this.tabsComponent.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the timeline. | ||||
|      * | ||||
|      * @param {number} [afterEventId] The last event id. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMyOverviewTimeline(afterEventId?: number): Promise<any> { | ||||
|         return this.myOverviewProvider.getActionEventsByTimesort(afterEventId).then((events) => { | ||||
|             this.timeline.events = events.events; | ||||
|             this.timeline.canLoadMore = events.canLoadMore; | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the timeline by courses. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMyOverviewTimelineByCourses(): Promise<any> { | ||||
|         return this.fetchUserCourses().then((courses) => { | ||||
|             const today = moment().unix(); | ||||
|             let courseIds; | ||||
|             courses = courses.filter((course) => { | ||||
|                 return course.startdate <= today && (!course.enddate || course.enddate >= today); | ||||
|             }); | ||||
| 
 | ||||
|             this.timelineCourses.courses = courses; | ||||
|             if (courses.length > 0) { | ||||
|                 courseIds = courses.map((course) => { | ||||
|                     return course.id; | ||||
|                 }); | ||||
| 
 | ||||
|                 return this.myOverviewProvider.getActionEventsByCourses(courseIds).then((courseEvents) => { | ||||
|                     this.timelineCourses.courses.forEach((course) => { | ||||
|                         course.events = courseEvents[course.id].events; | ||||
|                         course.canLoadMore = courseEvents[course.id].canLoadMore; | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the courses for my overview. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchMyOverviewCourses(): Promise<any> { | ||||
|         return this.fetchUserCourses().then((courses) => { | ||||
|             // Fetch course completion status.
 | ||||
|             return Promise.all(courses.map((course) => { | ||||
|                 if (typeof course.enablecompletion != 'undefined' && course.enablecompletion == 0) { | ||||
|                     // Completion is disabled for this course, there is no need to fetch the completion status.
 | ||||
|                     return Promise.resolve(course); | ||||
|                 } | ||||
| 
 | ||||
|                 return this.courseCompletionProvider.getCompletion(course.id).catch(() => { | ||||
|                     // Ignore error, maybe course compleiton is disabled or user ha no permission.
 | ||||
|                 }).then((completion) => { | ||||
|                     course.completed = completion && completion.completed; | ||||
| 
 | ||||
|                     return course; | ||||
|                 }); | ||||
|             })); | ||||
|         }).then((courses) => { | ||||
|             const today = moment().unix(); | ||||
| 
 | ||||
|             this.courses.past = []; | ||||
|             this.courses.inprogress = []; | ||||
|             this.courses.future = []; | ||||
| 
 | ||||
|             courses.forEach((course) => { | ||||
|                 if ((course.enddate && course.enddate < today) || course.completed) { | ||||
|                     // Courses that have already ended.
 | ||||
|                     this.courses.past.push(course); | ||||
|                 } else if (course.startdate > today) { | ||||
|                     // Courses that have not started yet.
 | ||||
|                     this.courses.future.push(course); | ||||
|                 } else { | ||||
|                     // Courses still in progress.
 | ||||
|                     this.courses.inprogress.push(course); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             this.courses.filter = ''; | ||||
|             this.showFilter = false; | ||||
|             this.filteredCourses = this.courses[this.courses.selected]; | ||||
| 
 | ||||
|             this.initPrefetchCoursesIcons(); | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.'); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch user courses. | ||||
|      * | ||||
|      * @return {Promise<any[]>} Promise resolved when done. | ||||
|      */ | ||||
|     protected fetchUserCourses(): Promise<any[]> { | ||||
|         return this.coursesProvider.getUserCourses().then((courses) => { | ||||
|             const promises = [], | ||||
|                 courseIds = courses.map((course) => { | ||||
|                 return course.id; | ||||
|             }); | ||||
| 
 | ||||
|             if (this.coursesProvider.canGetAdminAndNavOptions()) { | ||||
|                 // Load course options of the course.
 | ||||
|                 promises.push(this.coursesProvider.getCoursesAdminAndNavOptions(courseIds).then((options) => { | ||||
|                     courses.forEach((course) => { | ||||
|                         course.navOptions = options.navOptions[course.id]; | ||||
|                         course.admOptions = options.admOptions[course.id]; | ||||
|                     }); | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             this.courseIds = courseIds.join(','); | ||||
| 
 | ||||
|             promises.push(this.coursesHelper.loadCoursesExtraInfo(courses)); | ||||
| 
 | ||||
|             return Promise.all(promises).then(() => { | ||||
|                 return courses.sort((a, b) => { | ||||
|                     const compareA = a.fullname.toLowerCase(), | ||||
|                         compareB = b.fullname.toLowerCase(); | ||||
| 
 | ||||
|                     return compareA.localeCompare(compareB); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show or hide the filter. | ||||
|      */ | ||||
|     switchFilter(): void { | ||||
|         this.showFilter = !this.showFilter; | ||||
|         this.courses.filter = ''; | ||||
|         this.filteredCourses = this.courses[this.courses.selected]; | ||||
|         if (this.showFilter) { | ||||
|             setTimeout(() => { | ||||
|                 this.searchbar.setFocus(); | ||||
|             }, 500); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The filter has changed. | ||||
|      * | ||||
|      * @param {any} Received Event. | ||||
|      */ | ||||
|     filterChanged(event: any): void { | ||||
|         const newValue = event.target.value && event.target.value.trim().toLowerCase(); | ||||
|         if (!newValue || !this.courses[this.courses.selected]) { | ||||
|             this.filteredCourses = this.courses[this.courses.selected]; | ||||
|         } else { | ||||
|             this.filteredCourses = this.courses[this.courses.selected].filter((course) => { | ||||
|                 return course.fullname.toLowerCase().indexOf(newValue) > -1; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the data. | ||||
|      * | ||||
|      * @param {any} refresher Refresher. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     refreshMyOverview(refresher: any): Promise<any> { | ||||
|         const promises = []; | ||||
| 
 | ||||
|         if (this.tabShown == 'timeline') { | ||||
|             promises.push(this.myOverviewProvider.invalidateActionEventsByTimesort()); | ||||
|             promises.push(this.myOverviewProvider.invalidateActionEventsByCourses()); | ||||
|         } | ||||
| 
 | ||||
|         promises.push(this.coursesProvider.invalidateUserCourses().finally(() => { | ||||
|             // Invalidate course completion data.
 | ||||
|             return this.coursesProvider.getUserCourses().then((courses) => { | ||||
|                 return this.utils.allPromises(courses.map((course) => { | ||||
|                     return this.courseCompletionProvider.invalidateCourseCompletion(course.id); | ||||
|                  })); | ||||
|             }); | ||||
|         })); | ||||
| 
 | ||||
|         promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); | ||||
|         if (this.courseIds) { | ||||
|             promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds)); | ||||
|         } | ||||
| 
 | ||||
|         return this.utils.allPromises(promises).finally(() => { | ||||
|             switch (this.tabShown) { | ||||
|                 case 'timeline': | ||||
|                     switch (this.timeline.sort) { | ||||
|                         case 'sortbydates': | ||||
|                             return this.fetchMyOverviewTimeline(); | ||||
|                         case 'sortbycourses': | ||||
|                             return this.fetchMyOverviewTimelineByCourses(); | ||||
|                         default: | ||||
|                     } | ||||
|                     break; | ||||
|                 case 'courses': | ||||
|                     this.prefetchIconsInitialized = false; | ||||
| 
 | ||||
|                     return this.fetchMyOverviewCourses(); | ||||
|                 default: | ||||
|             } | ||||
|         }).finally(() => { | ||||
|             refresher.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Change timeline sort being viewed. | ||||
|      */ | ||||
|     switchSort(): void { | ||||
|         switch (this.timeline.sort) { | ||||
|             case 'sortbydates': | ||||
|                 if (!this.timeline.loaded) { | ||||
|                     this.fetchMyOverviewTimeline().finally(() => { | ||||
|                         this.timeline.loaded = true; | ||||
|                     }); | ||||
|                 } | ||||
|                 break; | ||||
|             case 'sortbycourses': | ||||
|                 if (!this.timelineCourses.loaded) { | ||||
|                     this.fetchMyOverviewTimelineByCourses().finally(() => { | ||||
|                         this.timelineCourses.loaded = true; | ||||
|                     }); | ||||
|                 } | ||||
|                 break; | ||||
|             default: | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The tab has changed. | ||||
|      * | ||||
|      * @param {string} tab Name of the new tab. | ||||
|      */ | ||||
|     tabChanged(tab: string): void { | ||||
|         this.tabShown = tab; | ||||
|         switch (this.tabShown) { | ||||
|             case 'timeline': | ||||
|                 if (!this.timeline.loaded) { | ||||
|                     this.fetchMyOverviewTimeline().finally(() => { | ||||
|                         this.timeline.loaded = true; | ||||
|                     }); | ||||
|                 } | ||||
|                 break; | ||||
|             case 'courses': | ||||
|                 if (!this.courses.loaded) { | ||||
|                     this.fetchMyOverviewCourses().finally(() => { | ||||
|                         this.courses.loaded = true; | ||||
|                     }); | ||||
|                 } | ||||
|                 break; | ||||
|             default: | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more events. | ||||
|      */ | ||||
|     loadMoreTimeline(): Promise<any> { | ||||
|         return this.fetchMyOverviewTimeline(this.timeline.canLoadMore); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more events. | ||||
|      * | ||||
|      * @param {any} course Course. | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     loadMoreCourse(course: any): Promise<any> { | ||||
|         return this.myOverviewProvider.getActionEventsByCourse(course.id, course.canLoadMore).then((courseEvents) => { | ||||
|             course.events = course.events.concat(courseEvents.events); | ||||
|             course.canLoadMore = courseEvents.canLoadMore; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Go to search courses. | ||||
|      */ | ||||
|     openSearch(): void { | ||||
|         this.navCtrl.push('CoreCoursesSearchPage'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The selected courses have changed. | ||||
|      */ | ||||
|     selectedChanged(): void { | ||||
|         this.filteredCourses = this.courses[this.courses.selected]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch all the shown courses. | ||||
|      * | ||||
|      * @return {Promise<any>} Promise resolved when done. | ||||
|      */ | ||||
|     prefetchCourses(): Promise<any> { | ||||
|         const selected = this.courses.selected, | ||||
|             selectedData = this.prefetchCoursesData[selected], | ||||
|             initialIcon = selectedData.icon; | ||||
| 
 | ||||
|         selectedData.icon = 'spinner'; | ||||
|         selectedData.badge = ''; | ||||
| 
 | ||||
|         return this.courseHelper.confirmAndPrefetchCourses(this.courses[selected], (progress) => { | ||||
|             selectedData.badge = progress.count + ' / ' + progress.total; | ||||
|         }).then(() => { | ||||
|             selectedData.icon = 'refresh'; | ||||
|         }).catch((error) => { | ||||
|             if (!this.isDestroyed) { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); | ||||
|                 selectedData.icon = initialIcon; | ||||
|             } | ||||
|         }).finally(() => { | ||||
|             selectedData.badge = ''; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the prefetch icon for selected courses. | ||||
|      */ | ||||
|     protected initPrefetchCoursesIcons(): void { | ||||
|         if (this.prefetchIconsInitialized || !this.downloadAllCoursesEnabled) { | ||||
|             // Already initialized.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.prefetchIconsInitialized = true; | ||||
| 
 | ||||
|         Object.keys(this.prefetchCoursesData).forEach((filter) => { | ||||
|             if (!this.courses[filter] || this.courses[filter].length < 2) { | ||||
|                 // Not enough courses.
 | ||||
|                 this.prefetchCoursesData[filter].icon = ''; | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.courseHelper.determineCoursesStatus(this.courses[filter]).then((status) => { | ||||
|                 let icon = this.courseHelper.getCourseStatusIconAndTitleFromStatus(status).icon; | ||||
|                 if (icon == 'spinner') { | ||||
|                     // It seems all courses are being downloaded, show a download button instead.
 | ||||
|                     icon = 'cloud-download'; | ||||
|                 } | ||||
|                 this.prefetchCoursesData[filter].icon = icon; | ||||
|             }); | ||||
| 
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the site name. | ||||
|      */ | ||||
|     protected loadSiteName(): void { | ||||
|         this.siteName = this.sitesProvider.getCurrentSite().getInfo().sitename; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.updateSiteObserver && this.updateSiteObserver.off(); | ||||
|     } | ||||
| } | ||||
| @ -21,7 +21,7 @@ import { CoreLoginHelperProvider } from '@core/login/providers/helper'; | ||||
|  * Handler to treat links to my overview. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreCoursesMyOverviewLinkHandler extends CoreContentLinksHandlerBase { | ||||
| export class CoreCoursesDashboardLinkHandler extends CoreContentLinksHandlerBase { | ||||
|     name = 'CoreCoursesMyOverviewLinkHandler'; | ||||
|     featureName = 'CoreMainMenuDelegate_CoreCourses'; | ||||
|     pattern = /\/my\/?$/; | ||||
| @ -44,7 +44,7 @@ export class CoreCoursesMyOverviewLinkHandler extends CoreContentLinksHandlerBas | ||||
|         return [{ | ||||
|             action: (siteId, navCtrl?): void => { | ||||
|                 // Always use redirect to make it the new history root (to avoid "loops" in history).
 | ||||
|                 this.loginHelper.redirect('CoreCoursesMyOverviewPage', undefined, siteId); | ||||
|                 this.loginHelper.redirect('CoreCoursesDashboardPage', undefined, siteId); | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
							
								
								
									
										64
									
								
								src/core/courses/providers/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/core/courses/providers/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| // (C) Copyright 2015 Martin Dougiamas
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreSite } from '@classes/site'; | ||||
| import { AddonBlockTimelineProvider } from '@addon/block/timeline/providers/timeline'; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding course overview. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreCoursesDashboardProvider { | ||||
| 
 | ||||
|     constructor(private sitesProvider: CoreSitesProvider, private timelineProvider: AddonBlockTimelineProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns whether or not My Overview is available for a certain site. | ||||
|      * | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<boolean>} Promise resolved with true if available, resolved with false or rejected otherwise. | ||||
|      */ | ||||
|     isAvailable(siteId?: string): Promise<boolean> { | ||||
|         return this.timelineProvider.isAvailable(siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if My Overview is disabled in a certain site. | ||||
|      * | ||||
|      * @param {CoreSite} [site] Site. If not defined, use current site. | ||||
|      * @return {boolean} Whether it's disabled. | ||||
|      */ | ||||
|     isDisabledInSite(site?: CoreSite): boolean { | ||||
|         site = site || this.sitesProvider.getCurrentSite(); | ||||
| 
 | ||||
|         return site.isFeatureDisabled('CoreMainMenuDelegate_CoreCourses'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if My Overview is available and not disabled. | ||||
|      * | ||||
|      * @return {Promise<boolean>} Promise resolved with true if enabled, resolved with false otherwise. | ||||
|      */ | ||||
|     isEnabled(): Promise<boolean> { | ||||
|         if (!this.isDisabledInSite()) { | ||||
|             return this.isAvailable().catch(() => { | ||||
|                 return false; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(false); | ||||
|     } | ||||
| } | ||||
| @ -72,4 +72,39 @@ export class CoreCoursesHelperProvider { | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get user courses with admin and nav options. | ||||
|      * | ||||
|      * @return {Promise<any[]>} Promise resolved when done. | ||||
|      */ | ||||
|     getUserCoursesWithOptions(): Promise<any[]> { | ||||
|         return this.coursesProvider.getUserCourses().then((courses) => { | ||||
|             const promises = [], | ||||
|                 courseIds = courses.map((course) => { | ||||
|                     return course.id; | ||||
|                 }); | ||||
| 
 | ||||
|             if (this.coursesProvider.canGetAdminAndNavOptions()) { | ||||
|                 // Load course options of the course.
 | ||||
|                 promises.push(this.coursesProvider.getCoursesAdminAndNavOptions(courseIds).then((options) => { | ||||
|                     courses.forEach((course) => { | ||||
|                         course.navOptions = options.navOptions[course.id]; | ||||
|                         course.admOptions = options.admOptions[course.id]; | ||||
|                     }); | ||||
|                 })); | ||||
|             } | ||||
| 
 | ||||
|             promises.push(this.loadCoursesExtraInfo(courses)); | ||||
| 
 | ||||
|             return Promise.all(promises).then(() => { | ||||
|                 return courses.sort((a, b) => { | ||||
|                     const compareA = a.fullname.toLowerCase(), | ||||
|                         compareB = b.fullname.toLowerCase(); | ||||
| 
 | ||||
|                     return compareA.localeCompare(compareB); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCoursesProvider } from './courses'; | ||||
| import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; | ||||
| import { CoreCoursesMyOverviewProvider } from '../providers/my-overview'; | ||||
| import { CoreCoursesDashboardProvider } from '../providers/dashboard'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to add My Courses or My Overview into main menu. | ||||
| @ -24,9 +24,9 @@ import { CoreCoursesMyOverviewProvider } from '../providers/my-overview'; | ||||
| export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler { | ||||
|     name = 'CoreCourses'; | ||||
|     priority = 1100; | ||||
|     isOverviewEnabled: boolean; | ||||
|     isDashboardEnabled: boolean; | ||||
| 
 | ||||
|     constructor(private coursesProvider: CoreCoursesProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) { } | ||||
|     constructor(private coursesProvider: CoreCoursesProvider, private dashboardProvider: CoreCoursesDashboardProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
| @ -35,8 +35,8 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler { | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         // Check if my overview is enabled.
 | ||||
|         return this.myOverviewProvider.isEnabled().then((enabled) => { | ||||
|             this.isOverviewEnabled = enabled; | ||||
|         return this.dashboardProvider.isEnabled().then((enabled) => { | ||||
|             this.isDashboardEnabled = enabled; | ||||
|             if (enabled) { | ||||
|                 return true; | ||||
|             } | ||||
| @ -52,11 +52,11 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler { | ||||
|      * @return {CoreMainMenuHandlerData} Data needed to render the handler. | ||||
|      */ | ||||
|     getDisplayData(): CoreMainMenuHandlerData { | ||||
|         if (this.isOverviewEnabled) { | ||||
|         if (this.isDashboardEnabled) { | ||||
|             return { | ||||
|                 icon: 'home', | ||||
|                 title: 'core.courses.courseoverview', | ||||
|                 page: 'CoreCoursesMyOverviewPage', | ||||
|                 page: 'CoreCoursesDashboardPage', | ||||
|                 class: 'core-courseoverview-handler' | ||||
|             }; | ||||
|         } else { | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSiteHomeProvider } from './sitehome'; | ||||
| import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate'; | ||||
| import { CoreCoursesMyOverviewProvider } from '@core/courses/providers/my-overview'; | ||||
| import { CoreCoursesDashboardProvider } from '@core/courses/providers/dashboard'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to add Site Home into main menu. | ||||
| @ -24,9 +24,8 @@ import { CoreCoursesMyOverviewProvider } from '@core/courses/providers/my-overvi | ||||
| export class CoreSiteHomeMainMenuHandler implements CoreMainMenuHandler { | ||||
|     name = 'CoreSiteHome'; | ||||
|     priority = 1200; | ||||
|     isOverviewEnabled: boolean; | ||||
| 
 | ||||
|     constructor(private siteHomeProvider: CoreSiteHomeProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) { } | ||||
|     constructor(private siteHomeProvider: CoreSiteHomeProvider, private dashboardProvider: CoreCoursesDashboardProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the handler is enabled on a site level. | ||||
| @ -35,7 +34,7 @@ export class CoreSiteHomeMainMenuHandler implements CoreMainMenuHandler { | ||||
|      */ | ||||
|     isEnabled(): boolean | Promise<boolean> { | ||||
|         // Check if my overview is enabled.
 | ||||
|         return this.myOverviewProvider.isEnabled().then((enabled) => { | ||||
|         return this.dashboardProvider.isEnabled().then((enabled) => { | ||||
|             if (enabled) { | ||||
|                 // My overview is enabled, Site Home will be inside the overview page.
 | ||||
|                 return false; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user