import { Injectable , Optional } from "@angular/core" ; import { from , Observable , Subject } from "rxjs" ; import { Child } from "./model/child" ; import { EntityMapperService } from "../../core/entity/entity-mapper.service" ; import { AttendanceMonth } from "../attendance/model/attendance-month" ; import { Note } from "../notes/model/note" ; import { EducationalMaterial } from "../educational-material/model/educational-material" ; import { Aser } from "../aser/model/aser" ; import { ChildSchoolRelation } from "./model/childSchoolRelation" ; import { HealthCheck } from "../health-checkup/model/health-check" ; import { EntitySchemaService } from "../../core/entity/schema/entity-schema.service" ; import { ChildPhotoService } from "./child-photo-service/child-photo.service" ; import moment , { Moment } from "moment" ; import { LoggingService } from "../../core/logging/logging.service" ; import { DatabaseIndexingService } from "../../core/entity/database-indexing/database-indexing.service" ; import { QueryOptions } from "../../core/database/database" ; @ Injectable ( ) export class ChildrenService { constructor ( private entityMapper , private entitySchemaService , private dbIndexing , @ Optional ( ) childPhotoService , @ Optional ( ) private logger ) { this . createDatabaseIndices ( ) ; } public createDatabaseIndices ( ) { this . createAttendanceAnalysisIndex ( ) ; this . createNotesIndex ( ) ; this . createAttendancesIndex ( ) ; this . createChildSchoolRelationIndex ( ) ; } /**
     * returns an observable which retrieves children from the database and loads their pictures
     */ getChildren ( ) { const results = new Subject < Child [ ] > ( ) ; this . entityMapper . loadType < Child > ( Child ) . then ( async ( loadedChildren ) => { results . next ( loadedChildren ) ; for ( const loadedChild of loadedChildren ) { const childCurrentSchoolInfo = await this . getCurrentSchoolInfoForChild ( loadedChild . getId ( ) ) ; await this . migrateToNewChildSchoolRelationModel ( loadedChild , childCurrentSchoolInfo ) ; loadedChild . schoolClass = childCurrentSchoolInfo . schoolClass ; loadedChild . schoolId = childCurrentSchoolInfo . schoolId ; } results . next ( loadedChildren ) ; results . complete ( ) ; } ) ; return results ; } param loadedChild param childCurrentSchoolInfo private async migrateToNewChildSchoolRelationModel ( loadedChild , childCurrentSchoolInfo ) { if ( ! loadedChild . schoolClass && ! loadedChild . schoolId ) { return ; } if ( loadedChild . schoolId !== childCurrentSchoolInfo . schoolId || loadedChild . schoolClass !== childCurrentSchoolInfo . schoolClass ) { const autoMigratedChildSchoolRelation = new ChildSchoolRelation ( ) ; autoMigratedChildSchoolRelation . childId = loadedChild . getId ( ) ; autoMigratedChildSchoolRelation . schoolId = loadedChild . schoolId ; autoMigratedChildSchoolRelation . schoolClass = loadedChild . schoolClass ; await this . entityMapper . save ( autoMigratedChildSchoolRelation ) ; this . logger ?. debug ( "migrated Child entity to new ChildSchoolRelation model " + loadedChild . _id ) ; console . log ( autoMigratedChildSchoolRelation ) ; } await this . entityMapper . save ( loadedChild ) ; } param id getChild ( id ) { const promise = this . entityMapper . load < Child > ( Child , id ) . then ( ( loadedChild ) => { return this . getCurrentSchoolInfoForChild ( id ) . then ( ( currentSchoolInfo ) => { loadedChild . schoolClass = currentSchoolInfo . schoolClass ; loadedChild . schoolId = currentSchoolInfo . schoolId ; return loadedChild ; } ) ; } ) ; return from ( promise ) ; } getAttendances ( ) { return from ( this . entityMapper . loadType < AttendanceMonth > ( AttendanceMonth ) ) ; } getAttendancesOfChild ( childId ) { const promise = this . dbIndexing . queryIndexDocs ( AttendanceMonth , "attendances_index/by_child" , childId ) ; return from ( promise ) ; } getAttendancesOfMonth ( month ) { const monthString = month . getFullYear ( ) . toString ( ) + "-" + ( month . getMonth ( ) + 1 ) . toString ( ) ; const promise = this . dbIndexing . queryIndexDocs ( AttendanceMonth , "attendances_index/by_month" , monthString ) ; return from ( promise ) ; } deprecated private private createAttendancesIndex ( ) { const designDoc = { _id : "_design/attendances_index" , views : { by_child : { map : "(doc) => { " + 'if (!doc._id.startsWith("' + AttendanceMonth . ENTITY_TYPE + '")) return;' + "emit(doc.student); " + "}" , } , by_month : { map : "(doc) => { " + 'if (!doc._id.startsWith("' + AttendanceMonth . ENTITY_TYPE + '")) return;' + "emit(doc.month); " + "}" , } , } , } ; return this . dbIndexing . createIndex ( designDoc ) ; } private createChildSchoolRelationIndex ( ) { const designDoc = { _id : "_design/childSchoolRelations_index" , views : { by_child : { map : `(doc) => {
            if (!doc._id.startsWith("${ ChildSchoolRelation . ENTITY_TYPE }")) return;
            emit(doc.childId);
            }` , } , by_school : { map : `(doc) => {
            if ( (!doc._id.startsWith("${ ChildSchoolRelation . ENTITY_TYPE }")) ||
                (doc.start && isAfterToday(new Date(doc.start))) ||
                (doc.end && !isAfterToday(new Date(doc.end))) ) {
              return;
            }
            emit(doc.schoolId);
            function isAfterToday(date) {
              const tomorrow = new Date();
              tomorrow.setHours(0, 0, 0, 0);
              tomorrow.setDate(tomorrow.getDate() + 1);
              return date >= tomorrow;
            }
            }` , } , by_date : { map : `(doc) => {
            if (!doc._id.startsWith("${ ChildSchoolRelation . ENTITY_TYPE }")) return;
            let timestamp = (new Date(doc.start || '3000-01-01')).getTime().toString().padStart(14, "0");
            emit(doc.childId + '_' + timestamp);
            }` , } , } , } ; return this . dbIndexing . createIndex ( designDoc ) ; } queryLatestRelation ( childId ) { return this . querySortedRelations ( childId , 1 ) . then ( ( children ) => children [ 0 ] ) ; } async querySortedRelations ( childId , limit ? ) { const options = { startkey : childId + "_\uffff" , endkey : childId + "_" , descending : true , include_docs : true , limit : limit , } ; return this . dbIndexing . queryIndexDocs ( ChildSchoolRelation , "childSchoolRelations_index/by_date" , options ) ; } async queryRelationsOf ( queryType , id ) { return this . dbIndexing . queryIndexDocs ( ChildSchoolRelation , "childSchoolRelations_index/by_" + queryType , id ) ; } async queryAttendanceLast3Months ( ) { return this . dbIndexing . queryIndexStats ( "avg_attendance_index/three_months" ) ; } async queryAttendanceLastMonth ( ) { return this . dbIndexing . queryIndexStats ( "avg_attendance_index/last_month" ) ; } private createAttendanceAnalysisIndex ( ) { const designDoc = { _id : "_design/avg_attendance_index" , views : { three_months : { map : this . getAverageAttendanceMapFunction ( ) , reduce : "_stats" , } , last_month : { map : this . getLastAverageAttendanceMapFunction ( ) , reduce : "_stats" , } , } , } ; return this . dbIndexing . createIndex ( designDoc ) ; } private getAverageAttendanceMapFunction ( ) { return ( "(doc) => {" + 'if (!doc._id.startsWith("AttendanceMonth:") ) { return; }' + "if (!isWithinLast3Months(new Date(doc.month), new Date())) { return; }" + "var attendance = (doc.daysAttended / (doc.daysWorking - doc.daysExcused));" + "if (!Number.isNaN(attendance)) { emit(doc.student, attendance); }" + "function isWithinLast3Months(date, now) {" + "  let months;" + "  months = (now.getFullYear() - date.getFullYear()) * 12;" + "  months -= date.getMonth();" + "  months += now.getMonth();" + "  if (months < 0) { return false; }" + "  return months <= 3;" + "}" + "}" ) ; } private getLastAverageAttendanceMapFunction ( ) { return ( "(doc) => {" + 'if (!doc._id.startsWith("AttendanceMonth:")) { return; }' + "if (!isWithinLastMonth(new Date(doc.month), new Date())) { return; }" + "var attendance = (doc.daysAttended / (doc.daysWorking - doc.daysExcused));" + "if (!Number.isNaN(attendance)) { emit(doc.student, attendance); }" + "function isWithinLastMonth(date, now) {" + "  let months;" + "  months = (now.getFullYear() - date.getFullYear()) * 12;" + "  months -= date.getMonth();" + "  months += now.getMonth();" + "  return months === 1;" + "}" + "}" ) ; } getNotesOfChild ( childId ) { const promise = this . dbIndexing . queryIndexDocs ( Note , "notes_index/by_child" , childId ) ; return from ( promise ) ; } param forLastNDays return public async getDaysSinceLastNoteOfEachChild ( forLastNDays = 30 ) { const startDay = moment ( ) . subtract ( forLastNDays , "days" ) ; const notes = await this . getNotesInTimespan ( startDay ) ; const results = new Map ( ) ; const children = await this . entityMapper . loadType ( Child ) ; children . filter ( ( c ) => c . isActive ) . forEach ( ( c ) => results . set ( c . getId ( ) , Number . POSITIVE_INFINITY ) ) ; for ( const note of notes ) { for ( const childId of note . children ) { const daysSinceNote = moment ( ) . diff ( note . date , "days" ) ; const previousValue = results . get ( childId ) ; if ( previousValue > daysSinceNote ) { results . set ( childId , daysSinceNote ) ; } } } return results ; } param startDay param endDay public async getNotesInTimespan ( startDay , endDay = moment ( ) ) { return this . dbIndexing . queryIndexDocsRange ( Note , "notes_index/note_child_by_date" , moment ( startDay ) . format ( "YYYY-MM-DD" ) , moment ( endDay ) . format ( "YYYY-MM-DD" ) ) ; } private createNotesIndex ( ) { const designDoc = { _id : "_design/notes_index" , views : { by_child : { map : "(doc) => { " + 'if (!doc._id.startsWith("' + Note . ENTITY_TYPE + '")) return;' + "if (!Array.isArray(doc.children)) return;" + "doc.children.forEach(childId => emit(childId)); " + "}" , } , note_child_by_date : { map : `(doc) => {
            if (!doc._id.startsWith("${ Note . ENTITY_TYPE }")) return;
            if (!Array.isArray(doc.children) || !doc.date) return;
            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);
          }` , } , } , } ; return this . dbIndexing . createIndex ( designDoc ) ; } async getEducationalMaterialsOfChild ( childId ) { const allMaterials = await this . entityMapper . loadType ( EducationalMaterial ) ; return allMaterials . filter ( ( mat ) => mat . child === childId ) ; } param childId getHealthChecksOfChild ( childId ) { return from ( this . entityMapper . loadType < HealthCheck > ( HealthCheck ) . then ( ( loadedEntities ) => { return loadedEntities . filter ( ( h ) => h . child === childId ) ; } ) ) ; } getAserResultsOfChild ( childId ) { return from ( this . entityMapper . loadType < Aser > ( Aser ) . then ( ( loadedEntities ) => { return loadedEntities . filter ( ( o ) => o . child === childId ) ; } ) ) ; } async getCurrentSchoolInfoForChild ( childId ) { const relations = ( await this . querySortedRelations ( childId ) ) || [ ] ; for ( const rel of relations ) { if ( moment ( rel . start ) . isSameOrBefore ( moment ( ) , "days" ) && moment ( rel . end ) . isSameOrAfter ( moment ( ) , "days" ) ) { return { schoolId : rel . schoolId , schoolClass : rel . schoolClass , } ; } } return { schoolId : null , schoolClass : null , } ; } async getSchoolRelationsFor ( childId ) { return await this . querySortedRelations ( childId ) ; } }