open-learning-exchange/planet

View on GitHub
src/app/manager-dashboard/reports/reports.service.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { Injectable } from '@angular/core';
import { forkJoin } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { CouchService } from '../../shared/couchdb.service';
import { findDocuments } from '../../shared/mangoQueries';
import { dedupeShelfReduce, ageFromBirthDate } from '../../shared/utils';
import { UsersService } from '../../users/users.service';
import { MatDialog } from '@angular/material/dialog';
import { DialogsViewComponent } from '../../shared/dialogs/dialogs-view.component';
import { StateService } from '../../shared/state.service';
import { CoursesService } from '../../courses/courses.service';

interface ActivityRequestObject {
  planetCode?: string;
  tillDate?: number;
  fromMyPlanet?: boolean;
  filterAdmin?: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class ReportsService {

  users: any[] = [];

  constructor(
    private couchService: CouchService,
    private usersService: UsersService,
    private dialog: MatDialog,
    private stateService: StateService,
    private coursesService: CoursesService
  ) {}

  groupBy(array, fields, { sumField = '', maxField = '', uniqueField = '', includeDocs = false } = {}) {
    return array.reduce((group, item) => {
      const currentValue = group.find((groupItem) => fields.every(field => groupItem[field] === item[field]));
      const sumValue = sumField ? (item[sumField] || 0) : 0;
      if (currentValue) {
        currentValue.count = currentValue.count + 1;
        currentValue.sum = currentValue.sum + sumValue;
        currentValue.max = maxField ?
          (currentValue.max[maxField] < item[maxField] ? item : currentValue.max) :
          {};
        currentValue.unique = uniqueField ? currentValue.unique.concat([ item[uniqueField] ]).reduce(dedupeShelfReduce, []) : [];
        currentValue.docs = includeDocs ? [ ...currentValue.docs, item ] : currentValue.docs;
      } else {
        const newEntry = fields.reduce((newObj, field) => {
          newObj[field] = item[field];
          return newObj;
        }, {});
        group.push({ ...newEntry, count: 1, sum: sumValue, max: item, unique: [ item[uniqueField] ], docs: [ item ] });
      }
      return group;
    }, []);
  }

  groupByMonth(array, dateField, uniqueField = '') {
    return this.groupBy(
      array.map(item => {
        const fullDate = new Date(item[dateField]);
        return { ...item, date: new Date(fullDate.getFullYear(), fullDate.getMonth(), 1).valueOf() };
      }),
      [ 'date', 'gender' ],
      { uniqueField }
    );
  }

  groupViewByMonth(rows: { key: any, value: any }[]) {
    return this.groupBy(
      this.appendGender(rows.map(row => ({
        ...row,
        user: row.key.user,
        date: new Date(row.key.year, row.key.month, 1).valueOf(),
        viewCount: row.value.count,
        createdOn: row.key.createdOn
      }))),
      [ 'createdOn', 'date', 'gender' ],
      { sumField: 'viewCount', uniqueField: 'user' }
    );
  }

  selector(planetCode: string, { field = 'createdOn', tillDate, dateField = 'time', fromMyPlanet }: any = { field: 'createdOn' }) {
    return planetCode ?
      findDocuments({
        ...{ [field]: planetCode },
        ...this.timeFilter(dateField, tillDate),
        ...(fromMyPlanet !== undefined ? { androidId: { '$exists': fromMyPlanet } } : {})
      }) :
      undefined;
  }

  getTotalUsers(planetCode: string, local: boolean) {
    const adminName = this.stateService.configuration.adminName.split('@')[0];
    const obs = local ?
      this.usersService.getAllUsers() :
      this.couchService.findAll('child_users', this.selector(planetCode, { field: 'planetCode' }));
    return obs.pipe(map((users: any) => {
      users = users.filter(user => user.name !== 'satellite' && user.name !== adminName);
      this.users = users;
      return this.groupUsers(users);
    }));
  }

  groupUsers(users: any[]) {
    return ({
      count: users.length,
      byGender: users.reduce((usersByGender: any, user: any) => {
        usersByGender[(user.doc || user).gender || 'didNotSpecify'] += 1;
        return usersByGender;
      }, { 'male': 0, 'female': 0, 'didNotSpecify': 0 }),
      byMonth: this.groupByMonth(users, 'joinDate')
    });
  }

  getActivities(db: 'login_activities' | 'resource_activities', view: 'byPlanet' | 'byPlanetRecent' | 'grouped' = 'byPlanet', domain?) {
    return this.couchService.get(`${db}/_design/${db}/_view/${view}?group=true`, { domain });
  }

  getAllActivities(
    db: 'login_activities' | 'resource_activities' | 'course_activities',
    { planetCode, tillDate, fromMyPlanet, filterAdmin }: ActivityRequestObject = {}
  ) {
    const dateField = db === 'login_activities' ? 'loginTime' : 'time';
    return this.couchService.findAll(db, this.selector(planetCode, { tillDate, dateField, fromMyPlanet }))
    .pipe(map((activities: any) => {
      return this.filterAdmin(activities, filterAdmin);
    }));
  }

  groupLoginActivities(loginActivities) {
    return ({
      byUser: this.groupBy(loginActivities, [ 'parentCode', 'createdOn', 'user' ], { maxField: 'loginTime' })
        .filter(loginActivity => loginActivity.user !== '' && loginActivity.user !== undefined).sort((a, b) => b.count - a.count),
      byMonth: this.groupByMonth(this.appendGender(loginActivities), 'loginTime', 'user')
    });
  }

  getRatingInfo({ planetCode, tillDate, fromMyPlanet, filterAdmin }: ActivityRequestObject = {}) {
    return this.couchService.findAll('ratings', this.selector(planetCode, { tillDate, dateField: 'time', fromMyPlanet })).pipe(
      map((ratings: any) => this.filterAdmin(ratings, filterAdmin)));
  }

  groupRatings(ratings) {
    return this.groupBy(ratings, [ 'parentCode', 'createdOn', 'type', 'item', 'title' ], { sumField: 'rate' })
      .filter(rating => rating.title !== '' && rating.title !== undefined)
      .sort((a: any, b: any) => (b.sum / b.count) - (a.sum / a.count)).map((r: any) =>
        ({ ...r, value: Math.round(10 * r.sum / r.count) / 10 }));
  }

  groupDocVisits(activites, type: 'resourceId' | 'courseId') {
    return ({
      byDoc: this.groupBy(activites, [ 'parentCode', 'createdOn', type ], { maxField: 'time' })
        .filter(activity => activity.title !== '' && activity !== undefined),
      byMonth: this.groupByMonth(this.appendGender(activites), 'time')
    });
  }

  getDatabaseCount(db: string) {
    return forkJoin([
      this.couchService.get(db),
      this.couchService.get(db + '/_design_docs')
    ]).pipe(map(([ schema, ddocs ]) => {
      return schema.doc_count - ddocs.total_rows;
    }));
  }

  getChildDatabaseCounts(code: string) {
    return this.couchService.get('child_statistics/' + code);
  }

  getAdminActivities({ planetCode, tillDate, domain }: { planetCode?: string, tillDate?: number, domain?: string }) {
    return this.couchService.findAll('admin_activities', this.selector(planetCode, { tillDate, dateField: 'time' }), { domain })
    .pipe(map(adminActivities => {
      return this.groupBy(adminActivities, [ 'parentCode', 'createdOn', 'type' ], { maxField: 'time' });
    }));
  }

  mostRecentAdminActivities(planet, logins, adminActivities) {
    const adminName = planet.adminName.split('@')[0];
    const findPlanetLog = (item: any) => item.createdOn === planet.code && item.parentCode === planet.parentCode;
    const findAdminActivity = (type: any) => (activity: any) => activity.type === type && findPlanetLog(activity);
    return ({
      lastAdminLogin: logins.find((item: any) => item.user === adminName && findPlanetLog(item)),
      lastUpgrade: adminActivities.find(findAdminActivity('upgrade')),
      lastSync: adminActivities.find(findAdminActivity('sync'))
    });
  }

  appendGender(array) {
    return array.map((item: any) => {
      const user = this.users.find((u: any) => u.name === item.user) || {};
      return ({
        ...item,
        gender: user.gender
      });
    });
  }

  appendAge(array, time) {
    return array.map((item: any) => {
      const user = this.users.find((u: any) => u.name === item.user) || {};
      return ({
        ...item,
        age: ageFromBirthDate(time, user.birthDate)
      });
    });
  }

  timeFilter(field, time) {
    return time !== undefined ? { [field]: { '$gt': time } } : {};
  }

  filterAdmin(records, filter) {
    return filter ? records.filter(rec => this.users.findIndex((u: any) => u.name === rec.user || u.name === rec.user.name) > -1) : records;
  }

  minTime(activities, timeField: string) {
    return activities.reduce((minTime, { [timeField as keyof Object]: time }) => minTime && minTime < time ? minTime : time, undefined);
  }

  planetTypeText(planetType) {
    return planetType === 'nation' ? $localize`Nation` : $localize`Community`;
  }

  viewPlanetDetails(planet) {
    this.dialog.open(DialogsViewComponent, {
      width: '600px',
      autoFocus: false,
      data: {
        allData: planet,
        title: $localize`${this.planetTypeText(planet.planetType)} Details`
      }
    });
  }

  courseProgressReport(parent = false) {
    this.coursesService.requestCourses(parent);
    return forkJoin([
      this.couchService.get('courses_progress/_design/courses_progress/_view/enrollment?group=true'),
      this.couchService.get('courses_progress/_design/courses_progress/_view/completion?group=true'),
      this.couchService.get('courses_progress/_design/courses_progress/_view/steps?group=true'),
      this.coursesService.coursesListener$().pipe(take(1))
    ]).pipe(map(([ { rows: enrollments }, { rows: completions }, { rows: steps }, courses ]) => {
      return {
        courses: courses.map(course => ({
          steps: course.doc.steps.length,
          exams: course.doc.steps.filter(step => step.exam).length,
          _id: course._id
        })),
        enrollments: enrollments.map(({ key, value }) => ({ ...key, time: value.min })),
        completions: completions.filter(({ key, value }) => {
            const course = courses.find(c => c._id === key.courseId);
            return course && value.count === course.doc.steps.length;
          })
          .map(({ key, value }) => ({ ...key, time: value.max, stepCount: value.count })),
        steps: steps.map(({ key, value }) => {
          const course = courses.find(c => c._id === key.courseId);
          return { ...key, time: value.max, title: course ? course.doc.courseTitle : '' };
        })
      };
    }));
  }

  groupStepCompletion(steps: any[]) {
    return ({
      byMonth: this.groupByMonth(this.appendGender(steps), 'time', 'userId')
    });
  }

}