// Uncomment these imports to begin using these cool features! import {AnyObject, Filter} from '@loopback/repository'; import {param, get, HttpErrors} from '@loopback/rest'; import { IAuthUserWithPermissions, MedicineLogStatus, PermissionKey, STATUS_CODE, } from '@sourcefuse-npm/alivio-lib'; import { authenticate, AuthenticationBindings, STRATEGY, } from 'loopback4-authentication'; import {authorize, AuthorizeErrorKeys} from 'loopback4-authorization'; import { DailyCheckIns, DurationRequiredType, Meals, MealType, MostFrequentlyUserdKeywords, MostUsedKeywords, Sentiment, SentimentAnalysis, Sleep, SleepLogs, } from '../models'; import { CheckInUsersService, DailyCheckInsService, DiPatientCaregiverUserService, MoodFactorLogsService, } from '../services'; import {inject} from '@loopback/core'; import moment from 'moment'; import { FoodLog, MedicineLog, MedicineLogService, MoodLogs, PatientService, StartAndEndDate, UsersService, } from '@sourcefuse-npm/patient-facade'; import {Moods} from '@sourcefuse-npm/patient-facade/dist/models/moods.model'; import { AnalysisTypeFormate, Image, JourneyAnalasis, JourneyResponse, MedicineAnalysisList, } from '../models/journey-response.model'; import * as AWS from 'aws-sdk'; import {AWSS3Bindings} from 'loopback4-s3'; import {groupBy} from 'lodash'; const dailyCheckInPath = '/patient/journey/{id}/'; const defaultDateFormat = 'YYYY-MM-DD'; const WeekDays = 7; const percentage = 100; const limit = 5; export class JourneyController { constructor( @inject('services.DailyCheckInsService') public dailyCheckIns: DailyCheckInsService, @inject('services.PatientService') protected patientService: PatientService, @inject('services.MoodFactorLogsService') protected moodFactorLogsService: MoodFactorLogsService, @inject('services.UsersService') protected usersService: UsersService, @inject('services.CheckInUsersService') protected checkInUsersService: CheckInUsersService, @inject('services.MedicineLog') protected medicineLogService: MedicineLogService, @inject('services.DiPatientCaregiverUserService') protected readonly patientCgUserService: DiPatientCaregiverUserService, ) {} private updatePreSignedUrl( imageUrl: string, s3: AWS.S3, bucket: string, ): string { if (!imageUrl) { throw new HttpErrors.NotFound('Image Url not found.'); } if (!process.env.PRE_SIGNED_URL_VALIDITY_IN_MINUTES) { throw new HttpErrors.NotFound( 'Pre-signed url validity time not defined.', ); } const urlValiditityTimeInMinutes = parseInt( process.env.PRE_SIGNED_URL_VALIDITY_IN_MINUTES, ); const secPerMinute = 60; const signedUrlExpireSeconds = secPerMinute * urlValiditityTimeInMinutes; return s3.getSignedUrl('getObject', { Bucket: bucket, Key: imageUrl, Expires: signedUrlExpireSeconds, }); } @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.ViewOwnPatient, PermissionKey.ViewPatient], }) @get(`${dailyCheckInPath}/`, { responses: { [STATUS_CODE.OK]: { description: 'DailyCheckIns model count', content: {'application/json': {schema: JourneyResponse}}, }, }, }) async getCheckInLogs( // eslint-disable-next-line @typescript-eslint/no-shadow @param.path.number('id') id: number, @inject(AuthenticationBindings.CURRENT_USER) currentUser: IAuthUserWithPermissions, @param.query.date('start', {required: true}) start: Date, @param.query.date('end', {required: false}) end: Date, @inject(AWSS3Bindings.AwsS3Provider) s3: AWS.S3, @param.header.string('Authorization') token?: string, @param.query.boolean('isWeeklyFormat', {required: false}) isWeeklyFormat = false, ): Promise { const isAccessAllowed = await this.patientCgUserService.isPatientAccessAllowed( currentUser, Number(id), // eslint-disable-next-line @typescript-eslint/no-explicit-any PermissionKey.UpdateOwnPatient, token, ); if (!isAccessAllowed) { throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess); } const startEndDates = { startDate: new Date(moment(start).startOf('day').toString()), endDate: new Date( moment(end ? end : start) .endOf('day') .toString(), ), }; const filter: Filter = { where: { patientId: id, logDate: {between: [startEndDates.startDate, startEndDates.endDate]}, }, order: ['logDate ASC'], }; const checkins = await this.dailyCheckIns.find(filter, token); const moodLogsIds = checkins.map(m => m.moodLogId); const moodLogs = await this.getMoods(moodLogsIds, token); const foodLogIds = checkins .map(m => m.foodLogId) .flat() .map(e => Number(e)); const foodLogs = await this.getFoods(foodLogIds, id, token); const sleepLogIds = checkins.map(m => m.sleepLogId); const sleepLogs = await this.getSleepLogs(sleepLogIds, token); return this.prepareJourneyResponse( id, checkins, startEndDates, moodLogs, foodLogs, sleepLogs, end, s3, isWeeklyFormat, token, ); // return responseData; } @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.ViewOwnPatient, PermissionKey.ViewPatient], }) @get('/patient/{patientId}/sentiment-analysis', { responses: { [STATUS_CODE.OK]: { description: 'sentiment - analysis', content: {'application/json': {schema: Sentiment}}, }, }, }) async getsentimentAnalysis( @param.path.number('patientId') patientId: number, @param.query.date('start', {required: true}) start: Date, @param.query.date('end', {required: false}) end: Date, @inject(AuthenticationBindings.CURRENT_USER) currentUser: IAuthUserWithPermissions, @param.header.string('Authorization') token?: string, ): Promise { const isAccessAllowed = await this.patientCgUserService.isPatientAccessAllowed( currentUser, Number(patientId), // eslint-disable-next-line @typescript-eslint/no-explicit-any PermissionKey.UpdateOwnPatient, token, ); if (!isAccessAllowed) { throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess); } const startEndDates = { startDate: new Date(moment(start).startOf('day').toString()), endDate: new Date( moment(end ? end : start) .endOf('day') .toString(), ), }; const filter: Filter = { where: { patientId: patientId, logDate: {between: [startEndDates.startDate, startEndDates.endDate]}, }, order: ['logDate ASC'], }; const checkins = await this.dailyCheckIns.find(filter, token); const sentimentAnalysis = await this.calculateSentimentalAnalysis( checkins, token, ); return sentimentAnalysis.sentiment; } @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.ViewOwnPatient, PermissionKey.ViewPatient], }) @get('/patient/sentiment/{patientId}/most-used-word', { responses: { [STATUS_CODE.OK]: { description: 'Finding most-used-word from analysis', content: {'application/json': {schema: MostFrequentlyUserdKeywords}}, }, }, }) async getsentimentMostUsedWords( @param.path.number('patientId') patientId: number, @param.query.date('start', {required: true}) start: Date, @param.query.date('end', {required: false}) end: Date, @inject(AuthenticationBindings.CURRENT_USER) currentUser: IAuthUserWithPermissions, @param.header.string('Authorization') token?: string, ): Promise { const isAccessAllowed = await this.patientCgUserService.isPatientAccessAllowed( currentUser, Number(patientId), // eslint-disable-next-line @typescript-eslint/no-explicit-any PermissionKey.ViewOwnPatient, token, ); if (!isAccessAllowed) { throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess); } const startEndDates = { startDate: new Date(moment(start).startOf('day').toString()), endDate: new Date( moment(end ? end : start) .endOf('day') .toString(), ), }; const filter: Filter = { where: { patientId: patientId, logDate: {between: [startEndDates.startDate, startEndDates.endDate]}, }, order: ['logDate ASC'], }; const checkins = await this.dailyCheckIns.find(filter, token); let sentimentAnalysisTextARray: string[] = checkins .filter(checkin => checkin.comment) .map(checkin => `${checkin.comment}`); sentimentAnalysisTextARray = sentimentAnalysisTextARray.filter(s => s); if (sentimentAnalysisTextARray.length > 0) { const taxonomy = await this.moodFactorLogsService.getTaxonomyBatch( sentimentAnalysisTextARray, token, ); const taxonomyDetailsObj: { [key: string]: { name: string; percentage: number; count: number; }; } = {}; taxonomy.taxonomy.forEach(tax => { tax.forEach(t => { if (taxonomyDetailsObj[t.tag]) { taxonomyDetailsObj[t.tag].count += 1; taxonomyDetailsObj[t.tag].percentage += t.confidence_score; } else { taxonomyDetailsObj[t.tag] = { name: t.tag, count: 1, percentage: t.confidence_score, }; } }); }); let taxonomiesMostUsed = Object.values(taxonomyDetailsObj); taxonomiesMostUsed = taxonomiesMostUsed.map(tax => { tax.percentage = tax.percentage / tax.count; return tax; }); taxonomiesMostUsed = taxonomiesMostUsed.sort((a, b) => a.percentage < b.percentage ? 1 : -1, ); const top5Bytopics = taxonomiesMostUsed.slice(0, limit); const sentiment = await this.moodFactorLogsService.getSentimentBatch( top5Bytopics.map(t => t.name), token, ); const sentimentBYtopic = sentiment.sentiment.map((sen, i) => { return {name: top5Bytopics[i].name, sentiment: sen}; }); return { frequentlyUsed: taxonomiesMostUsed as MostUsedKeywords[], top5SentimentalAnalysis: sentimentBYtopic, } as MostFrequentlyUserdKeywords; } return { frequentlyUsed: [] as MostUsedKeywords[], top5SentimentalAnalysis: [] as {name: string; sentiment: Sentiment}[], } as MostFrequentlyUserdKeywords; } async getMoods(moodLogIds: number[], token?: string) { return moodLogIds.length ? this.patientService.getMoodsLog( { where: { id: {inq: moodLogIds}, }, }, token, ) : []; } async getFoods(foodLogIds: number[], ptainetId: number, token?: string) { return foodLogIds.length ? this.patientService.getFoodLogs(token, ptainetId, { where: { id: {inq: foodLogIds}, }, }) : []; } async getSleepLogs(sleepLogIds: number[], token?: string) { return sleepLogIds.length ? this.moodFactorLogsService.getSleepLog( { where: { id: {inq: sleepLogIds}, }, }, token, ) : []; } async prepareJourneyResponse( patientId: number, checkins: DailyCheckIns[], startEndDates: StartAndEndDate, moodLogs: MoodLogs[], foodLogs: FoodLog[], sleepLog: SleepLogs[], actualEndDate: Date, s3: AWS.S3, isWeeklyFormat = false, token?: string, ): Promise { const daysDiff = this.getDiffDays( startEndDates.startDate, startEndDates.endDate, ); const moods = await this.usersService.getMoods({}, token); // sonarignore:start const configs = await this.usersService.findConfigs( { where: { // eslint-disable-next-line @typescript-eslint/no-explicit-any configKey: 'meals' as any, }, }, token, ); const medLogs: MedicineLog[] = await this.medicineLogService.getMedLog( { where: { patientId: patientId, and: [ { timeToTake: { gte: startEndDates.startDate, }, }, { timeToTake: { lte: startEndDates.endDate, }, }, ], }, include: [{relation: 'prescriptionMedicines'}], }, token, ); // sonarignore:end const food = (configs.length ? configs[0].configValue : {}) as Meals; const sleep = await this.checkInUsersService.getSleep({}, token); if ( (isWeeklyFormat === false && (daysDiff === 0 || daysDiff === WeekDays - 1)) || (daysDiff > 0 && daysDiff < WeekDays - 1) || daysDiff > WeekDays - 1 ) { const moodCountPer = this.calculateMoodLogsPercentage( moods, moodLogs, s3, ); // sonarignore:start // sonarignore:end const foodCountPer = this.calculateSMealLogsPercentage( food.meals, foodLogs, s3, ); const sleepCountPer = this.calculateSleepLogsPercentage( sleep, sleepLog, s3, ); return { mood: { analysisMax: this.getMaxValuePropery(moodCountPer.percenatge), analysisMin: this.getMinValuePropery(moodCountPer.percenatge), completeAnalysis: moodCountPer.completeAnalysis, }, meals: { analysisMax: this.getMaxValuePropery(foodCountPer.percenatge), analysisMin: this.getMinValuePropery(foodCountPer.percenatge), completeAnalysis: foodCountPer.completeAnalysis, }, sleep: { analysisMax: this.getMaxValuePropery(sleepCountPer.percenatge), analysisMin: this.getMinValuePropery(sleepCountPer.percenatge), completeAnalysis: sleepCountPer.completeAnalysis, }, medication: { medicationAdherence: { percentage: this.calculateMedicationAdherence(medLogs), }, missedMedicineList: this.calculateFrequetlyMissedMedication(medLogs), }, } as JourneyResponse; } const moodLogsWithImage = moodLogs.map(ml => { const mood = moods.find(m => m.alertKey === ml.alertKey); const mlImage = Object.assign({}, ml, new Image()); let url; if (mood?.image) { url = this.updatePreSignedUrl( mood.image, s3, `${process.env.MOODS_BUCKET}`, ); } mlImage.imageUrl = url; return mlImage; }); const fooLogsImage = foodLogs.map(fl => { const fd = food.meals.find(m => m.name === fl.alertKey); const foodImage = Object.assign({}, fl, new Image()); let url; if (fd?.image) { url = this.updatePreSignedUrl( fd.image, s3, `${process.env.FOOD_LOG_BUCKET}`, ); } foodImage.imageUrl = url; return foodImage; }); const sleepogsImage = sleepLog.map(sl => { const fd = sleep.find(m => m.alertKey === sl.alertKey); const sleepImage = Object.assign({}, sl, new Image()); let url; if (fd?.image) { url = this.updatePreSignedUrl( fd.image, s3, `${process.env.FOOD_LOG_BUCKET}`, ); } sleepImage.imageUrl = url; return sleepImage; }); return { mood: {logs: moodLogsWithImage} as JourneyAnalasis, meals: {logs: fooLogsImage} as JourneyAnalasis, sleep: {logs: sleepogsImage} as JourneyAnalasis, } as JourneyResponse; } async calculateSentimentalAnalysis( checkins: DailyCheckIns[], token?: string, ) { const sentimentAnalysisText: string = checkins .filter(checkin => checkin.comment) .map(checkin => checkin.comment) .join(' '); const sentimentAnalysis: SentimentAnalysis = sentimentAnalysisText ? await this.moodFactorLogsService.getSentiment( sentimentAnalysisText, token, ) : new SentimentAnalysis(); if (!sentimentAnalysis.sentiment) { sentimentAnalysis.sentiment = new Sentiment(); sentimentAnalysis.sentiment.negative = 0; sentimentAnalysis.sentiment.positive = 0; sentimentAnalysis.sentiment.neutral = 0; } if (sentimentAnalysis.sentiment) { sentimentAnalysis.sentiment.negative = sentimentAnalysis.sentiment.negative * percentage ?? 0; sentimentAnalysis.sentiment.positive = sentimentAnalysis.sentiment.positive * percentage ?? 0; sentimentAnalysis.sentiment.neutral = sentimentAnalysis.sentiment.neutral * percentage ?? 0; } return sentimentAnalysis; } calculateMedicationAdherence(data: MedicineLog[]) { const dataGroupByStatus = groupBy(data, 'status'); const done = dataGroupByStatus[MedicineLogStatus.DONE] ? dataGroupByStatus[MedicineLogStatus.DONE].length : 0; let adherence = 0; if (data && data.length > 0) { adherence = (done / data.length) * percentage; } return adherence; } calculateFrequetlyMissedMedication(data: MedicineLog[]) { const dataGroupByStatus = groupBy(data, 'status'); const missedMedicines = dataGroupByStatus[MedicineLogStatus.MISSED]; const frequentlyMissed = groupBy(missedMedicines, 'medicineId'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const frequentlyMissedList: MedicineAnalysisList[] = []; Object.keys(frequentlyMissed).forEach(key => { const missedItem: AnyObject | undefined = missedMedicines.find( medicine => medicine.medicineId === key, ); frequentlyMissedList.push({ name: missedItem?.prescriptionMedicines.name, foodRestriction: missedItem?.prescriptionMedicines.foodRestriction, frequency: missedItem?.prescriptionMedicines.frequency, count: frequentlyMissed[key].length, } as MedicineAnalysisList); }); return frequentlyMissedList; } getMaxValuePropery(obje: {[key: string]: number}) { return Object.keys(obje).reduce((a: string, b: string) => obje[a] > obje[b] ? a : b, ); } getMinValuePropery(obje: {[key: string]: number}) { return Object.keys(obje).reduce((a: string, b: string) => obje[a] < obje[b] ? a : b, ); } calculateMoodLogsPercentage( moods: Moods[], moodLogs: MoodLogs[], s3: AWS.S3, ) { const moodLoggedCount: {[key: string]: number} = {}; const moodLoggedPer: {[key: string]: number} = {}; const analysys: {[key: string]: AnalysisTypeFormate} = {}; moods.forEach(m => { moodLoggedCount[m.name] = 0; moodLoggedPer[m.name] = 0; analysys[m.name] = {} as AnalysisTypeFormate; analysys[m.name].name = m.name; analysys[m.name].alertKey = m.alertKey; analysys[m.name].count = 0; if (m.image) { analysys[m.name].image = this.updatePreSignedUrl( m.image, s3, `${process.env.MOODS_BUCKET}`, ); } }); moodLogs.forEach(moodLog => { if (analysys.hasOwnProperty(moodLog.mood)) { analysys[moodLog.mood].count += 1; moodLoggedCount[moodLog.mood] = analysys[moodLog.mood].count; } }); for (const moodKey in analysys) { analysys[moodKey].percentage = Number( (analysys[moodKey].count * percentage) / moodLogs.length, ); moodLoggedPer[moodKey] = analysys[moodKey].percentage; } return { count: moodLoggedCount, percenatge: moodLoggedPer, completeAnalysis: Object.values(analysys), }; } calculateSleepLogsPercentage( sleep: Sleep[], sleepLogs: SleepLogs[], s3: AWS.S3, ) { const sleepLoggedCount: {[key: string]: number} = {}; const sleepLoggedPer: {[key: string]: number} = {}; const analysys: {[key: string]: AnalysisTypeFormate} = {}; sleep.forEach(m => { sleepLoggedCount[m.name] = 0; sleepLoggedPer[m.name] = 0; analysys[m.name] = {} as AnalysisTypeFormate; analysys[m.name].name = m.name; analysys[m.name].alertKey = m.alertKey; analysys[m.name].count = 0; if (m.image) { analysys[m.name].image = this.updatePreSignedUrl( m.image, s3, `${process.env.SLEEP_BUCKET}`, ); } }); sleepLogs.forEach(sleepLog => { if (analysys.hasOwnProperty(sleepLog.sleep)) { sleepLoggedCount[sleepLog.sleep] += 1; analysys[sleepLog.sleep].count = sleepLoggedCount[sleepLog.sleep]; } }); for (const sleepKey in sleepLoggedCount) { sleepLoggedPer[sleepKey] = Number( (sleepLoggedCount[sleepKey] * percentage) / sleepLogs.length, ); analysys[sleepKey].percentage = sleepLoggedPer[sleepKey]; } return { count: sleepLoggedCount, percenatge: sleepLoggedPer, completeAnalysis: Object.values(analysys), }; } calculateSMealLogsPercentage( food: MealType[], foodLogs: FoodLog[], s3: AWS.S3, ) { const mealLoggedCount: {[key: string]: number} = {}; const mealLoggedPer: {[key: string]: number} = {}; const analysys: {[key: string]: AnalysisTypeFormate} = {}; food.forEach(m => { mealLoggedCount[m.name] = 0; mealLoggedPer[m.name] = 0; analysys[m.name] = {} as AnalysisTypeFormate; analysys[m.name].name = m.name; analysys[m.name].alertKey = m.type; analysys[m.name].count = 0; if (m.image) { analysys[m.name].image = m.image; } }); foodLogs.forEach(mealLog => { if (mealLog.meal && analysys.hasOwnProperty(mealLog.meal)) { mealLoggedCount[mealLog.meal] += 1; analysys[mealLog.meal].count = mealLoggedCount[mealLog.meal]; } }); for (const mealKey in mealLoggedCount) { mealLoggedPer[mealKey] = Number( (mealLoggedCount[mealKey] * percentage) / foodLogs.length, ); analysys[mealKey].percentage = mealLoggedPer[mealKey]; } return { count: mealLoggedCount, percenatge: mealLoggedPer, completeAnalysis: Object.values(analysys), }; } getDiffDays( startDate: Date | string, endDate: Date | string, type?: DurationRequiredType, ) { const start = moment(new Date(startDate), defaultDateFormat); const end = endDate ? moment(new Date(endDate), defaultDateFormat) : start.clone().add(1, type); return end.diff(start, 'days'); } }