MOBILE-3641 feedback: Migrate other pages
parent
77966d6fb4
commit
66993dc231
|
@ -17,8 +17,11 @@ import { RouterModule, Routes } from '@angular/router';
|
|||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModFeedbackComponentsModule } from './components/components.module';
|
||||
import { AddonModFeedbackIndexPage } from './pages/index/index';
|
||||
import { AddonModFeedbackRespondentsPage } from './pages/respondents/respondents';
|
||||
import { conditionalRoutes } from '@/app/app-routing.module';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
|
||||
const routes: Routes = [
|
||||
const commonRoutes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModFeedbackIndexPage,
|
||||
|
@ -27,6 +30,42 @@ const routes: Routes = [
|
|||
path: ':courseId/:cmId/form',
|
||||
loadChildren: () => import('./pages/form/form.module').then(m => m.AddonModFeedbackFormPageModule),
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/nonrespondents',
|
||||
loadChildren: () => import('./pages/nonrespondents/nonrespondents.module')
|
||||
.then(m => m.AddonModFeedbackNonRespondentsPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
const mobileRoutes: Routes = [
|
||||
...commonRoutes,
|
||||
{
|
||||
path: ':courseId/:cmId/respondents',
|
||||
component: AddonModFeedbackRespondentsPage,
|
||||
},
|
||||
{
|
||||
path: ':courseId/:cmId/attempt/:attemptId',
|
||||
loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
const tabletRoutes: Routes = [
|
||||
...commonRoutes,
|
||||
{
|
||||
path: ':courseId/:cmId/respondents',
|
||||
component: AddonModFeedbackRespondentsPage,
|
||||
children: [
|
||||
{
|
||||
path: 'attempt/:attemptId',
|
||||
loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
|
||||
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -37,6 +76,7 @@ const routes: Routes = [
|
|||
],
|
||||
declarations: [
|
||||
AddonModFeedbackIndexPage,
|
||||
AddonModFeedbackRespondentsPage,
|
||||
],
|
||||
})
|
||||
export class AddonModFeedbackLazyModule {}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<ng-container *ngIf="attempt">{{ attempt.fullname }}</ng-container>
|
||||
<ng-container *ngIf="anonAttempt">
|
||||
{{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}}
|
||||
</ng-container>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list class="ion-no-margin" *ngIf="attempt || anonAttempt">
|
||||
<ion-item *ngIf="attempt" class="ion-text-wrap" core-user-link [userId]="attempt.userid"
|
||||
[attr.aria-label]=" 'core.user.viewprofile' | translate" [courseId]="attempt.courseid" [title]="attempt.fullname">
|
||||
<core-user-avatar [user]="attempt" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{attempt.fullname}}</h2>
|
||||
<p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="anonAttempt">
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}}
|
||||
({{ 'addon.mod_feedback.anonymous' |translate }})
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item >
|
||||
<ng-container *ngIf="items && items.length">
|
||||
<ng-container *ngFor="let item of items">
|
||||
<ion-item-divider *ngIf="item.typ == 'pagebreak'">
|
||||
<ion-label></ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''">
|
||||
<ion-label>
|
||||
<h2 *ngIf="item.name" [core-mark-required]="item.required">
|
||||
<span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span>
|
||||
<core-format-text [component]="component" [componentId]="cmId" [text]="item.name"
|
||||
contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="item.submittedValue">
|
||||
<core-format-text [component]="component" [componentId]="cmId" [text]="item.submittedValue"
|
||||
contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,37 @@
|
|||
// (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 { AddonModFeedbackAttemptPage } from './attempt';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AddonModFeedbackAttemptPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModFeedbackAttemptPage,
|
||||
],
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AddonModFeedbackAttemptPageModule {}
|
|
@ -0,0 +1,125 @@
|
|||
// (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 { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import {
|
||||
AddonModFeedback,
|
||||
AddonModFeedbackProvider,
|
||||
AddonModFeedbackWSAnonAttempt,
|
||||
AddonModFeedbackWSAttempt,
|
||||
AddonModFeedbackWSFeedback,
|
||||
} from '../../services/feedback';
|
||||
import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper';
|
||||
|
||||
/**
|
||||
* Page that displays a feedback attempt review.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-feedback-attempt',
|
||||
templateUrl: 'attempt.html',
|
||||
})
|
||||
export class AddonModFeedbackAttemptPage implements OnInit {
|
||||
|
||||
protected attemptId!: number;
|
||||
|
||||
cmId!: number;
|
||||
courseId!: number;
|
||||
feedback?: AddonModFeedbackWSFeedback;
|
||||
attempt?: AddonModFeedbackWSAttempt;
|
||||
anonAttempt?: AddonModFeedbackWSAnonAttempt;
|
||||
items: AddonModFeedbackAttemptItem[] = [];
|
||||
component = AddonModFeedbackProvider.COMPONENT;
|
||||
loaded = false;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||
this.attemptId = CoreNavigator.getRouteNumberParam('attemptId')!;
|
||||
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchData(): Promise<void> {
|
||||
try {
|
||||
this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId);
|
||||
|
||||
const attempt = await AddonModFeedback.getAttempt(this.feedback.id, this.attemptId, { cmId: this.cmId });
|
||||
|
||||
if (this.isAnonAttempt(attempt)) {
|
||||
this.anonAttempt = attempt;
|
||||
delete this.attempt;
|
||||
} else {
|
||||
this.attempt = attempt;
|
||||
delete this.anonAttempt;
|
||||
}
|
||||
|
||||
const items = await AddonModFeedback.getItems(this.feedback.id, { cmId: this.cmId });
|
||||
|
||||
// Add responses and format items.
|
||||
this.items = <AddonModFeedbackAttemptItem[]> items.items.map((item) => {
|
||||
const formItem = AddonModFeedbackHelper.getItemForm(item, true);
|
||||
if (!formItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptItem = <AddonModFeedbackAttemptItem> formItem;
|
||||
|
||||
if (item.typ == 'label') {
|
||||
attemptItem.submittedValue = CoreTextUtils.replacePluginfileUrls(item.presentation, item.itemfiles);
|
||||
} else {
|
||||
for (const x in attempt.responses) {
|
||||
if (attempt.responses[x].id == item.id) {
|
||||
attemptItem.submittedValue = attempt.responses[x].printval;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attemptItem;
|
||||
}).filter((itemData) => itemData); // Filter items with errors.
|
||||
|
||||
} catch (message) {
|
||||
// Some call failed on fetch, go back.
|
||||
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||
CoreNavigator.back();
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attempt is anonymous or not.
|
||||
*
|
||||
* @param attempt Attempt to check.
|
||||
*/
|
||||
isAnonAttempt(attempt: AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt): attempt is AddonModFeedbackWSAnonAttempt {
|
||||
return !('fullname' in attempt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModFeedbackAttemptItem = AddonModFeedbackFormItem & {
|
||||
submittedValue?: string;
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_feedback.responses' |translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshFeedback($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list class="ion-no-margin">
|
||||
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||
<ion-label id="addon-feedback-groupslabel">
|
||||
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
|
||||
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)"
|
||||
aria-labelledby="addon-feedback-groupslabel" interface="action-sheet">
|
||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||
{{groupOpt.name}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>
|
||||
<ion-label>{{ 'addon.mod_feedback.non_respondents_students' | translate : {$a: total } }}</ion-label>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngIf="total > 0">
|
||||
<ion-item *ngFor="let user of users" class="ion-text-wrap">
|
||||
<core-user-avatar [user]="user" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
<p>
|
||||
<ion-badge color="success" *ngIf="user.started">
|
||||
{{ 'addon.mod_feedback.started' | translate}}
|
||||
</ion-badge>
|
||||
<ion-badge color="danger" *ngIf="!user.started">
|
||||
{{ 'addon.mod_feedback.not_started' | translate}}
|
||||
</ion-badge>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<core-infinite-loading [enabled]="canLoadMore" (action)="loadAttempts(undefined, $event)" [error]="loadMoreError">
|
||||
</core-infinite-loading>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,37 @@
|
|||
// (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 { AddonModFeedbackNonRespondentsPage } from './nonrespondents';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AddonModFeedbackNonRespondentsPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModFeedbackNonRespondentsPage,
|
||||
],
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AddonModFeedbackNonRespondentsPageModule {}
|
|
@ -0,0 +1,165 @@
|
|||
// (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 { IonRefresher } from '@ionic/angular';
|
||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { AddonModFeedback, AddonModFeedbackWSFeedback } from '../../services/feedback';
|
||||
import { AddonModFeedbackHelper, AddonModFeedbackNonRespondent } from '../../services/feedback-helper';
|
||||
|
||||
/**
|
||||
* Page that displays feedback non respondents.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-feedback-nonrespondents',
|
||||
templateUrl: 'nonrespondents.html',
|
||||
})
|
||||
export class AddonModFeedbackNonRespondentsPage implements OnInit {
|
||||
|
||||
protected cmId!: number;
|
||||
protected courseId!: number;
|
||||
protected feedback?: AddonModFeedbackWSFeedback;
|
||||
protected page = 0;
|
||||
|
||||
selectedGroup!: number;
|
||||
groupInfo?: CoreGroupInfo;
|
||||
users: AddonModFeedbackNonRespondent[] = [];
|
||||
total = 0;
|
||||
canLoadMore = false;
|
||||
loaded = false;
|
||||
loadMoreError = false;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
||||
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*
|
||||
* @param refresh Empty events array first.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchData(refresh: boolean = false): Promise<void> {
|
||||
this.page = 0;
|
||||
this.total = 0;
|
||||
this.users = [];
|
||||
|
||||
try {
|
||||
this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId);
|
||||
|
||||
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId);
|
||||
|
||||
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
|
||||
|
||||
await this.loadGroupUsers(this.selectedGroup);
|
||||
} catch (message) {
|
||||
CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||
|
||||
if (!refresh) {
|
||||
// Some call failed on first fetch, go back.
|
||||
CoreNavigator.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Group responses.
|
||||
*
|
||||
* @param groupId If defined it will change group if not, it will load more users for the same group.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadGroupUsers(groupId?: number): Promise<void> {
|
||||
this.loadMoreError = false;
|
||||
|
||||
if (typeof groupId == 'undefined') {
|
||||
this.page++;
|
||||
} else {
|
||||
this.selectedGroup = groupId;
|
||||
this.page = 0;
|
||||
this.total = 0;
|
||||
this.users = [];
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await AddonModFeedbackHelper.getNonRespondents(this.feedback!.id, {
|
||||
groupId: this.selectedGroup,
|
||||
page: this.page,
|
||||
cmId: this.cmId,
|
||||
});
|
||||
|
||||
this.total = response.total;
|
||||
if (this.users.length < response.total) {
|
||||
this.users = this.users.concat(response.users);
|
||||
}
|
||||
|
||||
this.canLoadMore = this.users.length < response.total;
|
||||
} catch (error) {
|
||||
this.loadMoreError = true;
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change selected group or load more users.
|
||||
*
|
||||
* @param groupId Group ID selected. If not defined, it will load more users.
|
||||
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||
*/
|
||||
async loadAttempts(groupId?: number, infiniteComplete?: () => void): Promise<void> {
|
||||
try {
|
||||
await this.loadGroupUsers(groupId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
} finally {
|
||||
infiniteComplete && infiniteComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the attempts.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshFeedback(refresher: IonRefresher): Promise<void> {
|
||||
try {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreGroups.invalidateActivityGroupInfo(this.cmId));
|
||||
if (this.feedback) {
|
||||
promises.push(AddonModFeedback.invalidateNonRespondentsData(this.feedback.id));
|
||||
}
|
||||
|
||||
await CoreUtils.ignoreErrors(Promise.all(promises));
|
||||
|
||||
await this.fetchData(true);
|
||||
} finally {
|
||||
refresher.complete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'addon.mod_feedback.responses' |translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshFeedback($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list class="ion-no-margin">
|
||||
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||
<ion-label id="addon-feedback-groupslabel">
|
||||
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
|
||||
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)"
|
||||
aria-labelledby="addon-feedback-groupslabel" interface="action-sheet">
|
||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||
{{groupOpt.name}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="responses.responses.total > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: responses.responses.total } }}
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let attempt of responses.responses.attempts" class="ion-text-wrap" tappable detail="true"
|
||||
(click)="responses.select(attempt)" [class.core-selected-item]="responses.isSelected(attempt)">
|
||||
<core-user-avatar [user]="attempt" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>{{ attempt.fullname }}</h2>
|
||||
<p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Button and spinner to show more attempts. -->
|
||||
<ion-button *ngIf="responses.responses.canLoadMore && !loadingMore" class="ion-margin" expand="block"
|
||||
(click)="loadAttempts()">
|
||||
{{ 'core.loadmore' | translate }}
|
||||
</ion-button>
|
||||
<ion-item *ngIf="responses.responses.canLoadMore && loadingMore" class="ion-text-center">
|
||||
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="responses.anonResponses.total > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: responses.anonResponses.total } }}
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let attempt of responses.anonResponses.attempts" class="ion-text-wrap" tappable detail="true"
|
||||
(click)="responses.select(attempt)" [class.core-selected-item]="responses.isSelected(attempt)">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Button and spinner to show more attempts. -->
|
||||
<ion-button *ngIf="responses.anonResponses.canLoadMore && !loadingMore" class="ion-margin" expand="block"
|
||||
(click)="loadAttempts()">
|
||||
{{ 'core.loadmore' | translate }}
|
||||
</ion-button>
|
||||
<ion-item *ngIf="responses.anonResponses.canLoadMore && loadingMore" class="ion-text-center">
|
||||
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
</ion-content>
|
|
@ -0,0 +1,248 @@
|
|||
// (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, Component, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import {
|
||||
AddonModFeedback,
|
||||
AddonModFeedbackWSAnonAttempt,
|
||||
AddonModFeedbackWSAttempt,
|
||||
AddonModFeedbackWSFeedback,
|
||||
} from '../../services/feedback';
|
||||
import { AddonModFeedbackHelper, AddonModFeedbackResponsesAnalysis } from '../../services/feedback-helper';
|
||||
|
||||
/**
|
||||
* Page that displays feedback respondents.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-feedback-respondents',
|
||||
templateUrl: 'respondents.html',
|
||||
})
|
||||
export class AddonModFeedbackRespondentsPage implements AfterViewInit {
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
protected cmId!: number;
|
||||
protected courseId!: number;
|
||||
protected page = 0;
|
||||
protected feedback?: AddonModFeedbackWSFeedback;
|
||||
|
||||
responses: AddonModFeedbackResponsesManager;
|
||||
selectedGroup!: number;
|
||||
groupInfo?: CoreGroupInfo;
|
||||
loaded = false;
|
||||
loadingMore = false;
|
||||
|
||||
constructor(
|
||||
route: ActivatedRoute,
|
||||
) {
|
||||
this.responses = new AddonModFeedbackResponsesManager(
|
||||
route.component,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
|
||||
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
||||
|
||||
await this.fetchData();
|
||||
|
||||
this.responses.start(this.splitView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*
|
||||
* @param refresh Empty events array first.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async fetchData(refresh: boolean = false): Promise<void> {
|
||||
this.page = 0;
|
||||
this.responses.resetItems();
|
||||
|
||||
try {
|
||||
this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId);
|
||||
|
||||
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId);
|
||||
|
||||
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
|
||||
|
||||
await this.loadGroupAttempts(this.selectedGroup);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
|
||||
if (!refresh) {
|
||||
// Some call failed on first fetch, go back.
|
||||
CoreNavigator.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Group attempts.
|
||||
*
|
||||
* @param groupId If defined it will change group if not, it will load more attempts for the same group.
|
||||
* @return Resolved with the attempts loaded.
|
||||
*/
|
||||
protected async loadGroupAttempts(groupId?: number): Promise<void> {
|
||||
if (typeof groupId == 'undefined') {
|
||||
this.page++;
|
||||
this.loadingMore = true;
|
||||
} else {
|
||||
this.selectedGroup = groupId;
|
||||
this.page = 0;
|
||||
this.responses.resetItems();
|
||||
}
|
||||
|
||||
try {
|
||||
const responses = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback!.id, {
|
||||
groupId: this.selectedGroup,
|
||||
page: this.page,
|
||||
cmId: this.cmId,
|
||||
});
|
||||
|
||||
this.responses.setResponses(responses);
|
||||
} finally {
|
||||
this.loadingMore = false;
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change selected group or load more attempts.
|
||||
*
|
||||
* @param groupId Group ID selected. If not defined, it will load more attempts.
|
||||
*/
|
||||
async loadAttempts(groupId?: number): Promise<void> {
|
||||
try {
|
||||
await this.loadGroupAttempts(groupId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the attempts.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshFeedback(refresher: IonRefresher): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreGroups.invalidateActivityGroupInfo(this.cmId));
|
||||
if (this.feedback) {
|
||||
promises.push(AddonModFeedback.invalidateResponsesAnalysisData(this.feedback.id));
|
||||
}
|
||||
|
||||
try {
|
||||
await CoreUtils.ignoreErrors(Promise.all(promises));
|
||||
|
||||
await this.fetchData(true);
|
||||
} finally {
|
||||
refresher.complete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of items that can be held by the entries manager.
|
||||
*/
|
||||
type EntryItem = AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt;
|
||||
|
||||
/**
|
||||
* Entries manager.
|
||||
*/
|
||||
class AddonModFeedbackResponsesManager extends CorePageItemsListManager<EntryItem> {
|
||||
|
||||
responses: AddonModFeedbackResponses = {
|
||||
attempts: [],
|
||||
total: 0,
|
||||
canLoadMore: false,
|
||||
};
|
||||
|
||||
anonResponses: AddonModFeedbackAnonResponses = {
|
||||
attempts: [],
|
||||
total: 0,
|
||||
canLoadMore: false,
|
||||
};
|
||||
|
||||
constructor(pageComponent: unknown) {
|
||||
super(pageComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update responses.
|
||||
*
|
||||
* @param responses Responses.
|
||||
*/
|
||||
setResponses(responses: AddonModFeedbackResponsesAnalysis): void {
|
||||
this.responses.total = responses.totalattempts;
|
||||
this.anonResponses.total = responses.totalanonattempts;
|
||||
|
||||
if (this.anonResponses.attempts.length < responses.totalanonattempts) {
|
||||
this.anonResponses.attempts = this.anonResponses.attempts.concat(responses.anonattempts);
|
||||
}
|
||||
if (this.responses.attempts.length < responses.totalattempts) {
|
||||
this.responses.attempts = this.responses.attempts.concat(responses.attempts);
|
||||
}
|
||||
|
||||
this.anonResponses.canLoadMore = this.anonResponses.attempts.length < responses.totalanonattempts;
|
||||
this.responses.canLoadMore = this.responses.attempts.length < responses.totalattempts;
|
||||
|
||||
this.setItems((<EntryItem[]> this.responses.attempts).concat(this.anonResponses.attempts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
resetItems(): void {
|
||||
super.resetItems();
|
||||
this.responses.total = 0;
|
||||
this.responses.attempts = [];
|
||||
this.anonResponses.total = 0;
|
||||
this.anonResponses.attempts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(entry: EntryItem): string {
|
||||
return `attempt/${entry.id}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModFeedbackResponses = {
|
||||
attempts: AddonModFeedbackWSAttempt[];
|
||||
total: number;
|
||||
canLoadMore: boolean;
|
||||
};
|
||||
|
||||
type AddonModFeedbackAnonResponses = {
|
||||
attempts: AddonModFeedbackWSAnonAttempt[];
|
||||
total: number;
|
||||
canLoadMore: boolean;
|
||||
};
|
Loading…
Reference in New Issue