Merge pull request #3533 from alfonso-salces/MOBILE-4077
Mobile 4077 - Support user custom reports
This commit is contained in:
		
						commit
						c1370250f5
					
				| @ -1652,6 +1652,12 @@ | ||||
|   "core.courses.totalcoursesearchresults": "local_moodlemobileapp", | ||||
|   "core.currentdevice": "local_moodlemobileapp", | ||||
|   "core.custom": "form", | ||||
|   "core.reportbuilder.modifiedby": "tool_reportbuilder", | ||||
|   "core.reportbuilder.reportstab": "tool_reportbuilder", | ||||
|   "core.reportbuilder.reportsource": "tool_reportbuilder", | ||||
|   "core.reportbuilder.timecreated": "tool_reportbuilder", | ||||
|   "core.reportbuilder.showcolumns": "local_moodlemobileapp", | ||||
|   "core.reportbuilder.hidecolumns": "local_moodlemobileapp", | ||||
|   "core.datastoredoffline": "local_moodlemobileapp", | ||||
|   "core.date": "moodle", | ||||
|   "core.datecreated": "repository", | ||||
|  | ||||
| @ -43,6 +43,7 @@ import { CoreUserModule } from './user/user.module'; | ||||
| import { CoreUserToursModule } from './usertours/user-tours.module'; | ||||
| import { CoreViewerModule } from './viewer/viewer.module'; | ||||
| import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
| import { CoreReportBuilderModule } from './reportbuilder/reportbuilder.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
| @ -74,6 +75,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; | ||||
|         CoreUserToursModule, | ||||
|         CoreViewerModule, | ||||
|         CoreXAPIModule, | ||||
|         CoreReportBuilderModule, | ||||
| 
 | ||||
|         // Import last to allow overrides.
 | ||||
|         CoreEmulatorModule, | ||||
|  | ||||
							
								
								
									
										55
									
								
								src/core/features/reportbuilder/classes/reports-source.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/core/features/reportbuilder/classes/reports-source.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; | ||||
| import { CoreReportBuilder, CoreReportBuilderReport, REPORTS_LIST_LIMIT } from '../services/reportbuilder'; | ||||
| 
 | ||||
| /** | ||||
|  * Provides a list of reports. | ||||
|  */ | ||||
| export class CoreReportBuilderReportsSource extends CoreRoutedItemsManagerSource<CoreReportBuilderReport> { | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getItemPath(report: CoreReportBuilderReport): string { | ||||
|         return report.id.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async loadPageItems(page: number): Promise<{ items: CoreReportBuilderReport[]; hasMoreItems: boolean }> { | ||||
|         const reports = await CoreReportBuilder.getReports(page, this.getPageLength()); | ||||
| 
 | ||||
|         return { items: reports, hasMoreItems: reports.length > 0 }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected setItems(reports: CoreReportBuilderReport[], hasMoreItems: boolean): void { | ||||
|         const sortedReports = reports.slice(0); | ||||
|         reports.sort((a, b) => a.timecreated < b.timecreated ? 1 : -1); | ||||
|         super.setItems(sortedReports, hasMoreItems); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getPageLength(): number { | ||||
|         return REPORTS_LIST_LIMIT; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,36 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreReportBuilderReportColumnComponent } from './report-column/report-column'; | ||||
| import { CoreReportBuilderReportDetailComponent } from './report-detail/report-detail'; | ||||
| import { CoreReportBuilderReportSummaryComponent } from './report-summary/report-summary'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         CoreReportBuilderReportDetailComponent, | ||||
|         CoreReportBuilderReportColumnComponent, | ||||
|         CoreReportBuilderReportSummaryComponent, | ||||
|     ], | ||||
|     exports: [ | ||||
|         CoreReportBuilderReportDetailComponent, | ||||
|         CoreReportBuilderReportColumnComponent, | ||||
|         CoreReportBuilderReportSummaryComponent, | ||||
|     ], | ||||
| }) | ||||
| export class CoreReportBuilderComponentsModule {} | ||||
| @ -0,0 +1,11 @@ | ||||
| <ion-item class="ion-text-wrap" lines="inset" [detail]="false" [button]="isExpandable" [attr.aria-expanded]="isExpanded" | ||||
|     [attr.aria-controls]="'core-report-builder-column-' + rowIndex" | ||||
|     [attr.aria-label]="(isExpanded ? 'core.hidecolumns' : 'core.showcolumns') | translate" (click)="toggleRow()"> | ||||
|     <ion-label> | ||||
|         <h3 *ngIf="columnIndex !== 0 || (columnIndex === 0 && showFirstTitle)"> {{ header }} </h3> | ||||
|         <core-format-text [text]="column" contextLevel="site" [contextInstanceId]="contextId"></core-format-text> | ||||
|     </ion-label> | ||||
|     <ion-icon [class.expandable-status-icon-expanded]="!isExpanded" slot="end" aria-hidden="true" name="fas-chevron-up" | ||||
|         class="expandable-status-icon" *ngIf="isExpandable" flip-rtl> | ||||
|     </ion-icon> | ||||
| </ion-item> | ||||
| @ -0,0 +1,11 @@ | ||||
| @import "~theme/globals"; | ||||
| 
 | ||||
| :host { | ||||
|     --rotate-expandable: rotate(180deg); | ||||
| 
 | ||||
|     .expandable-status-icon { | ||||
|         font-size: var(--text-size); | ||||
|         @include margin-horizontal(0, 2px); | ||||
|         @include core-transition(transform, 200ms); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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, EventEmitter, Input, Output } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'core-report-builder-report-column', | ||||
|     templateUrl: './report-column.html', | ||||
|     styleUrls: ['./report-column.scss'], | ||||
| }) | ||||
| export class CoreReportBuilderReportColumnComponent { | ||||
| 
 | ||||
|     @Input() isExpanded = false; | ||||
|     @Input() isExpandable = false; | ||||
|     @Input() showFirstTitle = false; | ||||
|     @Input() columnIndex!: number; | ||||
|     @Input() rowIndex!: number; | ||||
|     @Input() column!: string; | ||||
|     @Input() contextId!: number; | ||||
|     @Input() header!: string; | ||||
|     @Output() onToggleRow: EventEmitter<number> = new EventEmitter(); | ||||
| 
 | ||||
|     /** | ||||
|      * Emits row click | ||||
|      */ | ||||
|     toggleRow(): void { | ||||
|         this.onToggleRow.emit(this.rowIndex); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,60 @@ | ||||
| <ng-container *ngIf="state$ | async as state"> | ||||
| 
 | ||||
|     <core-loading [hideUntil]="state.loaded"> | ||||
| 
 | ||||
|         <ng-container *ngIf="state.report?.data?.rows && state.report?.data?.headers && state.report?.details; else empty"> | ||||
| 
 | ||||
|             <ion-refresher slot="fixed" [disabled]="!state.loaded" (ionRefresh)="refreshReport($event.target)"> | ||||
|                 <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|             </ion-refresher> | ||||
| 
 | ||||
|             <ng-container *ngIf="isCardLayout"> | ||||
|                 <ion-card *ngFor="let row of state.report.data.rows; let rowIndex = index"> | ||||
|                     <ion-list class="ion-text-wrap"> | ||||
|                         <core-report-builder-report-column *ngFor="let column of row.columns | slice:0:row.isExpanded ? | ||||
|                             row.columns.length : state.cardVisibleColumns; let columnIndex = index" [columnIndex]="columnIndex" | ||||
|                             [rowIndex]="rowIndex" [isExpandable]="columnIndex === 0 && row.columns.length > state.cardVisibleColumns" | ||||
|                             [isExpanded]="row.isExpanded" [showFirstTitle]="state.cardviewShowFirstTitle" | ||||
|                             [contextId]="state.report.details.contextid" [header]="state.report.data.headers[columnIndex]" [column]="column" | ||||
|                             (onToggleRow)="toggleRow(rowIndex)"> | ||||
|                         </core-report-builder-report-column> | ||||
|                     </ion-list> | ||||
|                 </ion-card> | ||||
|             </ng-container> | ||||
| 
 | ||||
|             <ng-container *ngIf="!isCardLayout"> | ||||
|                 <table> | ||||
|                     <thead> | ||||
|                         <tr> | ||||
|                             <th *ngFor="let header of state.report.data.headers"> | ||||
|                                 {{ header }} | ||||
|                             </th> | ||||
|                         </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                         <tr *ngFor="let row of state.report.data.rows"> | ||||
|                             <td *ngFor="let column of row.columns"> | ||||
|                                 <core-format-text [text]="column" [contextLevel]="'site'" | ||||
|                                     [contextInstanceId]="state.report.details.contextid"> | ||||
|                                 </core-format-text> | ||||
|                             </td> | ||||
|                         </tr> | ||||
|                     </tbody> | ||||
|                 </table> | ||||
|             </ng-container> | ||||
| 
 | ||||
|         </ng-container> | ||||
| 
 | ||||
|         <ng-template #empty> | ||||
|             <core-empty-box *ngIf="!state.report?.data?.rows || !state.report?.data?.headers || !state.report?.details" icon="fa-list-alt" | ||||
|                 [message]="'core.course.nocontentavailable' | translate"></core-empty-box> | ||||
|         </ng-template> | ||||
| 
 | ||||
|         <core-infinite-loading *ngIf="!isBlock && state.report?.data?.rows && state.report?.data?.headers && state.report?.details" | ||||
|             [enabled]="state.canLoadMoreRows" (action)="fetchMoreInfo($event)" [error]="state.errorLoadingRows"> | ||||
|         </core-infinite-loading> | ||||
| 
 | ||||
| 
 | ||||
|     </core-loading> | ||||
| 
 | ||||
| </ng-container> | ||||
| @ -0,0 +1,44 @@ | ||||
| @import "~theme/globals"; | ||||
| 
 | ||||
| :host { | ||||
|     --header-background: var(--white); | ||||
|     --border-color: var(--stroke); | ||||
| 
 | ||||
|     .report-title { | ||||
|         ion-item { | ||||
|             width: 100%; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     table { | ||||
|         width: 98%; | ||||
|         margin: 1em auto; | ||||
|         border-collapse: collapse; | ||||
|         color: var(--ion-text-color); | ||||
|         overflow-x: auto; | ||||
|         display: block; | ||||
| 
 | ||||
|         tbody { | ||||
|             display: table; | ||||
|         } | ||||
| 
 | ||||
|         th { | ||||
|             background-color: var(--header-background); | ||||
|         } | ||||
| 
 | ||||
|         tr { | ||||
|             border-bottom: 1px solid var(--border-color); | ||||
| 
 | ||||
|             &:nth-child(even) { | ||||
|                 background: var(--light); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         th, td { | ||||
|             @include padding(8px, 8px, 8px, null); | ||||
|             text-align: start; | ||||
|             min-width: 200px; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,201 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { | ||||
|     CoreReportBuilder, | ||||
|     CoreReportBuilderReportDetail, | ||||
|     CoreReportBuilderRetrieveReportMapped, | ||||
|     REPORT_ROWS_LIMIT, | ||||
| } from '@features/reportbuilder/services/reportbuilder'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { BehaviorSubject } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'core-report-builder-report-detail', | ||||
|     templateUrl: './report-detail.html', | ||||
|     styleUrls: ['./report-detail.scss'], | ||||
|     changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class CoreReportBuilderReportDetailComponent implements OnInit { | ||||
| 
 | ||||
|     @Input() reportId!: string; | ||||
|     @Input() isBlock = true; | ||||
|     @Input() perPage?: number; | ||||
|     @Input() layout: 'card' | 'table' | 'adaptative' = 'adaptative'; | ||||
|     @Output() onReportLoaded = new EventEmitter<CoreReportBuilderReportDetail>(); | ||||
| 
 | ||||
|     get isCardLayout(): boolean { | ||||
|         return this.layout === 'card' || (CoreScreen.isMobile && this.layout === 'adaptative'); | ||||
|     } | ||||
| 
 | ||||
|     state$: Readonly<BehaviorSubject<CoreReportBuilderReportDetailState>> = new BehaviorSubject({ | ||||
|         report: null, | ||||
|         loaded: false, | ||||
|         canLoadMoreRows: true, | ||||
|         errorLoadingRows: false, | ||||
|         cardviewShowFirstTitle: false, | ||||
|         cardVisibleColumns: 1, | ||||
|         page: 0, | ||||
|     }); | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         await this.getReport(); | ||||
|         this.updateState({ loaded: true }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get report data. | ||||
|      */ | ||||
|     async getReport(): Promise<void> { | ||||
|         if (!this.reportId) { | ||||
|             CoreDomUtils.showErrorModal(new CoreError('No report found')); | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const { page } = this.state$.getValue(); | ||||
| 
 | ||||
|         const report = await CoreReportBuilder.loadReport(parseInt(this.reportId), page,this.perPage ?? REPORT_ROWS_LIMIT); | ||||
| 
 | ||||
|         if (!report) { | ||||
|             CoreDomUtils.showErrorModal(new CoreError('No report found')); | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await CoreReportBuilder.viewReport(this.reportId); | ||||
| 
 | ||||
|         this.updateState({ | ||||
|             report, | ||||
|             cardVisibleColumns: report.details.settingsdata.cardviewVisibleColumns, | ||||
|             cardviewShowFirstTitle: report.details.settingsdata.cardviewShowFirstTitle, | ||||
|         }); | ||||
| 
 | ||||
|         this.onReportLoaded.emit(report.details); | ||||
|     } | ||||
| 
 | ||||
|     updateState(state: Partial<CoreReportBuilderReportDetailState>): void { | ||||
|         const previousState = this.state$.getValue(); | ||||
|         this.state$.next({ ...previousState, ...state }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update report data. | ||||
|      * | ||||
|      * @param ionRefresher ionic refresher. | ||||
|      */ | ||||
|     async refreshReport(ionRefresher?: IonRefresher): Promise<void> { | ||||
|         await CoreUtils.ignoreErrors(CoreReportBuilder.invalidateReport()); | ||||
|         this.updateState({ page: 0, canLoadMoreRows: false }); | ||||
|         await CoreUtils.ignoreErrors(this.getReport()); | ||||
|         await ionRefresher?.complete(); | ||||
|         this.updateState({ canLoadMoreRows: true }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Increment page of report rows. | ||||
|      */ | ||||
|     protected incrementPage(): void { | ||||
|         const { page } = this.state$.getValue(); | ||||
|         this.updateState({ page: page + 1 }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load a new batch of pages. | ||||
|      * | ||||
|      * @param complete Completion callback. | ||||
|      */ | ||||
|     async fetchMoreInfo(complete: () => void): Promise<void> { | ||||
|         const { canLoadMoreRows, report } = this.state$.getValue(); | ||||
| 
 | ||||
|         if (!canLoadMoreRows) { | ||||
|             complete(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             this.incrementPage(); | ||||
| 
 | ||||
|             const { page: currentPage } = this.state$.getValue(); | ||||
| 
 | ||||
|             const newReport = await CoreReportBuilder.loadReport(parseInt(this.reportId), currentPage, REPORT_ROWS_LIMIT); | ||||
| 
 | ||||
|             if (!report || !newReport || newReport.data.rows.length === 0) { | ||||
|                 this.updateState({ canLoadMoreRows: false }); | ||||
|                 complete(); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.updateState({ | ||||
|                 report: { | ||||
|                     ...report, | ||||
|                     data: { | ||||
|                         ...report.data, | ||||
|                         rows: [ | ||||
|                             ...report.data.rows, | ||||
|                             ...newReport.data.rows, | ||||
|                         ], | ||||
|                     }, | ||||
|                 }, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading more reports'); | ||||
| 
 | ||||
|             this.updateState({ canLoadMoreRows: false }); | ||||
|             this.updateState({ errorLoadingRows: true }); | ||||
|         } | ||||
| 
 | ||||
|         complete(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Expand or close card. | ||||
|      * | ||||
|      * @param rowIndex card to expand or close. | ||||
|      */ | ||||
|     toggleRow(rowIndex: number): void { | ||||
|         const { report } = this.state$.getValue(); | ||||
| 
 | ||||
|         if (!report?.data?.rows[rowIndex]) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         report.data.rows[rowIndex].isExpanded = !report.data.rows[rowIndex].isExpanded; | ||||
|         this.updateState({ report }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type CoreReportBuilderReportDetailState = { | ||||
|     report: CoreReportBuilderRetrieveReportMapped | null; | ||||
|     loaded: boolean; | ||||
|     canLoadMoreRows: boolean; | ||||
|     errorLoadingRows: boolean; | ||||
|     cardviewShowFirstTitle: boolean; | ||||
|     cardVisibleColumns: number; | ||||
|     page: number; | ||||
| }; | ||||
| @ -0,0 +1,39 @@ | ||||
| <ion-header class="no-title"> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| 
 | ||||
| <ion-content> | ||||
|     <div class="list-item-limited-width"> | ||||
|         <ion-item class="ion-text-wrap course-name"> | ||||
|             <ion-label> | ||||
|                 <h1> | ||||
|                     <core-format-text [text]="reportDetail.name" contextLevel="report" [contextInstanceId]="reportDetail.id"> | ||||
|                     </core-format-text> | ||||
|                 </h1> | ||||
|             </ion-label> | ||||
|             <ion-button fill="clear" [href]="reportUrl" core-link [showBrowserWarning]="false" | ||||
|                 [attr.aria-label]="'core.openinbrowser' | translate" slot="end"> | ||||
|                 <ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <ion-item class="ion-text-wrap" [detail]="false" *ngFor="let item of reportDetailToDisplay"> | ||||
|             <ion-label> | ||||
|                 <p>{{ item.title | translate }}</p> | ||||
|                 <core-format-text [text]="item.text" contextLevel="report" [contextInstanceId]="reportDetail.id"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </div> | ||||
| </ion-content> | ||||
| @ -0,0 +1,60 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||||
| import { CoreReportBuilderReportDetail } from '@features/reportbuilder/services/reportbuilder'; | ||||
| import { CoreFormatDatePipe } from '@pipes/format-date'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { ModalController } from '@singletons'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'core-report-builder-report-summary', | ||||
|     templateUrl: './report-summary.html', | ||||
|     changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class CoreReportBuilderReportSummaryComponent implements OnInit { | ||||
| 
 | ||||
|     @Input() reportDetail!: CoreReportBuilderReportDetail; | ||||
|     reportUrl!: string; | ||||
|     reportDetailToDisplay!: { title: string; text: string }[]; | ||||
| 
 | ||||
|     ngOnInit(): void { | ||||
|         const formatDate = new CoreFormatDatePipe(); | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
|         this.reportUrl = `${site.getURL()}/reportbuilder/view.php?id=${this.reportDetail.id}`; | ||||
|         this.reportDetailToDisplay = [ | ||||
|             { | ||||
|                 title: 'core.reportbuilder.reportsource', | ||||
|                 text: this.reportDetail.sourcename, | ||||
|             }, | ||||
|             { | ||||
|                 title: 'core.reportbuilder.timecreated', | ||||
|                 text: formatDate.transform(this.reportDetail.timecreated * 1000), | ||||
|             }, | ||||
|             { | ||||
|                 title: 'addon.mod_data.timemodified', | ||||
|                 text: formatDate.transform(this.reportDetail.timemodified * 1000), | ||||
|             }, | ||||
|             { | ||||
|                 title: 'core.reportbuilder.modifiedby', | ||||
|                 text: this.reportDetail.modifiedby.fullname, | ||||
|             }, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     closeModal(): void { | ||||
|         ModalController.dismiss(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/core/features/reportbuilder/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/core/features/reportbuilder/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| { | ||||
|     "modifiedby": "Modified by", | ||||
|     "reportstab": "Reports", | ||||
|     "reportsource": "Report source", | ||||
|     "timecreated": "Time created", | ||||
|     "showcolumns": "Show columns", | ||||
|     "hidecolumns": "Hide columns" | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/core/features/reportbuilder/pages/list/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/core/features/reportbuilder/pages/list/list.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <h1>{{ 'core.reportbuilder.reportstab' | translate }}</h1> | ||||
|         </ion-title> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content *ngIf="state$ | async as state"> | ||||
|     <ion-refresher slot="fixed" [disabled]="!state.loaded" (ionRefresh)="refreshReports($event.target)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="state.loaded"> | ||||
|         <ion-list *ngIf="reports.items?.length; else empty"> | ||||
|             <ion-item [attr.aria-current]="reports.getItemAriaCurrent(report)" [detail]="true" class="ion-text-wrap" [button]="true" | ||||
|                 *ngFor="let report of reports.items" (click)="reports.select(report)"> | ||||
|                 <ion-label> | ||||
|                     <h3>{{ report.name }}</h3> | ||||
|                     <p>{{ report.sourcename }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ng-template #empty> | ||||
|             <core-empty-box *ngIf="!reports.items?.length" icon="fa-list-alt" [message]="'core.course.nocontentavailable' | translate"> | ||||
|             </core-empty-box> | ||||
|         </ng-template> | ||||
| 
 | ||||
|         <core-infinite-loading *ngIf="reports.items?.length" [enabled]="reports.loaded && !reports.completed" | ||||
|             (action)="fetchMoreReports($event)" [error]="state.loadMoreError"> | ||||
|         </core-infinite-loading> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
							
								
								
									
										128
									
								
								src/core/features/reportbuilder/pages/list/list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/core/features/reportbuilder/pages/list/list.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,128 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; | ||||
| import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; | ||||
| import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; | ||||
| import { CoreReportBuilderReportsSource } from '@features/reportbuilder/classes/reports-source'; | ||||
| import { CoreReportBuilder, CoreReportBuilderReport, REPORTS_LIST_LIMIT } from '@features/reportbuilder/services/reportbuilder'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { BehaviorSubject } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'core-report-builder-list', | ||||
|     templateUrl: './list.html', | ||||
|     changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     reports!: CoreListItemsManager<CoreReportBuilderReport, CoreReportBuilderReportsSource>; | ||||
| 
 | ||||
|     state$: Readonly<BehaviorSubject<CoreReportBuilderListState>> = new BehaviorSubject({ | ||||
|         page: 1, | ||||
|         perpage: REPORTS_LIST_LIMIT, | ||||
|         loaded: false, | ||||
|         loadMoreError: false, | ||||
|     }); | ||||
| 
 | ||||
|     constructor() { | ||||
|         try { | ||||
|             const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreReportBuilderReportsSource, []); | ||||
|             this.reports = new CoreListItemsManager(source, CoreReportBuilderListPage); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
|             CoreNavigator.back(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchReports(true); | ||||
|             this.updateState({ loaded: true }); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading reports'); | ||||
| 
 | ||||
|             this.reports.reset(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update reports list or loads it. | ||||
|      * | ||||
|      * @param reload is reoading or not. | ||||
|      */ | ||||
|     async fetchReports(reload: boolean): Promise<void> { | ||||
|         reload ? await this.reports.reload() : await this.reports.load(); | ||||
|         this.updateState({ loadMoreError: false }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Properties of the state to update. | ||||
|      * | ||||
|      * @param state Object to update. | ||||
|      */ | ||||
|     updateState(state: Partial<CoreReportBuilderListState>): void { | ||||
|         const previousState = this.state$.getValue(); | ||||
|         this.state$.next({ ...previousState, ...state }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load a new batch of Reports. | ||||
|      * | ||||
|      * @param complete Completion callback. | ||||
|      */ | ||||
|     async fetchMoreReports(complete: () => void): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchReports(false); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading more reports'); | ||||
| 
 | ||||
|             this.updateState({ loadMoreError: true }); | ||||
|         } | ||||
| 
 | ||||
|         complete(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh reports list. | ||||
|      * | ||||
|      * @param ionRefresher ionRefresher. | ||||
|      */ | ||||
|     async refreshReports(ionRefresher?: IonRefresher): Promise<void> { | ||||
|         await CoreUtils.ignoreErrors(CoreReportBuilder.invalidateReportsList()); | ||||
|         await CoreUtils.ignoreErrors(this.fetchReports(true)); | ||||
|         await ionRefresher?.complete(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.reports.destroy(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type CoreReportBuilderListState = { | ||||
|     page: number; | ||||
|     perpage: number; | ||||
|     loaded: boolean; | ||||
|     loadMoreError: boolean; | ||||
| }; | ||||
							
								
								
									
										21
									
								
								src/core/features/reportbuilder/pages/report/report.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/core/features/reportbuilder/pages/report/report.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
|         </ion-buttons> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="openInfo()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-info-circle" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|         <ion-title *ngIf="reportDetail"> | ||||
|             <h1> {{ reportDetail.name }} </h1> | ||||
|             <p class="subheading"> {{ reportDetail.sourcename }} </p> | ||||
|         </ion-title> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| 
 | ||||
| <ion-content> | ||||
|     <core-report-builder-report-detail [isBlock]="false" [reportId]="reportId" (onReportLoaded)="loadReportDetail($event)"> | ||||
|     </core-report-builder-report-detail> | ||||
| </ion-content> | ||||
							
								
								
									
										52
									
								
								src/core/features/reportbuilder/pages/report/report.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/core/features/reportbuilder/pages/report/report.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { CoreReportBuilderReportSummaryComponent } from '@features/reportbuilder/components/report-summary/report-summary'; | ||||
| import { CoreReportBuilderReportDetail } from '@features/reportbuilder/services/reportbuilder'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'core-report-builder-report', | ||||
|     templateUrl: './report.html', | ||||
| }) | ||||
| export class CoreReportBuilderReportPage implements OnInit { | ||||
| 
 | ||||
|     reportId!: string; | ||||
|     reportDetail?: CoreReportBuilderReportDetail; | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.reportId = CoreNavigator.getRequiredRouteParam('id'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the report detail | ||||
|      * | ||||
|      * @param reportDetail it contents the detail of the report. | ||||
|      */ | ||||
|     loadReportDetail(reportDetail: CoreReportBuilderReportDetail): void { | ||||
|         this.reportDetail = reportDetail; | ||||
|     } | ||||
| 
 | ||||
|     openInfo(): void { | ||||
|         CoreDomUtils.openSideModal<void>({ | ||||
|             component: CoreReportBuilderReportSummaryComponent, | ||||
|             componentProps: { reportDetail: this.reportDetail }, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/core/features/reportbuilder/reportbuilder-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/core/features/reportbuilder/reportbuilder-lazy.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CoreReportBuilderComponentsModule } from './components/components.module'; | ||||
| import { CoreReportBuilderListPage } from './pages/list/list'; | ||||
| import { CoreReportBuilderReportPage } from './pages/report/report'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: CoreReportBuilderListPage, | ||||
|     }, | ||||
|     { | ||||
|         path: ':id', | ||||
|         component: CoreReportBuilderReportPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreSharedModule, | ||||
|         CoreReportBuilderComponentsModule, | ||||
|         RouterModule.forChild(routes), | ||||
|     ], | ||||
|     declarations: [ | ||||
|         CoreReportBuilderListPage, | ||||
|         CoreReportBuilderReportPage, | ||||
|     ], | ||||
| }) | ||||
| export class CoreReportBuilderLazyModule {} | ||||
							
								
								
									
										39
									
								
								src/core/features/reportbuilder/reportbuilder.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/core/features/reportbuilder/reportbuilder.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CoreUserDelegate } from '@features/user/services/user-delegate'; | ||||
| import { CoreReportBuilderHandler, CoreReportBuilderHandlerService } from './services/handlers/reportbuilder'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: CoreReportBuilderHandlerService.PAGE_NAME, | ||||
|         loadChildren: () => import('./reportbuilder-lazy.module').then(m => m.CoreReportBuilderLazyModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [RouterModule.forChild(routes)], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             useValue: () => { | ||||
|                 CoreUserDelegate.registerHandler(CoreReportBuilderHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class CoreReportBuilderModule {} | ||||
| @ -0,0 +1,59 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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 { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreReportBuilder } from '../reportbuilder'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to visualize custom reports. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreReportBuilderHandlerService implements CoreUserProfileHandler { | ||||
| 
 | ||||
|     static readonly PAGE_NAME = 'reportbuilder'; | ||||
| 
 | ||||
|     type = CoreUserDelegateService.TYPE_NEW_PAGE; | ||||
|     cacheEnabled = true; | ||||
|     name = 'CoreReportBuilderDelegate'; | ||||
|     priority = 350; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return await CoreReportBuilder.isEnabled(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getDisplayData(): CoreUserProfileHandlerData { | ||||
|         return { | ||||
|             class: 'core-report-builder', | ||||
|             icon: 'fa-list-alt', | ||||
|             title: 'core.reportbuilder.reportstab', | ||||
|             action: async (event): Promise<void> => { | ||||
|                 event.preventDefault(); | ||||
|                 event.stopPropagation(); | ||||
|                 await CoreNavigator.navigate(`/${CoreReportBuilderHandlerService.PAGE_NAME}`); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreReportBuilderHandler = makeSingleton(CoreReportBuilderHandlerService); | ||||
							
								
								
									
										265
									
								
								src/core/features/reportbuilder/services/reportbuilder.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								src/core/features/reportbuilder/services/reportbuilder.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,265 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // 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.
 | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreSiteWSPreSets } from '@classes/site'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreWSExternalWarning } from '@services/ws'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'mmaReportBuilder:'; | ||||
| export const REPORTS_LIST_LIMIT = 20; | ||||
| export const REPORT_ROWS_LIMIT = 20; | ||||
| 
 | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreReportBuilderService { | ||||
| 
 | ||||
|     /** | ||||
|      * Obtain the reports list. | ||||
|      * | ||||
|      * @param page Current page. | ||||
|      * @param perpage Reports obtained per page. | ||||
|      * @returns Reports list. | ||||
|      */ | ||||
|     async getReports(page?: number, perpage?: number): Promise<CoreReportBuilderReport[]> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
|         const preSets: CoreSiteWSPreSets = { cacheKey: this.getReportBuilderCacheKey() }; | ||||
|         const response = await site.read<CoreReportBuilderListReportsWSResponse>( | ||||
|             'core_reportbuilder_list_reports', | ||||
|             { page, perpage }, | ||||
|             preSets, | ||||
|         ); | ||||
| 
 | ||||
|         return response.reports; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the detail of a report. | ||||
|      * | ||||
|      * @param reportid Report id | ||||
|      * @param page Current page. | ||||
|      * @param perpage Rows obtained per page. | ||||
|      * @returns Detail of the report. | ||||
|      */ | ||||
|     async loadReport(reportid: number, page?: number, perpage?: number): Promise<CoreReportBuilderRetrieveReportMapped> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
|         const preSets: CoreSiteWSPreSets = { cacheKey: this.getReportBuilderReportCacheKey() }; | ||||
|         const report = await site.read<CoreReportBuilderRetrieveReportWSResponse>( | ||||
|             'core_reportbuilder_retrieve_report', | ||||
|             { reportid, page, perpage }, | ||||
|             preSets, | ||||
|         ); | ||||
| 
 | ||||
|         if (!report) { | ||||
|             throw new CoreError('An error ocurred.'); | ||||
|         } | ||||
| 
 | ||||
|         const settingsData: { | ||||
|             // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|             cardview_showfirsttitle: number; | ||||
|             // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|             cardview_visiblecolumns: number; | ||||
|         } = report.details.settingsdata ? JSON.parse(report.details.settingsdata) : {}; | ||||
| 
 | ||||
|         const mappedSettingsData: CoreReportBuilderReportDetailSettingsData = { | ||||
|             cardviewShowFirstTitle: settingsData.cardview_showfirsttitle === 1, | ||||
|             cardviewVisibleColumns: settingsData.cardview_visiblecolumns ?? 1, | ||||
|         }; | ||||
| 
 | ||||
|         return { | ||||
|             ...report, | ||||
|             details: { | ||||
|                 ...report.details, | ||||
|                 settingsdata: mappedSettingsData, | ||||
|             }, | ||||
|             data: { | ||||
|                 ...report.data, | ||||
|                 rows: [...report.data.rows.map(row => ({ columns: row.columns, isExpanded: row.isExpanded ?? false }))], | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View a report. | ||||
|      * | ||||
|      * @param reportid Report viewed. | ||||
|      * @returns Response of the WS. | ||||
|      */ | ||||
|     async viewReport(reportid: string): Promise<void> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
| 
 | ||||
|         await site.write<CoreReportBuilderViewReportWSResponse>('core_reportbuilder_view_report', { reportid }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the feature is enabled or disabled. | ||||
|      * | ||||
|      * @returns Feature enabled or disabled. | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
|         const hasTheVersionRequired = site.isVersionGreaterEqualThan('4.1'); | ||||
|         const hasAdvancedFeatureEnabled = site.canUseAdvancedFeature('enablecustomreports'); | ||||
|         const isFeatureDisabled = site.isFeatureDisabled('CoreReportBuilderDelegate'); | ||||
| 
 | ||||
|         return hasTheVersionRequired && hasAdvancedFeatureEnabled && !isFeatureDisabled; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates reports list WS calls. | ||||
|      * | ||||
|      * @returns Promise resolved when the list is invalidated. | ||||
|      */ | ||||
|     async invalidateReportsList(): Promise<void> { | ||||
|         const site = CoreSites.getRequiredCurrentSite(); | ||||
|         await site.invalidateWsCacheForKey(this.getReportBuilderCacheKey()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidates report WS calls. | ||||
|      * | ||||
|      * @returns Promise resolved when report is invalidated. | ||||
|      */ | ||||
|     async invalidateReport(): Promise<void> { | ||||
|         const site = CoreSites.getCurrentSite(); | ||||
| 
 | ||||
|         if (!site) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await site.invalidateWsCacheForKey(this.getReportBuilderReportCacheKey()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for report builder list WS calls. | ||||
|      * | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getReportBuilderCacheKey(): string { | ||||
|         return ROOT_CACHE_KEY + 'list'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for report builder report WS calls. | ||||
|      * | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getReportBuilderReportCacheKey(): string { | ||||
|         return ROOT_CACHE_KEY + 'report'; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreReportBuilder = makeSingleton(CoreReportBuilderService); | ||||
| 
 | ||||
| type CoreReportBuilderPagination = { | ||||
|     page?: number; | ||||
|     perpage?: number; | ||||
| }; | ||||
| 
 | ||||
| export type CoreReportBuilderRetrieveReportWSParams = CoreReportBuilderPagination & { | ||||
|     reportid: number; // Report ID.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by core_reportbuilder_list_reports WS. | ||||
|  */ | ||||
| export type CoreReportBuilderListReportsWSResponse = { | ||||
|     reports: CoreReportBuilderReportWSResponse[]; | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| export type CoreReportBuilderReportWSResponse = { | ||||
|     name: string; // Name.
 | ||||
|     source: string; // Source.
 | ||||
|     type: number; // Type.
 | ||||
|     uniquerows: boolean; // Uniquerows.
 | ||||
|     conditiondata: string; // Conditiondata.
 | ||||
|     settingsdata: string | null; // Settingsdata.
 | ||||
|     contextid: number; // Contextid.
 | ||||
|     component: string; // Component.
 | ||||
|     area: string; // Area.
 | ||||
|     itemid: number; // Itemid.
 | ||||
|     usercreated: number; // Usercreated.
 | ||||
|     id: number; // Id.
 | ||||
|     timecreated: number; // Timecreated.
 | ||||
|     timemodified: number; // Timemodified.
 | ||||
|     usermodified: number; // Usermodified.
 | ||||
|     sourcename: string; // Sourcename.
 | ||||
|     modifiedby: { | ||||
|         id: number; // Id.
 | ||||
|         email: string; // Email.
 | ||||
|         idnumber: string; // Idnumber.
 | ||||
|         phone1: string; // Phone1.
 | ||||
|         phone2: string; // Phone2.
 | ||||
|         department: string; // Department.
 | ||||
|         institution: string; // Institution.
 | ||||
|         fullname: string; // Fullname.
 | ||||
|         identity: string; // Identity.
 | ||||
|         profileurl: string; // Profileurl.
 | ||||
|         profileimageurl: string; // Profileimageurl.
 | ||||
|         profileimageurlsmall: string; // Profileimageurlsmall.
 | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by core_reportbuilder_retrieve_report WS. | ||||
|  */ | ||||
| export type CoreReportBuilderRetrieveReportWSResponse = { | ||||
|     details: CoreReportBuilderReportWSResponse; | ||||
|     data: CoreReportBuilderReportDataWSResponse; | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| export interface CoreReportBuilderRetrieveReportMapped extends Omit<CoreReportBuilderRetrieveReportWSResponse, 'details'> { | ||||
|     details: CoreReportBuilderReportDetail; | ||||
| } | ||||
| 
 | ||||
| export type CoreReportBuilderReportDataWSResponse = { | ||||
|     headers: string[]; // Headers.
 | ||||
|     rows: { // Rows.
 | ||||
|         columns: string[]; // Columns.
 | ||||
|         isExpanded: boolean; | ||||
|     }[]; | ||||
|     totalrowcount: number; // Totalrowcount.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Params of core_reportbuilder_view_report WS. | ||||
|  */ | ||||
| export type CoreReportBuilderViewReportWSParams = { | ||||
|     reportid: number; // Report ID.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data returned by core_reportbuilder_view_report WS. | ||||
|  */ | ||||
| export type CoreReportBuilderViewReportWSResponse = { | ||||
|     status: boolean; // Success.
 | ||||
|     warnings?: CoreWSExternalWarning[]; | ||||
| }; | ||||
| 
 | ||||
| export interface CoreReportBuilderReportDetail extends Omit<CoreReportBuilderReportWSResponse, 'settingsdata'> { | ||||
|     settingsdata: CoreReportBuilderReportDetailSettingsData; | ||||
| } | ||||
| 
 | ||||
| export type CoreReportBuilderReportDetailSettingsData = { | ||||
|     cardviewShowFirstTitle: boolean; | ||||
|     cardviewVisibleColumns: number; | ||||
| }; | ||||
| 
 | ||||
| export interface CoreReportBuilderReport extends CoreReportBuilderReportWSResponse {}; | ||||
| @ -0,0 +1,145 @@ | ||||
| @app @javascript @core_reportbuilder | ||||
| Feature: Report builder | ||||
| 
 | ||||
|   Background: | ||||
|     Given the following "core_reportbuilder > Reports" exist: | ||||
|       | name         | source                                   | default | | ||||
|       | My report 01 | core_user\reportbuilder\datasource\users | 1       | | ||||
|       | My report 02 | core_user\reportbuilder\datasource\users | 2       | | ||||
|       | My report 03 | core_user\reportbuilder\datasource\users | 3       | | ||||
|       | My report 04 | core_user\reportbuilder\datasource\users | 4       | | ||||
|       | My report 05 | core_user\reportbuilder\datasource\users | 5       | | ||||
|       | My report 06 | core_user\reportbuilder\datasource\users | 6       | | ||||
|       | My report 07 | core_user\reportbuilder\datasource\users | 7       | | ||||
|       | My report 08 | core_user\reportbuilder\datasource\users | 8       | | ||||
|       | My report 09 | core_user\reportbuilder\datasource\users | 9       | | ||||
|       | My report 10 | core_user\reportbuilder\datasource\users | 10      | | ||||
|       | My report 11 | core_user\reportbuilder\datasource\users | 11      | | ||||
|       | My report 12 | core_user\reportbuilder\datasource\users | 12      | | ||||
|       | My report 13 | core_user\reportbuilder\datasource\users | 13      | | ||||
|       | My report 14 | core_user\reportbuilder\datasource\users | 14      | | ||||
|       | My report 15 | core_user\reportbuilder\datasource\users | 15      | | ||||
|       | My report 16 | core_user\reportbuilder\datasource\users | 16      | | ||||
|       | My report 17 | core_user\reportbuilder\datasource\users | 17      | | ||||
|       | My report 18 | core_user\reportbuilder\datasource\users | 18      | | ||||
|       | My report 19 | core_user\reportbuilder\datasource\users | 19      | | ||||
|       | My report 20 | core_user\reportbuilder\datasource\users | 20      | | ||||
|       | My report 21 | core_user\reportbuilder\datasource\users | 21      | | ||||
|       | My report 22 | core_user\reportbuilder\datasource\users | 22      | | ||||
|       | My report 23 | core_user\reportbuilder\datasource\users | 23      | | ||||
|       | My report 24 | core_user\reportbuilder\datasource\users | 24      | | ||||
|       | My report 25 | core_user\reportbuilder\datasource\users | 25      | | ||||
|       | My report 26 | core_user\reportbuilder\datasource\users | 26      | | ||||
|       | My report 27 | core_user\reportbuilder\datasource\users | 27      | | ||||
|       | My report 28 | core_user\reportbuilder\datasource\users | 28      | | ||||
|       | My report 29 | core_user\reportbuilder\datasource\users | 29      | | ||||
|       | My report 30 | core_user\reportbuilder\datasource\users | 30      | | ||||
|       | My report 31 | core_user\reportbuilder\datasource\users | 31      | | ||||
|       | My report 32 | core_user\reportbuilder\datasource\users | 32      | | ||||
|       | My report 33 | core_user\reportbuilder\datasource\users | 33      | | ||||
|       | My report 34 | core_user\reportbuilder\datasource\users | 34      | | ||||
|       | My report 35 | core_user\reportbuilder\datasource\users | 35      | | ||||
|     And the following "core_reportbuilder > Columns" exist: | ||||
|       | report       | uniqueidentifier | | ||||
|       | My report 01 | user:fullname    | | ||||
|       | My report 02 | user:fullname    | | ||||
|       | My report 03 | user:fullname    | | ||||
|       | My report 04 | user:fullname    | | ||||
|       | My report 05 | user:fullname    | | ||||
|       | My report 06 | user:fullname    | | ||||
|       | My report 07 | user:fullname    | | ||||
|       | My report 08 | user:fullname    | | ||||
|       | My report 09 | user:fullname    | | ||||
|       | My report 10 | user:fullname    | | ||||
|       | My report 11 | user:fullname    | | ||||
|       | My report 12 | user:fullname    | | ||||
|       | My report 13 | user:fullname    | | ||||
|       | My report 14 | user:fullname    | | ||||
|       | My report 15 | user:fullname    | | ||||
|       | My report 16 | user:fullname    | | ||||
|       | My report 17 | user:fullname    | | ||||
|       | My report 18 | user:fullname    | | ||||
|       | My report 19 | user:fullname    | | ||||
|       | My report 20 | user:fullname    | | ||||
|       | My report 21 | user:fullname    | | ||||
|       | My report 22 | user:fullname    | | ||||
|       | My report 23 | user:fullname    | | ||||
|       | My report 24 | user:fullname    | | ||||
|       | My report 25 | user:fullname    | | ||||
|       | My report 26 | user:fullname    | | ||||
|       | My report 27 | user:fullname    | | ||||
|       | My report 28 | user:fullname    | | ||||
|       | My report 29 | user:fullname    | | ||||
|       | My report 30 | user:fullname    | | ||||
|       | My report 31 | user:fullname    | | ||||
|       | My report 32 | user:fullname    | | ||||
|       | My report 33 | user:fullname    | | ||||
|       | My report 34 | user:fullname    | | ||||
|       | My report 35 | user:fullname    | | ||||
|     And the following "core_reportbuilder > Audiences" exist: | ||||
|       | report       | configdata | classname                                          | | ||||
|       | My report 01 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 02 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 03 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 04 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 05 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 06 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 07 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 08 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 09 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 10 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 11 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 12 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 13 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 14 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 15 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 16 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 17 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 18 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 19 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 20 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 21 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 22 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 23 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 24 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 25 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 26 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 27 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 28 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 29 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 30 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 31 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 32 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 33 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 34 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|       | My report 35 |            | core_reportbuilder\reportbuilder\audience\allusers | | ||||
|     And the following "users" exist: | ||||
|       | username | firstname   | lastname | email              | city     | | ||||
|       | student1 | Lionel      | Smith    | lionel@example.com | Bilbao   | | ||||
| 
 | ||||
|   Scenario: Open report in mobile | ||||
|     Given I enter the app | ||||
|     And I log in as "student1" | ||||
|     And I press the user menu button in the app | ||||
|     When I press "Reports" in the app | ||||
| 
 | ||||
|     # Find report in the screen | ||||
|     Then I should find "My report 03" in the app | ||||
|     And I press "My report 03" in the app | ||||
|     And I should find "My report 03" in the app | ||||
|     And I should find "Lionel Smith" in the app | ||||
|     But I should not find "My report 02" in the app | ||||
| 
 | ||||
|   Scenario: Open report in tablet | ||||
|     Given I enter the app | ||||
|     And I change viewport size to "1200x640" | ||||
|     And I log in as "student1" | ||||
|     And I press the user menu button in the app | ||||
|     When I press "Reports" in the app | ||||
| 
 | ||||
|     # Find report in the screen | ||||
|     Then I should find "My report 02" in the app | ||||
|     And I press "My report 02" in the app | ||||
|     And I should find "My report 02" in the app | ||||
|     And I should find "Lionel Smith" in the app | ||||
|     But I should not find "My report 03" in the app | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user