diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts
index 7b5d7e569..6c4e3240e 100644
--- a/src/core/features/features.module.ts
+++ b/src/core/features/features.module.ts
@@ -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,
diff --git a/src/core/features/reportbuilder/components/components.module.ts b/src/core/features/reportbuilder/components/components.module.ts
new file mode 100644
index 000000000..f8f269fc3
--- /dev/null
+++ b/src/core/features/reportbuilder/components/components.module.ts
@@ -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 {}
diff --git a/src/core/features/reportbuilder/components/report-column/report-column.html b/src/core/features/reportbuilder/components/report-column/report-column.html
new file mode 100644
index 000000000..c5ee3a6ff
--- /dev/null
+++ b/src/core/features/reportbuilder/components/report-column/report-column.html
@@ -0,0 +1,11 @@
+
+
+
{{ header }}
+
+
+
+
+
diff --git a/src/core/features/reportbuilder/components/report-column/report-column.scss b/src/core/features/reportbuilder/components/report-column/report-column.scss
new file mode 100644
index 000000000..65df0f953
--- /dev/null
+++ b/src/core/features/reportbuilder/components/report-column/report-column.scss
@@ -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);
+ }
+}
diff --git a/src/core/features/reportbuilder/components/report-column/report-column.ts b/src/core/features/reportbuilder/components/report-column/report-column.ts
new file mode 100644
index 000000000..cf5575c3d
--- /dev/null
+++ b/src/core/features/reportbuilder/components/report-column/report-column.ts
@@ -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 = new EventEmitter();
+
+ /**
+ * Emits row click
+ */
+ toggleRow(): void {
+ this.onToggleRow.emit(this.rowIndex);
+ }
+
+}
diff --git a/src/core/features/reportbuilder/components/report-detail/report-detail.html b/src/core/features/reportbuilder/components/report-detail/report-detail.html
new file mode 100644
index 000000000..13e856f82
--- /dev/null
+++ b/src/core/features/reportbuilder/components/report-detail/report-detail.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ state.cardVisibleColumns"
+ [isExpanded]="row.isExpanded" [showFirstTitle]="state.cardviewShowFirstTitle"
+ [contextId]="state.report.details.contextid" [header]="state.report.data.headers[columnIndex]" [column]="column"
+ (onToggleRow)="toggleRow(rowIndex)">
+
+
+
+
+
+
+
+
diff --git a/src/core/features/reportbuilder/components/report-summary/report-summary.ts b/src/core/features/reportbuilder/components/report-summary/report-summary.ts
new file mode 100644
index 000000000..712914e3a
--- /dev/null
+++ b/src/core/features/reportbuilder/components/report-summary/report-summary.ts
@@ -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();
+ }
+
+}
diff --git a/src/core/features/reportbuilder/pages/list/list.html b/src/core/features/reportbuilder/pages/list/list.html
new file mode 100644
index 000000000..832fc49d5
--- /dev/null
+++ b/src/core/features/reportbuilder/pages/list/list.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
{{ 'core.reportbuilder.reportstab' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ report.name }}
+
{{ report.sourcename }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/features/reportbuilder/pages/list/list.ts b/src/core/features/reportbuilder/pages/list/list.ts
new file mode 100644
index 000000000..2538cb260
--- /dev/null
+++ b/src/core/features/reportbuilder/pages/list/list.ts
@@ -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;
+
+ state$: Readonly> = 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 {
+ 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 {
+ 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): 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 {
+ 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 {
+ 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;
+};
diff --git a/src/core/features/reportbuilder/pages/report/report.html b/src/core/features/reportbuilder/pages/report/report.html
new file mode 100644
index 000000000..4dd499a70
--- /dev/null
+++ b/src/core/features/reportbuilder/pages/report/report.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ reportDetail.name }}
+
{{ reportDetail.sourcename }}
+
+
+
+
+
+
+
+
diff --git a/src/core/features/reportbuilder/pages/report/report.ts b/src/core/features/reportbuilder/pages/report/report.ts
new file mode 100644
index 000000000..958509675
--- /dev/null
+++ b/src/core/features/reportbuilder/pages/report/report.ts
@@ -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({
+ component: CoreReportBuilderReportSummaryComponent,
+ componentProps: { reportDetail: this.reportDetail },
+ });
+ }
+
+}
diff --git a/src/core/features/reportbuilder/reportbuilder-lazy.module.ts b/src/core/features/reportbuilder/reportbuilder-lazy.module.ts
new file mode 100644
index 000000000..c5064d388
--- /dev/null
+++ b/src/core/features/reportbuilder/reportbuilder-lazy.module.ts
@@ -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 {}
diff --git a/src/core/features/reportbuilder/reportbuilder.module.ts b/src/core/features/reportbuilder/reportbuilder.module.ts
new file mode 100644
index 000000000..833c8df1f
--- /dev/null
+++ b/src/core/features/reportbuilder/reportbuilder.module.ts
@@ -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 {}