File

src/app/child-dev-project/attendance/attendance.service.ts

Index

Methods

Constructor

constructor(entityMapper: EntityMapperService, dbIndexing: DatabaseIndexingService, childrenService: ChildrenService)
Parameters :
Name Type Optional
entityMapper EntityMapperService No
dbIndexing DatabaseIndexingService No
childrenService ChildrenService No

Methods

Async createEventForActivity
createEventForActivity(activity: RecurringActivity, date: Date)
Parameters :
Name Type Optional
activity RecurringActivity No
date Date No
Returns : Promise<EventNote>
Async getActivitiesForChild
getActivitiesForChild(childId: string)
Parameters :
Name Type Optional
childId string No
Async getActivityAttendances
getActivityAttendances(activity: RecurringActivity, sinceDate?: Date)

Load and calculate activity attendance records.

Parameters :
Name Type Optional Description
activity RecurringActivity No

To activity for which records are loaded.

sinceDate Date Yes

(Optional) date starting from which events should be considered. Events before this are ignored to improve performance.

Async getAllActivityAttendancesForPeriod
getAllActivityAttendancesForPeriod(from: Date, until: Date)
Parameters :
Name Type Optional
from Date No
until Date No
Async getEventsForActivity
getEventsForActivity(activityId: string, sinceDate?: Date)

Load events related to the given recurring activity.

Parameters :
Name Type Optional Description
activityId string No

The reference activity the events should relate to.

sinceDate Date Yes

(Optional) date starting from which events should be considered. Events before this are ignored to improve performance.

Returns : Promise<EventNote[]>
Async getEventsOnDate
getEventsOnDate(startDate: Date, endDate: Date)

Return all events on the given date or date range.

Parameters :
Name Type Optional Default value Description
startDate Date No

The date (or start date of a range)

endDate Date No startDate

(Optional) end date of the period to be queried; if not given, defaults to the start date

Returns : Promise<EventNote[]>
Async getEventsWithUpdatedParticipants
getEventsWithUpdatedParticipants(date: Date)
Parameters :
Name Type Optional
date Date No
Returns : unknown
import { Injectable } from "@angular/core";
import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service";
import moment from "moment";
import { RecurringActivity } from "./model/recurring-activity";
import { ActivityAttendance } from "./model/activity-attendance";
import { groupBy } from "../../utils/utils";
import { DatabaseIndexingService } from "../../core/entity/database-indexing/database-indexing.service";
import { EventNote } from "./model/event-note";
import { ChildrenService } from "../children/children.service";

@Injectable({
  providedIn: "root",
})
export class AttendanceService {
  constructor(
    private entityMapper: EntityMapperService,
    private dbIndexing: DatabaseIndexingService,
    private childrenService: ChildrenService,
  ) {
    this.createIndices();
  }

  private createIndices() {
    this.createEventsIndex();
    this.createRecurringActivitiesIndex();
  }

  private createEventsIndex(): Promise<void> {
    const designDoc = {
      _id: "_design/events_index",
      views: {
        by_date: {
          map: `(doc) => {
            if (doc._id.startsWith("${EventNote.ENTITY_TYPE}")) {
              if (doc.date && doc.date.length === 10) {
                emit(doc.date);
              } else {
                var d = new Date(doc.date || null);
                var dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
                emit(dString);
              }
            }
          }`,
        },
        // TODO: remove this and use general Note's relatedEntities index?
        by_activity: {
          map: `(doc) => {
            if (doc._id.startsWith("${EventNote.ENTITY_TYPE}") && doc.relatesTo) {
              var dString;
              if (doc.date && doc.date.length === 10) {
                dString = doc.date;
              } else {            
                var d = new Date(doc.date || null);
                dString = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
              }
              emit(doc.relatesTo + "_" + dString);
            }
          }`,
        },
      },
    };

    return this.dbIndexing.createIndex(designDoc);
  }

  private createRecurringActivitiesIndex(): Promise<void> {
    const designDoc = {
      _id: "_design/activities_index",
      views: {
        by_participant: {
          map: `(doc) => {
            if (doc._id.startsWith("${RecurringActivity.ENTITY_TYPE}")) {
              for (var p of (doc.participants || [])) {
                emit(p);
              }
            }
          }`,
        },
        by_school: {
          map: `(doc) => {
            if (doc._id.startsWith("${RecurringActivity.ENTITY_TYPE}")) {
              for (var g of (doc.linkedGroups || [])) {
                emit(g);
              }
            }
          }`,
        },
      },
    };

    return this.dbIndexing.createIndex(designDoc);
  }

  /**
   * Return all events on the given date or date range.
   * @param startDate The date (or start date of a range)
   * @param endDate (Optional) end date of the period to be queried; if not given, defaults to the start date
   */
  async getEventsOnDate(
    startDate: Date,
    endDate: Date = startDate,
  ): Promise<EventNote[]> {
    const start = moment(startDate);
    const end = moment(endDate);

    const eventNotes = this.dbIndexing.queryIndexDocsRange(
      EventNote,
      "events_index/by_date",
      start.format("YYYY-MM-DD"),
      end.format("YYYY-MM-DD"),
    );

    const relevantNormalNotes = this.childrenService
      .getNotesInTimespan(start, end)
      .then((notes) => notes.filter((n) => n.category?.isMeeting));

    const allResults = await Promise.all([eventNotes, relevantNormalNotes]);
    return allResults[0].concat(allResults[1]);
  }

  async getEventsWithUpdatedParticipants(date: Date) {
    const events = await this.getEventsOnDate(date, date);
    for (const event of events) {
      const participants = await this.loadParticipantsOfGroups(
        event.schools,
        date,
      );
      for (const newParticipant of participants) {
        event.addChild(newParticipant);
      }
    }
    return events;
  }

  /**
   * Load events related to the given recurring activity.
   * @param activityId The reference activity the events should relate to.
   * @param sinceDate (Optional) date starting from which events should be considered. Events before this are ignored to improve performance.
   */
  async getEventsForActivity(
    activityId: string,
    sinceDate?: Date,
  ): Promise<EventNote[]> {
    if (!activityId.startsWith(RecurringActivity.ENTITY_TYPE)) {
      activityId = RecurringActivity.ENTITY_TYPE + ":" + activityId;
    }

    let dateLimit = "";
    if (sinceDate) {
      dateLimit =
        "_" +
        sinceDate.getFullYear() +
        "-" +
        String(sinceDate.getMonth() + 1).padStart(2, "0") +
        "-" +
        String(sinceDate.getDate()).padStart(2, "0");
    }

    return this.dbIndexing.queryIndexDocsRange(
      EventNote,
      "events_index/by_activity",
      activityId + dateLimit,
      activityId,
    );
  }

  /**
   * Load and calculate activity attendance records.
   * @param activity To activity for which records are loaded.
   * @param sinceDate (Optional) date starting from which events should be considered. Events before this are ignored to improve performance.
   */
  async getActivityAttendances(
    activity: RecurringActivity,
    sinceDate?: Date,
  ): Promise<ActivityAttendance[]> {
    const periods = new Map<number, ActivityAttendance>();

    function getOrCreateAttendancePeriod(event) {
      const month = new Date(event.date.getFullYear(), event.date.getMonth());
      let attMonth = periods.get(month.getTime());
      if (!attMonth) {
        attMonth = ActivityAttendance.create(month);
        attMonth.periodTo = moment(month).endOf("month").toDate();
        attMonth.activity = activity;
        periods.set(month.getTime(), attMonth);
      }
      return attMonth;
    }

    const events = await this.getEventsForActivity(activity.getId(), sinceDate);

    for (const event of events) {
      const record = getOrCreateAttendancePeriod(event);
      record.events.push(event);
    }

    return Array.from(periods.values()).sort(
      (a, b) => a.periodFrom.getTime() - b.periodFrom.getTime(),
    );
  }

  async getAllActivityAttendancesForPeriod(
    from: Date,
    until: Date,
  ): Promise<ActivityAttendance[]> {
    const matchingEvents = await this.getEventsOnDate(from, until);
    const groupedEvents = groupBy(matchingEvents, "relatesTo");

    const records = [];
    for (const [activityId, activityEvents] of groupedEvents) {
      const activityRecord = ActivityAttendance.create(from, activityEvents);
      activityRecord.periodTo = until;
      if (activityId) {
        activityRecord.activity = await this.entityMapper
          .load<RecurringActivity>(RecurringActivity, activityId)
          .catch(() => undefined);
      }

      records.push(activityRecord);
    }

    return records;
  }

  async getActivitiesForChild(childId: string): Promise<RecurringActivity[]> {
    const activities = await this.dbIndexing.queryIndexDocs(
      RecurringActivity,
      "activities_index/by_participant",
      childId,
    );

    const visitedSchools =
      await this.childrenService.queryActiveRelationsOf(childId);
    for (const currentRelation of visitedSchools) {
      const activitiesThroughRelation = await this.dbIndexing.queryIndexDocs(
        RecurringActivity,
        "activities_index/by_school",
        currentRelation.schoolId,
      );
      for (const activityThroughRelation of activitiesThroughRelation) {
        if (
          !activities.some((a) => a.getId() === activityThroughRelation.getId())
        ) {
          activities.push(activityThroughRelation);
        }
      }
    }

    return activities;
  }

  async createEventForActivity(
    activity: RecurringActivity,
    date: Date,
  ): Promise<EventNote> {
    const instance = new EventNote();
    instance.date = date;
    instance.subject = activity.title;
    instance.children = await this.getActiveParticipantsOfActivity(
      activity,
      date,
    );
    instance.schools = activity.linkedGroups;
    instance.relatesTo = activity.getId();
    instance.category = activity.type;
    return instance;
  }

  private async getActiveParticipantsOfActivity(
    activity: RecurringActivity,
    date: Date,
  ): Promise<string[]> {
    const schoolParticipants = await this.loadParticipantsOfGroups(
      activity.linkedGroups,
      date,
    );

    return [
      ...new Set(activity.participants.concat(...schoolParticipants)), //  remove duplicates
    ].filter((p) => !activity.excludedParticipants.includes(p));
  }

  /**
   * Load all participants' ids for the given list of groups
   * @param linkedGroups
   * @param date on which the participants should be part of the group
   */
  private async loadParticipantsOfGroups(
    linkedGroups: string[],
    date: Date,
  ): Promise<string[]> {
    const childIdPromises = linkedGroups.map((groupId) =>
      this.childrenService
        .queryActiveRelationsOf(groupId, date)
        .then((relations) =>
          relations.map((r) => r.childId).filter((id) => !!id),
        ),
    );
    const allParticipants = await Promise.all(childIdPromises);
    // flatten and remove duplicates
    return Array.from(new Set([].concat(...allParticipants)));
  }
}

results matching ""

    No results matching ""