src/app/modules/groups/components/activity/activity-details/activity-details.component.ts
OnInit
OnDestroy
selector | app-activity-details |
styleUrls | ./activity-details.component.scss |
templateUrl | ./activity-details.component.html |
constructor(resourceService: ResourceService, activatedRoute: ActivatedRoute, groupService: GroupsService, toasterService: ToasterService, userService: UserService, router: Router, layoutService: LayoutService, playerService: PublicPlayerService, searchService: SearchService, utilService: UtilService, configService: ConfigService)
|
||||||||||||||||||||||||||||||||||||
Parameters :
|
addTelemetry | ||||||||||
addTelemetry(id, cdata, extra?, obj?)
|
||||||||||
Parameters :
Returns :
void
|
checkForNestedCourses | ||||||
checkForNestedCourses(groupData, activityData)
|
||||||
Parameters :
Returns :
void
|
downloadCSVFile |
downloadCSVFile()
|
Returns :
void
|
fetchActivity | ||||||
fetchActivity(type: string)
|
||||||
Parameters :
Returns :
void
|
Private fetchActivityOnParamChange |
fetchActivityOnParamChange()
|
Returns :
void
|
flattenDeep | ||||
flattenDeep(contents)
|
||||
Parameters :
Returns :
any
|
getActivityInfo |
getActivityInfo()
|
Returns :
void
|
getContent | ||||||
getContent(groupData, activityData)
|
||||||
Parameters :
Returns :
void
|
getSortedMembers |
getSortedMembers()
|
Returns :
any
|
getUpdatedActivityData | ||||||
getUpdatedActivityData(groupData, activityData)
|
||||||
Parameters :
Returns :
void
|
handleSelectedCourse | ||||
handleSelectedCourse(course)
|
||||
Parameters :
Returns :
void
|
initLayout |
initLayout()
|
Returns :
void
|
isContentTrackable | ||||||
isContentTrackable(content, type)
|
||||||
Parameters :
Returns :
any
|
navigateBack |
navigateBack()
|
Returns :
void
|
navigateToActivityDashboard |
navigateToActivityDashboard()
|
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
processData | ||||||
processData(aggResponse: any)
|
||||||
Parameters :
Returns :
void
|
search | ||||||
search(searchKey: string)
|
||||||
Parameters :
Returns :
void
|
showActivityType |
showActivityType()
|
Returns :
any
|
toggleDropdown |
toggleDropdown()
|
Returns :
void
|
updateArray | ||||
updateArray(course)
|
||||
Parameters :
Returns :
void
|
validateUser | ||||
validateUser(group)
|
||||
Parameters :
Returns :
void
|
activity |
Type : IActivity
|
activityDetails |
Type : any
|
activityId |
Type : string
|
activityType |
Type : string
|
courseHierarchy |
Type : any
|
Public csvExporter |
Type : any
|
dropdownContent |
Default value : true
|
enrolmentCount |
Type : number
|
groupData |
groupId |
Type : string
|
layoutConfiguration |
Type : any
|
leafNodesCount |
Type : number
|
loaderMessage |
Default value : _.get(this.resourceService.messages.fmsg, 'm0087')
|
memberCardConfig |
Type : object
|
Default value : { size: 'small', isBold: false, isSelectable: false, view: 'horizontal' }
|
memberListToShow |
Type : []
|
Default value : []
|
memberListUpdatedOn |
Type : string
|
members |
membersCount |
Type : number
|
nestedCourses |
Type : []
|
Default value : []
|
parentId |
Type : string
|
queryParams |
Public resourceService |
Type : ResourceService
|
searchInputBox |
Decorators :
@ViewChild('searchInputBox')
|
selectedCourse |
showLoader |
Default value : true
|
showSearchResults |
Default value : false
|
telemetryImpression |
Type : IImpressionEventInput
|
unsubscribe$ |
Default value : new Subject<void>()
|
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { UserService, SearchService } from '@sunbird/core';
import { ResourceService, ToasterService, LayoutService, UtilService, ConfigService } from '@sunbird/shared';
import { IImpressionEventInput } from '@sunbird/telemetry';
import * as _ from 'lodash-es';
import { combineLatest, Subject } from 'rxjs';
import { debounceTime, delay, map, takeUntil, tap } from 'rxjs/operators';
import { GroupsService } from './../../../services';
import { IActivity } from '../activity-list/activity-list.component';
import { PublicPlayerService } from '@sunbird/public';
import { ACTIVITY_DASHBOARD, MY_GROUPS, GROUP_DETAILS } from '../../../interfaces/routerLinks';
@Component({
selector: 'app-activity-details',
templateUrl: './activity-details.component.html',
styleUrls: ['./activity-details.component.scss']
})
export class ActivityDetailsComponent implements OnInit, OnDestroy {
@ViewChild('searchInputBox') searchInputBox;
unsubscribe$ = new Subject<void>();
showLoader = true;
queryParams;
activityId: string;
groupId: string;
groupData;
activity: IActivity;
memberListToShow = [];
showSearchResults = false;
memberCardConfig = { size: 'small', isBold: false, isSelectable: false, view: 'horizontal' };
enrolmentCount: number;
leafNodesCount: number;
membersCount: number;
members;
telemetryImpression: IImpressionEventInput;
loaderMessage = _.get(this.resourceService.messages.fmsg, 'm0087');
layoutConfiguration: any;
memberListUpdatedOn: string;
nestedCourses = [];
selectedCourse;
dropdownContent = true;
parentId: string;
public csvExporter: any;
activityType: string;
courseHierarchy: any;
activityDetails: any;
constructor(
public resourceService: ResourceService,
private activatedRoute: ActivatedRoute,
private groupService: GroupsService,
private toasterService: ToasterService,
private userService: UserService,
private router: Router,
private layoutService: LayoutService,
private playerService: PublicPlayerService,
private searchService: SearchService,
private utilService: UtilService,
private configService: ConfigService,
) { }
ngOnInit() {
this.initLayout();
this.fetchActivityOnParamChange();
this.telemetryImpression = this.groupService.getImpressionObject(this.activatedRoute.snapshot, this.router.url);
}
initLayout() {
this.layoutConfiguration = this.layoutService.initlayoutConfig();
this.layoutService.switchableLayout().
pipe(takeUntil(this.unsubscribe$)).subscribe(layoutConfig => {
if (layoutConfig != null) {
this.layoutConfiguration = layoutConfig.layout;
}
});
}
private fetchActivityOnParamChange() {
combineLatest([this.activatedRoute.params, this.activatedRoute.queryParams])
.pipe(debounceTime(5), // to sync params and queryParams events
delay(10), // to trigger page exit telemetry event
tap(data => {
this.showLoader = true;
}),
map((result) => ({ params: { groupId: result[0].groupId, activityId: result[0].activityId }, queryParams: result[1] })),
takeUntil(this.unsubscribe$))
.subscribe(({ params, queryParams }) => {
this.queryParams = { ...queryParams };
this.groupId = params.groupId;
this.activityId = params.activityId;
this.activityType = _.get(this.queryParams, 'primaryCategory') || 'Course';
this.fetchActivity(this.activityType);
});
}
validateUser(group) {
const user = _.find(_.get(group, 'members'), (m) => _.get(m, 'userId') === this.userService.userid);
/* istanbul ignore else */
if (!user || _.get(user, 'role') === 'member' || _.get(user, 'status') === 'inactive' || _.get(group, 'status') === 'inactive') {
this.toasterService.warning(this.resourceService.messages.emsg.noAdminRoleActivity);
this.groupService.goBack();
}
}
fetchActivity(type: string) {
this.showLoader = true;
const activityData = { id: this.activityId, type };
this.groupService.getGroupById(this.groupId, true, true).subscribe(res => {
this.validateUser(res);
this.groupData = res;
this.getActivityInfo();
if (_.get(this.queryParams, 'mimeType') === this.configService.appConfig.PLAYER_CONFIG.MIME_TYPE.collection) {
this.checkForNestedCourses(res, activityData);
} else {
this.getContent(res, activityData);
}
}, err => {
this.navigateBack();
});
}
navigateBack() {
this.showLoader = false;
this.toasterService.error(this.resourceService.messages.emsg.m0005);
this.groupService.goBack();
}
search(searchKey: string) {
searchKey = _.toLower(searchKey);
if (searchKey.trim().length) {
this.showSearchResults = true;
this.memberListToShow = this.members.filter(item => _.toLower(item.title).includes(searchKey));
this.addTelemetry('activity-dashboard-member-search', [], { query: searchKey });
} else {
this.showSearchResults = false;
this.memberListToShow = _.cloneDeep(this.members);
}
}
processData(aggResponse: any) {
const enrolmentInfo = _.find(aggResponse.activity.agg, { metric: 'enrolmentCount' });
this.leafNodesCount = _.get(this.selectedCourse, 'leafNodesCount') || 0;
this.enrolmentCount = _.get(enrolmentInfo, 'value');
this.membersCount = _.get(aggResponse, 'members.length');
this.memberListUpdatedOn = _.max(_.get(aggResponse, 'activity.agg').map(agg => agg.lastUpdatedOn));
this.members = aggResponse.members.map((item, index) => {
/* istanbul ignore else */
if (_.get(item, 'status') === 'active') {
const completedCount = _.get(_.find(item.agg, { metric: 'completedCount' }), 'value');
const userProgress = {
title: _.get(item, 'userId') === this.userService.userid ?
`${_.get(item, 'name')}(${this.resourceService.frmelmnts.lbl.you})` : _.get(item, 'name'),
identifier: _.get(item, 'userId'),
initial: _.get(item, 'name[0]'),
indexOfMember: index
};
if (this.isContentTrackable(this.activity, _.get(this.activity, 'contentType'))) {
const progress = completedCount ? _.toString(Math.round((completedCount / this.leafNodesCount) * 100)) || '0' : '0';
userProgress['progress'] = progress >= 100 ? '100' : progress;
}
this.showLoader = false;
return userProgress;
}
});
this.memberListToShow = this.getSortedMembers();
}
getSortedMembers() {
// sorting by name - alpha order
const sortedMembers = this.members.sort((a, b) =>
((a.title).toLowerCase() > (b.title).toLowerCase()) ? 1 : (((b.title).toLowerCase() > (a.title).toLowerCase()) ? -1 : 0));
// const sortMembersByProgress = [];
// const sortMembersByName = [];
// _.map(_.cloneDeep(this.members), member => {
// if (_.get(member, 'identifier') !== this.userService.userid) {
// _.get(member, 'progress') > 0 ? sortMembersByProgress.push(member) : sortMembersByName.push(member);
// }
// });
// const currentUser = _.find(this.members, {identifier: this.userService.userid});
// const sortedMembers = _.sortBy(sortMembersByProgress, 'progress', 'dsc').concat(_.sortBy(sortMembersByName, 'title', 'asc'));
// sortedMembers.unshift(currentUser);
return sortedMembers || [];
}
getActivityInfo() {
const activityData = _.find(this.groupData.activities, ['id', this.activityId]);
this.activity = _.get(activityData, 'activityInfo');
}
addTelemetry(id, cdata, extra?, obj?) {
this.groupService.addTelemetry({ id, extra }, this.activatedRoute.snapshot, cdata, this.groupId, obj);
}
/**
* @description - To get the course hierarchy Data
* @param {} groupData
* @param {} activityData
*/
checkForNestedCourses(groupData, activityData) {
this.playerService.getCollectionHierarchy(this.activityId, {})
.pipe(takeUntil(this.unsubscribe$))
.subscribe((data) => {
this.courseHierarchy = data.result.content;
this.parentId = _.get(this.courseHierarchy, 'identifier');
this.leafNodesCount = this.courseHierarchy.leafNodesCount;
this.updateArray(this.courseHierarchy);
this.getUpdatedActivityData(groupData, activityData);
const childCourses = this.flattenDeep(this.courseHierarchy.children).filter(c => c.contentType === 'Course');
if (childCourses.length > 0) {
childCourses.map((course) => {
this.updateArray(course);
});
}
this.showLoader = false;
}, err => {
this.toasterService.error(this.resourceService.messages.fmsg.m0051);
this.navigateBack();
});
}
/**
* @description - get updated activity progress data
* @param {} groupData
* @param {} activityData
*/
getUpdatedActivityData(groupData, activityData) {
this.showLoader = true;
this.groupService.getActivity(this.groupId, activityData, groupData, this.leafNodesCount).subscribe(data => {
this.processData(data);
this.showLoader = false;
this.activityDetails = data;
});
}
getContent(groupData, activityData) {
this.playerService.getContent(this.activityId, {})
.pipe(takeUntil(this.unsubscribe$))
.subscribe((data) => {
const courseHierarchy = data.result.content;
this.leafNodesCount = _.get(courseHierarchy, 'leafNodesCount') || 0;
this.updateArray(courseHierarchy);
this.getUpdatedActivityData(groupData, activityData);
this.showLoader = false;
}, err => {
this.toasterService.error(this.resourceService.messages.fmsg.m0051);
this.navigateBack();
});
}
updateArray(course) {
this.nestedCourses.push({
identifier: _.get(course, 'identifier'),
name: _.get(course, 'name'),
leafNodesCount: _.get(course, 'leafNodesCount') || 0,
pkgVersion: _.get(course, 'pkgVersion') ? `${_.get(course, 'pkgVersion')}` : '1.0',
primaryCategory: _.get(course, 'primaryCategory')
});
this.selectedCourse = this.nestedCourses[0];
}
flattenDeep(contents) {
if (contents) {
return contents.reduce((acc, val) => {
if (val.children) {
acc.push(val);
return acc.concat(this.flattenDeep(val.children));
} else {
return acc.concat(val);
}
}, []);
}
return [];
}
handleSelectedCourse(course) {
this.searchInputBox.nativeElement.value = '';
this.selectedCourse = course;
this.toggleDropdown();
const activityData = { id: this.selectedCourse.identifier, type: 'Course' };
this.groupService.getActivity(this.groupId, activityData, this.groupData)
.subscribe((data) => {
this.showLoader = false;
this.processData(data);
}, error => {
this.showLoader = false;
this.toasterService.error(this.resourceService.messages.emsg.m0005);
this.groupService.goBack();
});
}
toggleDropdown() {
this.dropdownContent = !this.dropdownContent;
}
isContentTrackable(content, type) {
return this.searchService.isContentTrackable(content, type);
}
showActivityType() {
return _.lowerCase(_.get(this.queryParams, 'title'));
}
downloadCSVFile() {
this.addTelemetry('download-csv', [], {},
{ id: _.get(this.selectedCourse, 'identifier'), type: _.get(this.selectedCourse, 'primaryCategory'), ver: _.get(this.selectedCourse, 'pkgVersion') });
const data = _.map(this.memberListToShow, member => {
const name = _.get(member, 'title');
return {
courseName: _.get(this.selectedCourse, 'name'),
memberName: name.replace('(You)', ''),
progress: _.get(member, 'progress') + '%'
};
});
this.utilService.downloadCSV(this.selectedCourse, data);
}
/**
* @description - navigate to activity dashboard page
*/
navigateToActivityDashboard() {
this.addTelemetry('activity-detail', [{ id: _.get(this.activity, 'identifier'), type: _.get(this.activity, 'resourceType') }]);
this.router.navigate([`${MY_GROUPS}/${GROUP_DETAILS}`, _.get(this.groupData, 'id'), `${ACTIVITY_DASHBOARD}`, _.get(this.activity, 'identifier')],
{
state: {
hierarchyData: this.courseHierarchy,
activity: this.activityDetails,
memberListUpdatedOn: this.memberListUpdatedOn
}
});
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
<app-landing-section [textToDisplay]="" [layoutConfiguration]="layoutConfiguration" [noTitle]="true">
</app-landing-section>
<div [ngClass]="layoutConfiguration ? 'sbt-center-container sbt-back-header sbt-mygroups-details relative9' : ''">
<app-back-button></app-back-button>
<div class="ui container py-16" *ngIf="showLoader">
<app-loader [data]='loaderMessage'></app-loader>
</div>
<div *ngIf="!showLoader" [appTelemetryImpression]="telemetryImpression">
<div class="content-header">
<div class="ui container py-16">
<div class="content-header__content">
<div class="d-flex flex-basis-1 mr-32 min-w-0 content-header__content__title">
<div class="content-header__img mr-8">
<img [src]="activity?.appIcon || './assets/images/book.png'" alt="" />
</div>
<div class="d-flex flex-dc">
<div class="content-header__title font-weight-bold ellipsis text-left" role="heading" aria-level="2">{{activity?.name}}</div>
<div class="d-flex flex-ai-center content-header__info mt-4">
<span>{{activity?.subject?.join(', ')}}</span>
<span class="dot-divider" *ngIf="activity?.subject?.length"></span>
<span>{{enrolmentCount}}/{{membersCount}} {{resourceService?.frmelmnts?.lbl?.started}}</span>
</div>
</div>
</div>
<div class="d-flex flex-ai-end flex-w-wrap content-header__buttons">
<!--Activity Dashboard-->
<button tabindex="0" (click)="navigateToActivityDashboard()"
[disabled]="!isContentTrackable(activity, activity?.contentType)"
[ngClass]="{'dt-disabled-btn': !isContentTrackable(activity, activity?.contentType)}"
class="sb-btn sb-btn-normal sb-btn-link sb-left-icon-btn ml-8 sb-btn-icon-fix disabled sb-btn-link-primary sb-activity-dashboard-btn"
title="Activity Dashboard">
<img src="assets/images/Activity_icon.svg" width="20px" class="mr-8" alt="Activity Dashboard">
Activity Dashboard
</button>
<div class="hide">
<button class="sb-btn sb-btn-outline-secondary sb-btn-normal mr-8"></button>
<div class="kabab-menu"></div>
</div>
</div>
</div>
</div>
</div>
<div class="ui container mt-24">
<div class="sb-g mt-24 sbt-sb-g">
<div class="sb-g-col-xs-12 sb-g-col-md-5 sb-g-col-lg-5 sb-g-col-xxxl-4">
<div class="nested-group-container flex-dc d-flex">
<div class="nested-group-content">
<div class="sb-bg-color-primary nested-course-area" *ngIf="nestedCourses.length > 1">
<div class="d-flex flex-ai-center flex-jc-space-between py-16 px-24" tabindex="0"
(click)="toggleDropdown();addTelemetry('course-dropdown', [])">
<div class="fsmall">{{selectedCourse?.name}}</div>
<div class="ml-8" [ngClass]="{'open': dropdownContent}">
<!-- <img src="assets/images/arrow-dropdown.svg"> -->
<button type="button"
class="sb-btn sb-btn-normal sb-btn-outline-primary br-24 btn-border">{{resourceService?.frmelmnts?.lbl?.change}}</button>
</div>
</div>
</div>
<div class="dropdown-content p-24" [hidden]="dropdownContent">
<div class="members-group-list d-flex">
<div class="members-group-list-item d-flex flex-dc w-100">
<div class="member-name" tabindex="0"
(click)="handleSelectedCourse(course);addTelemetry('select-course', [{id: course?.identifier, type: 'SelectedDropdown'}], {selectedCourse: {id: course?.identifier }})"
*ngFor="let course of nestedCourses">
<span class='fnormal'
[ngClass]="{'font-weight-bold': course?.identifier === selectedCourse?.identifier }">{{course?.name}}</span>
</div>
</div>
</div>
</div>
</div>
<div class="px-16 pt-16">
<div *ngIf="!isContentTrackable(activity, activity?.contentType)" class="pb-16"><span
class="note-color">{{resourceService?.frmelmnts?.lbl?.note}}</span>
{{resourceService?.frmelmnts.lbl.noProgress | interpolate:'{type}': (showActivityType())}}</div>
<div class="sb-search-box no-btn">
<div class="input-div relative">
<i class="search icon" aria-hidden="true"></i>
<input class="sb-search-input" type="text" #searchInputBox (input)="search(searchInputBox.value)"
[placeholder]="resourceService?.frmelmnts?.lbl?.searchWithinGroup" aria-label="search box" />
</div>
<button class="sb-btn sb-btn-normal"
type="button">{{ resourceService?.frmelmnts?.lbl?.search }}</button>
</div>
</div>
<div class="d-flex flex-ai-center px-16 py-8 flex-jc-space-between"
*ngIf="isContentTrackable(activity, activity?.contentType)">
<div *ngIf="parentId !== selectedCourse?.identifier"></div>
<span class="fxsmall"> {{ resourceService?.frmelmnts?.lbl?.LastUpdated }}:
{{memberListUpdatedOn | date:'MMM d, y, h:mm a'}} </span>
<div class="cursor-pointer" *ngIf="parentId === selectedCourse?.identifier" tabindex="0"
(click)="downloadCSVFile()">
<i class="download icon sb-color-primary"></i>
<span class="download-text">{{resourceService?.frmelmnts?.lbl?.downloadAsCSV}}</span>
</div>
</div>
<div class="px-24">
</div>
<div class="d-flex flex-dc p-16">
<div *ngFor="let member of memberListToShow" class="relative">
<sb-member-card [title]="member?.title" [identifier]="member?.identifier" [config]="memberCardConfig"
[progressDisplay]="member?.progress" [indexOfMember]="member?.indexOfMember"
[initial]="member?.initial">
</sb-member-card>
</div>
</div>
<div class="member-no-result p-24" *ngIf="showSearchResults && !memberListToShow?.length">
<div class="d-flex flex-dc flex-jc-center flex-ai-center text-center">
<img width="197" src="./assets/images/add-member@2x.svg" alt="">
<div class="mt-16 no-result-text">
{{ resourceService?.frmelmnts?.lbl?.NoSerchGroupMemberResults }}
</div>
<div class="my-8 no-result-desc hide">
<div>{{ resourceService?.frmelmnts?.lbl?.checkYourSpelling }}</div>
<div>{{ resourceService?.frmelmnts?.lbl?.similarButDiffName }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
./activity-details.component.scss
@use "@project-sunbird/sb-styles/assets/mixins/mixins" as *;
@use 'pages/content-header' as *;
.member-no-result {
.no-result-text {
font-size: calculateRem(14px);
}
.no-result-desc {
font-size: calculateRem(12px);
color: var(--gray-200);
}
}
//nested group css
.nested-group-content {
border-radius: calculateRem(8px);
position: relative;
.nested-course-area {
border-bottom: calculateRem(1px) solid var(--gray-100);
color: var(--white);
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
}
.dropdown-content {
position: absolute;
z-index: 999;
background: var(--white);
width: 100%;
box-shadow: 0 0.1875rem 0.3125rem 0.25rem rgba(0, 0, 0, 0.05);
.members-group-list {
cursor: pointer;
width: 100%;
span {
font-size: calculateRem(16px);
padding-left: calculateRem(8px);
}
.members-group-list-item {
position: relative;
.member-name {
color: var(--gray-800);
font-size: calculateRem(16px);
letter-spacing: 0;
line-height:normal; //20px
flex: 1;
text-align: left;
margin-bottom: calculateRem(24px);
&:last-child {
margin-bottom:0px;
}
}
}
}
}
}
.open {
img {
transform: rotate(180deg);
}
}
.btn-border{
border-radius:calculateRem(24px);
}
.note-color {
color:var(--red-100);
}
.download-text{
font-size:calculateRem(12px);
color:var(--primary-400);
font-weight: bold;
}
.dt-disabled-btn:disabled, .disabled[disabled]{
background-color: var(--gray-100)!important;
border: .0625rem solid!important;
color: var(--gray-300);
pointer-events: none !important;
opacity: 0.95 !important;
}