MOBILE-3641 feedback: Migrate index page
parent
267f4445ae
commit
d3e689aa81
|
@ -0,0 +1,34 @@
|
|||
// (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 { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
import { AddonModFeedbackIndexComponent } from './index/index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModFeedbackIndexComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCourseComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModFeedbackIndexComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModFeedbackComponentsModule {}
|
|
@ -0,0 +1,230 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
|
||||
[href]="externalUrl" iconAction="fas-external-link-alt">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
|
||||
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
|
||||
iconAction="far-newspaper" (action)="gotoBlog()">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
|
||||
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)"
|
||||
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
|
||||
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
|
||||
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<!-- Content. -->
|
||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||
|
||||
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
||||
<core-tabs [hideUntil]="tabsReady" [selectedIndex]="firstSelectedTab">
|
||||
<core-tab [title]="'addon.mod_feedback.overview' | translate" id="overview" (ionSelect)="tabChanged('overview')">
|
||||
<ng-template>
|
||||
<ng-container *ngTemplateOutlet="tabOverview"></ng-container>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
<core-tab *ngIf="showAnalysis && access && access.canviewreports" id="analysis"
|
||||
[title]="'addon.mod_feedback.analysis' | translate" (ionSelect)="tabChanged('analysis')">
|
||||
<ng-template>
|
||||
<ng-container *ngTemplateOutlet="tabAnalysis"></ng-container>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
|
||||
<core-tab *ngIf="showAnalysis && access && !access.canviewreports" id="analysis"
|
||||
[title]="'addon.mod_feedback.completed_feedbacks' | translate" (ionSelect)="tabChanged('analysis')">
|
||||
<ng-template>
|
||||
<ng-container *ngTemplateOutlet="tabAnalysis"></ng-container>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
</core-tabs>
|
||||
</core-loading>
|
||||
|
||||
<ng-template #basicInfo>
|
||||
<ion-list *ngIf="access && access.canviewanalysis && !access.isempty">
|
||||
<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)]="group" (ionChange)="setGroup(group)" 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 class="ion-text-wrap" (click)="openRespondents()" [detail]="access.canviewreports && completedCount > 0">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.completed_feedbacks' | translate }}</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end">{{completedCount}}</ion-badge>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!access.isanonymous && access.canviewreports" (click)="openNonRespondents()"
|
||||
detail="true" tappable="true">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.show_nonrespondents' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.questions' | translate }}</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end">{{itemsCount}}</ion-badge>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ng-template>
|
||||
|
||||
<!-- Template to render the overview. -->
|
||||
<ng-template #tabOverview>
|
||||
<core-loading [hideUntil]="tabsLoaded.overview">
|
||||
<ng-container *ngTemplateOutlet="basicInfo"></ng-container>
|
||||
|
||||
<!-- Feedback done in offline but not synchronized -->
|
||||
<ion-card class="core-warning-card" *ngIf="hasOffline">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="core-info-card" *ngIf="access && access.cancomplete && !access.isopen">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-question-circle" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'addon.mod_feedback.feedback_is_not_open' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="core-success-card" *ngIf="access && access.cancomplete && access.isopen && !access.cansubmit">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-check" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-list *ngIf="access && (access.canedititems || access.canviewreports || !access.isempty)">
|
||||
<ion-item class="ion-text-wrap" *ngIf="access.canedititems && overview.timeopen">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.feedbackopen' | translate }}</h2>
|
||||
<p>{{overview.openTimeReadable}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="access.canedititems && overview.timeclose">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.feedbackclose' | translate }}</h2>
|
||||
<p>{{overview.closeTimeReadable}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="access.canedititems && feedback && feedback.page_after_submit">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.page_after_submit' | translate }}</h2>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="feedback.page_after_submit"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="!access.isempty">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_feedback.mode' | translate }}</h2>
|
||||
<p *ngIf="access.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p>
|
||||
<p *ngIf="!access.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col>
|
||||
<ion-button expand="block" fill="outline" (click)="gotoAnswerQuestions(true)" class="ion-text-wrap">
|
||||
<ion-icon name="fas-search" slot="start"></ion-icon>
|
||||
{{ 'addon.mod_feedback.preview' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="access.cancomplete && access.cansubmit && access.isopen">
|
||||
<ion-button expand="block" (click)="gotoAnswerQuestions()" class="ion-text-wrap">
|
||||
<ng-container *ngIf="!goPage">
|
||||
{{ 'addon.mod_feedback.complete_the_form' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="goPage">
|
||||
{{ 'addon.mod_feedback.continue_the_form' | translate }}
|
||||
</ng-container>
|
||||
<ion-icon name="fas-chevron-right" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ng-template>
|
||||
|
||||
<!-- Template to render the analysis. -->
|
||||
<ng-template #tabAnalysis>
|
||||
<core-loading [hideUntil]="tabsLoaded.analysis">
|
||||
<ng-container *ngTemplateOutlet="basicInfo"></ng-container>
|
||||
|
||||
<ng-container *ngIf="access && (access.canedititems || !access.isempty)">
|
||||
<ion-card class="core-info-card" *ngIf="warning">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-question-circle" slot="start"></ion-icon>
|
||||
<ion-label>{{ warning }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-list *ngIf="items && items.length">
|
||||
<ion-item class="ion-text-wrap core-analysis" *ngFor="let item of items">
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{item.num}}. <core-format-text [component]="component" [componentId]="componentId" [text]="item.name"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="item.label"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<ng-container [ngSwitch]="item.templateName">
|
||||
<ng-container *ngSwitchCase="'numeric'">
|
||||
<ul>
|
||||
<li *ngFor="let data of item.data">{{ data }}</li>
|
||||
</ul>
|
||||
<p>{{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'list'">
|
||||
<ul>
|
||||
<ng-container *ngFor="let data of item.data">
|
||||
<li *ngIf="data">{{ data }}</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'chart'">
|
||||
<core-chart [type]="item.chartType" [data]="item.chartData" [labels]="item.labels" height="300"
|
||||
contextLevel="module" [contextInstanceId]="module.id" [wsNotFiltered]="true"
|
||||
[courseId]="courseId">
|
||||
</core-chart>
|
||||
<p *ngIf="item.average">
|
||||
{{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}}
|
||||
</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ng-container>
|
||||
</core-loading>
|
||||
</ng-template>
|
|
@ -0,0 +1,503 @@
|
|||
// (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, Input, Optional, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
AddonModFeedback,
|
||||
AddonModFeedbackGetFeedbackAccessInformationWSResponse,
|
||||
AddonModFeedbackProvider,
|
||||
AddonModFeedbackWSFeedback,
|
||||
AddonModFeedbackWSItem,
|
||||
} from '../../services/feedback';
|
||||
import { AddonModFeedbackOffline } from '../../services/feedback-offline';
|
||||
import {
|
||||
AddonModFeedbackAutoSyncData,
|
||||
AddonModFeedbackSync,
|
||||
AddonModFeedbackSyncProvider,
|
||||
AddonModFeedbackSyncResult,
|
||||
} from '../../services/feedback-sync';
|
||||
import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module';
|
||||
import { AddonModFeedbackPrefetchHandler } from '../../services/handlers/prefetch';
|
||||
|
||||
/**
|
||||
* Component that displays a feedback index page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-feedback-index',
|
||||
templateUrl: 'addon-mod-feedback-index.html',
|
||||
})
|
||||
export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent;
|
||||
|
||||
@Input() tab = 'overview';
|
||||
@Input() group = 0;
|
||||
|
||||
component = AddonModFeedbackProvider.COMPONENT;
|
||||
moduleName = 'feedback';
|
||||
feedback?: AddonModFeedbackWSFeedback;
|
||||
goPage?: number;
|
||||
items: AddonModFeedbackItem[] = [];
|
||||
warning?: string;
|
||||
showAnalysis = false;
|
||||
tabsReady = false;
|
||||
firstSelectedTab?: number;
|
||||
access?: AddonModFeedbackGetFeedbackAccessInformationWSResponse;
|
||||
completedCount = 0;
|
||||
itemsCount = 0;
|
||||
groupInfo?: CoreGroupInfo;
|
||||
|
||||
overview = {
|
||||
timeopen: 0,
|
||||
openTimeReadable: '',
|
||||
timeclose: 0,
|
||||
closeTimeReadable: '',
|
||||
};
|
||||
|
||||
tabsLoaded = {
|
||||
overview: false,
|
||||
analysis: false,
|
||||
};
|
||||
|
||||
protected submitObserver: CoreEventObserver;
|
||||
protected syncEventName = AddonModFeedbackSyncProvider.AUTO_SYNCED;
|
||||
|
||||
constructor(
|
||||
protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModLessonIndexComponent', content, courseContentsPage);
|
||||
|
||||
// Listen for form submit events.
|
||||
this.submitObserver = CoreEvents.on(AddonModFeedbackProvider.FORM_SUBMITTED, async (data) => {
|
||||
if (!this.feedback || data.feedbackId != this.feedback.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabsLoaded.analysis = false;
|
||||
this.tabsLoaded.overview = false;
|
||||
this.loaded = false;
|
||||
|
||||
// Prefetch data if needed.
|
||||
if (!data.offline && this.isPrefetched()) {
|
||||
await CoreUtils.ignoreErrors(AddonModFeedbackSync.prefetchAfterUpdate(
|
||||
AddonModFeedbackPrefetchHandler.instance,
|
||||
this.module,
|
||||
this.courseId,
|
||||
));
|
||||
}
|
||||
|
||||
// Load the right tab.
|
||||
if (data.tab != this.tab) {
|
||||
this.tabChanged(data.tab);
|
||||
} else {
|
||||
this.loadContent(true);
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
super.ngOnInit();
|
||||
|
||||
try {
|
||||
await this.loadContent(false, true);
|
||||
|
||||
if (this.feedback) {
|
||||
CoreUtils.ignoreErrors(AddonModFeedback.logView(this.feedback.id, this.feedback.name));
|
||||
}
|
||||
} finally {
|
||||
this.tabsReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModFeedback.invalidateFeedbackData(this.courseId));
|
||||
if (this.feedback) {
|
||||
promises.push(AddonModFeedback.invalidateFeedbackAccessInformationData(this.feedback.id));
|
||||
promises.push(AddonModFeedback.invalidateAnalysisData(this.feedback.id));
|
||||
promises.push(CoreGroups.invalidateActivityAllowedGroups(this.feedback.coursemodule));
|
||||
promises.push(CoreGroups.invalidateActivityGroupMode(this.feedback.coursemodule));
|
||||
promises.push(AddonModFeedback.invalidateResumePageData(this.feedback.id));
|
||||
}
|
||||
|
||||
this.tabsLoaded.analysis = false;
|
||||
this.tabsLoaded.overview = false;
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: AddonModFeedbackAutoSyncData): boolean {
|
||||
if (this.feedback && syncEventData.feedbackId == this.feedback.id) {
|
||||
// Refresh the data.
|
||||
this.content?.scrollToTop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||
try {
|
||||
this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.module.id);
|
||||
|
||||
this.description = this.feedback.intro;
|
||||
this.dataRetrieved.emit(this.feedback);
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the feedback.
|
||||
await this.syncActivity(showErrors);
|
||||
}
|
||||
|
||||
// Check if there are answers stored in offline.
|
||||
this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback.id, { cmId: this.module.id });
|
||||
|
||||
this.showAnalysis = (this.access.canviewreports || this.access.canviewanalysis) && !this.access.isempty;
|
||||
this.firstSelectedTab = 0;
|
||||
if (!this.showAnalysis) {
|
||||
this.tab = 'overview';
|
||||
}
|
||||
|
||||
if (this.tab == 'analysis') {
|
||||
this.firstSelectedTab = 1;
|
||||
|
||||
return await this.fetchFeedbackAnalysisData();
|
||||
}
|
||||
|
||||
await this.fetchFeedbackOverviewData();
|
||||
} finally {
|
||||
// Now fill the context menu.
|
||||
this.fillContextMenu(refresh);
|
||||
|
||||
if (this.feedback) {
|
||||
// Check if there are responses stored in offline.
|
||||
this.hasOffline = await AddonModFeedbackOffline.hasFeedbackOfflineData(this.feedback.id);
|
||||
}
|
||||
|
||||
if (this.tabsReady) {
|
||||
// Make sure the right tab is selected.
|
||||
this.tabsComponent?.selectTab(this.tab || 'overview');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get feedback overview data.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async fetchFeedbackOverviewData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (this.access!.cancomplete && this.access!.cansubmit && this.access!.isopen) {
|
||||
promises.push(AddonModFeedback.getResumePage(this.feedback!.id, { cmId: this.module.id }).then((goPage) => {
|
||||
this.goPage = goPage > 0 ? goPage : undefined;
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.access!.canedititems) {
|
||||
this.overview.timeopen = (this.feedback!.timeopen || 0) * 1000;
|
||||
this.overview.openTimeReadable = this.overview.timeopen ? CoreTimeUtils.userDate(this.overview.timeopen) : '';
|
||||
this.overview.timeclose = (this.feedback!.timeclose || 0) * 1000;
|
||||
this.overview.closeTimeReadable = this.overview.timeclose ? CoreTimeUtils.userDate(this.overview.timeclose) : '';
|
||||
}
|
||||
if (this.access!.canviewanalysis) {
|
||||
// Get groups (only for teachers).
|
||||
promises.push(this.fetchGroupInfo(this.module.id));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
this.tabsLoaded.overview = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get feedback analysis data.
|
||||
*
|
||||
* @param accessData Retrieved access data.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async fetchFeedbackAnalysisData(): Promise<void> {
|
||||
try {
|
||||
if (this.access!.canviewanalysis) {
|
||||
// Get groups (only for teachers).
|
||||
await this.fetchGroupInfo(this.module.id);
|
||||
} else {
|
||||
this.tabChanged('overview');
|
||||
}
|
||||
|
||||
} finally {
|
||||
this.tabsLoaded.analysis = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Group info data.
|
||||
*
|
||||
* @param cmId Course module ID.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async fetchGroupInfo(cmId: number): Promise<void> {
|
||||
this.groupInfo = await CoreGroups.getActivityGroupInfo(cmId);
|
||||
|
||||
await this.setGroup(CoreGroups.validateGroupId(this.group, this.groupInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the analysis info to show the info correctly formatted.
|
||||
*
|
||||
* @param item Item to parse.
|
||||
* @return Parsed item.
|
||||
*/
|
||||
protected parseAnalysisInfo(item: AddonModFeedbackItem): AddonModFeedbackItem {
|
||||
switch (item.typ) {
|
||||
case 'numeric':
|
||||
item.average = item.data.reduce((prev, current) => prev + Number(current), 0) / item.data.length;
|
||||
item.templateName = 'numeric';
|
||||
break;
|
||||
|
||||
case 'info':
|
||||
item.data = <string[]> item.data.map((dataItem) => {
|
||||
const parsed = <Record<string, string>> CoreTextUtils.parseJSON(dataItem);
|
||||
|
||||
return typeof parsed.show != 'undefined' ? parsed.show : false;
|
||||
}).filter((dataItem) => dataItem); // Filter false entries.
|
||||
|
||||
case 'textfield':
|
||||
case 'textarea':
|
||||
item.templateName = 'list';
|
||||
break;
|
||||
|
||||
case 'multichoicerated':
|
||||
case 'multichoice': {
|
||||
const parsedData = <Record<string, string | number>[]> item.data.map((dataItem) => {
|
||||
const parsed = <Record<string, string | number>> CoreTextUtils.parseJSON(dataItem);
|
||||
|
||||
return typeof parsed.answertext != 'undefined' ? parsed : false;
|
||||
}).filter((dataItem) => dataItem); // Filter false entries.
|
||||
|
||||
// Format labels.
|
||||
item.labels = parsedData.map((dataItem) => {
|
||||
dataItem.quotient = (<number> dataItem.quotient * 100).toFixed(2);
|
||||
let label = '';
|
||||
|
||||
if (typeof dataItem.value != 'undefined') {
|
||||
label = '(' + dataItem.value + ') ';
|
||||
}
|
||||
label += dataItem.answertext;
|
||||
label += Number(dataItem.quotient) > 0 ? ' (' + dataItem.quotient + '%)' : '';
|
||||
|
||||
return label;
|
||||
});
|
||||
|
||||
item.chartData = parsedData.map((dataItem) => <number> dataItem.answercount);
|
||||
|
||||
if (item.typ == 'multichoicerated') {
|
||||
item.average = parsedData.reduce((prev, current) => prev + Number(current.avg), 0.0);
|
||||
}
|
||||
|
||||
const subtype = item.presentation.charAt(0);
|
||||
|
||||
const single = subtype != 'c';
|
||||
item.chartType = single ? 'doughnut' : 'bar';
|
||||
item.templateName = 'chart';
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to go to the questions form.
|
||||
*
|
||||
* @param preview Preview or edit the form.
|
||||
*/
|
||||
gotoAnswerQuestions(preview: boolean = false): void {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/form`,
|
||||
{
|
||||
params: {
|
||||
preview,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
super.ionViewDidEnter();
|
||||
|
||||
this.tabsComponent?.ionViewDidEnter();
|
||||
}
|
||||
|
||||
/**
|
||||
* User left the page that contains the component.
|
||||
*/
|
||||
ionViewDidLeave(): void {
|
||||
super.ionViewDidLeave();
|
||||
|
||||
this.tabsComponent?.ionViewDidLeave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open non respondents page.
|
||||
*/
|
||||
openNonRespondents(): void {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/nonrespondents`,
|
||||
{
|
||||
params: {
|
||||
group: this.group,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open respondents page.
|
||||
*/
|
||||
openRespondents(): void {
|
||||
if (!this.access!.canviewreports || this.completedCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CoreNavigator.navigateToSitePath(
|
||||
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/respondents`,
|
||||
{
|
||||
params: {
|
||||
group: this.group,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab changed, fetch content again.
|
||||
*
|
||||
* @param tabName New tab name.
|
||||
*/
|
||||
tabChanged(tabName: string): void {
|
||||
this.tab = tabName;
|
||||
|
||||
if (!this.tabsLoaded[this.tab]) {
|
||||
this.loadContent(false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group to see the analysis.
|
||||
*
|
||||
* @param groupId Group ID.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async setGroup(groupId: number): Promise<void> {
|
||||
this.group = groupId;
|
||||
|
||||
const analysis = await AddonModFeedback.getAnalysis(this.feedback!.id, { groupId, cmId: this.module.id });
|
||||
|
||||
this.completedCount = analysis.completedcount;
|
||||
this.itemsCount = analysis.itemscount;
|
||||
|
||||
if (this.tab == 'analysis') {
|
||||
let num = 1;
|
||||
|
||||
this.items = <AddonModFeedbackItem[]> analysis.itemsdata.map((itemData) => {
|
||||
const item: AddonModFeedbackItem = Object.assign(itemData.item, {
|
||||
data: itemData.data,
|
||||
num: num++,
|
||||
});
|
||||
|
||||
// Move data inside item.
|
||||
if (item.data && item.data.length) {
|
||||
return this.parseAnalysisInfo(item);
|
||||
}
|
||||
|
||||
return false;
|
||||
}).filter((item) => item);
|
||||
|
||||
this.warning = '';
|
||||
if (analysis.warnings?.length) {
|
||||
const warning = analysis.warnings.find((warning) => warning.warningcode == 'insufficientresponsesforthisgroup');
|
||||
this.warning = warning?.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected sync(): Promise<AddonModFeedbackSyncResult> {
|
||||
return AddonModFeedbackSync.syncFeedback(this.feedback!.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected hasSyncSucceed(result: AddonModFeedbackSyncResult): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
this.submitObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonModFeedbackItem = AddonModFeedbackWSItem & {
|
||||
data: string[];
|
||||
num: number;
|
||||
templateName?: string;
|
||||
average?: number;
|
||||
labels?: string[];
|
||||
chartData?: number[];
|
||||
chartType?: string;
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
// (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 { RouterModule, Routes } from '@angular/router';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModFeedbackComponentsModule } from './components/components.module';
|
||||
import { AddonModFeedbackIndexPage } from './pages/index/index';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: ':courseId/:cmId',
|
||||
component: AddonModFeedbackIndexPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
AddonModFeedbackComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
AddonModFeedbackIndexPage,
|
||||
],
|
||||
})
|
||||
export class AddonModFeedbackLazyModule {}
|
|
@ -0,0 +1,22 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-feedback-index [module]="module" [courseId]="courseId" [group]="selectedGroup" [tab]="selectedTab"
|
||||
(dataRetrieved)="updateData($event)"></addon-mod-feedback-index>
|
||||
</ion-content>
|
|
@ -0,0 +1,43 @@
|
|||
// (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, ViewChild } from '@angular/core';
|
||||
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { AddonModFeedbackIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays a feedback.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-feedback-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModFeedbackIndexPage extends CoreCourseModuleMainActivityPage<AddonModFeedbackIndexComponent> implements OnInit {
|
||||
|
||||
@ViewChild(AddonModFeedbackIndexComponent) activityComponent?: AddonModFeedbackIndexComponent;
|
||||
|
||||
selectedTab?: string;
|
||||
selectedGroup?: number;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.selectedTab = CoreNavigator.getRouteParam('tab');
|
||||
this.selectedGroup = CoreNavigator.getRouteNumberParam('group');
|
||||
}
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
|
||||
[attr.aria-label]="description" aria-hidden="false">
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<ion-slide [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide {{tab.class}}"
|
||||
<ion-slide *ngIf="tab.enabled" [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide {{tab.class}}"
|
||||
role="tab" [attr.aria-label]="tab.title | translate" [attr.aria-controls]="tab.id" [id]="tab.id! + '-tab'"
|
||||
[tabindex]="selected == tab.id ? null : -1" (click)="selectTab(tab.id, $event)">
|
||||
<ion-icon *ngIf="tab.icon" [name]="tab.icon"></ion-icon>
|
||||
|
|
|
@ -49,19 +49,21 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
|
|||
@Input() icon?: string; // The tab icon.
|
||||
@Input() badge?: string; // A badge to add in the tab.
|
||||
@Input() badgeStyle?: string; // The badge color.
|
||||
@Input() enabled = true; // Whether the tab is enabled.
|
||||
@Input() class?: string; // Class, if needed.
|
||||
@Input() set show(val: boolean) { // Whether the tab should be shown. Use a setter to detect changes on the value.
|
||||
if (typeof val != 'undefined') {
|
||||
const hasChanged = this.isShown != val;
|
||||
this.isShown = val;
|
||||
@Input() set enabled(value: boolean) { // Whether the tab should be shown.
|
||||
value = value === undefined ? true : value;
|
||||
const hasChanged = this.isEnabled != value;
|
||||
this.isEnabled = value;
|
||||
|
||||
if (this.initialized && hasChanged) {
|
||||
this.tabs.tabVisibilityChanged();
|
||||
}
|
||||
if (this.initialized && hasChanged) {
|
||||
this.tabs.tabVisibilityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.isEnabled;
|
||||
}
|
||||
|
||||
@Input() id?: string; // An ID to identify the tab.
|
||||
@Output() ionSelect: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>();
|
||||
|
||||
|
@ -70,9 +72,10 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
|
|||
element: HTMLElement; // The core-tab element.
|
||||
loaded = false;
|
||||
initialized = false;
|
||||
isShown = true;
|
||||
tabElement?: HTMLElement | null;
|
||||
|
||||
protected isEnabled = true;
|
||||
|
||||
constructor(
|
||||
protected tabs: CoreTabsComponent,
|
||||
element: ElementRef,
|
||||
|
|
Loading…
Reference in New Issue