import { Injectable } from '@nestjs/common' import { addDays, endOfDay, format, startOfDay } from 'date-fns' import { zonedTimeToUtc } from 'date-fns-tz' import { isNil, isNumber, meanBy } from 'lodash' import { Appointment, Roles, User } from '../../../models' import { AppointmentRepository, AvailabilityRepository, AvailabilityTimeRepository, COUNT_APPOINTMENTS, MessageRepository, TOTAL_RATINGS, UserRepository, } from '../../../repositories' import { TransferService } from '../../../services/bookings' import { CountryHelper } from '../../../utils' import { AppointmentType, GENERAL_REPORT_ARGS, REPORT_DATA, GeneralAppointmentsReport } from './types' type REPORT_APPOINTMENT_PARAMS = GENERAL_REPORT_ARGS & { nutritionistId?: string roles?: string[] } type REPORT_APPOINTMENTS_DATA = { syntactic: COUNT_APPOINTMENTS analytical: { [key: string]: COUNT_APPOINTMENTS } } @Injectable() export class Booking { constructor( private readonly appointmentRepository: AppointmentRepository, private readonly availabilityRepository: AvailabilityRepository, private readonly availabilityTimeRepository: AvailabilityTimeRepository, private readonly messageRepository: MessageRepository, private readonly userRepository: UserRepository, private readonly transferService: TransferService, ) {} async generateGeneralData({ beginDate, endDate, appointmentType, countryCode, }: GENERAL_REPORT_ARGS): Promise { return { [appointmentType]: await this.makeGeneralDataByRole( beginDate, endDate, countryCode, this.getRolesByAppointmentType(appointmentType), ), } } async generateNutritionistData({ beginDate, endDate, nutritionistId, }: GENERAL_REPORT_ARGS & { nutritionistId: string }): Promise { const params: [Date, Date, string] = [beginDate, endDate, nutritionistId] const appointmentsReport = await this.makeAppointmentReport({ beginDate, endDate, nutritionistId }) const ratings = await this.appointmentRepository.getUserTotalRatings(...params) const appointments = await this.appointmentRepository.findUserAppointmentsWithDuration( beginDate, endDate, nutritionistId, ) const appointmentType = this.getAppointmentTypeByRole(await this.userRepository.getRole(nutritionistId)) return { [appointmentType]: await this.createNutritionistReportBy(params, appointmentsReport, appointments, ratings), } } async transferUserAppointments(userId: string, date: Date): Promise { const user = await this.userRepository.get(userId) const { beginUserDate, endUserDate } = this.convertFromUTCScheduled({ beginDate: date, endDate: date, user, }) const appointments = ( await this.appointmentRepository.findUserAppointments(beginUserDate, endUserDate, user.id) ).filter(appointment => appointment.state === 'scheduled') if (appointments.length <= 0) return await Promise.all(appointments.map(async appointment => this.transferService.transfer(appointment))) await this.availabilityTimeRepository.blockAllAvailabilityTimes(user.id, date) } async transferAllUserAppointments(userId: string): Promise { const dates = await this.availabilityRepository.findScheduledAvailabilityDates(userId) await Promise.all(dates.map(date => this.transferUserAppointments(userId, date))) } private async createNutritionistReportBy( params: [Date, Date, string], appointmentsReport: REPORT_APPOINTMENTS_DATA, appointments: Array, ratings: TOTAL_RATINGS, ) { const feedback = (ratings.total * 100) / appointmentsReport.syntactic.performed || 0 return { appointments: appointmentsReport, ratings: { ...ratings, feedback_rating: feedback, }, consultation_average_duration: this.getAppointmentsConsultationAvgDuration(appointments), available_times: await this.availabilityTimeRepository.getUserAvailability(...params), appointments_delayed: await this.appointmentRepository.getUserCountOfAppointmentsWithDelayedPatient(...params), appointments_by_type: await this.appointmentRepository.getUserTotalAppointmentsByType(...params), pending_chat: await this.messageRepository.getUserPendingMessagesCount(...params), delayed_food_plan: (await this.appointmentRepository.findDelayedsFoodPlan(params[1], params[2])).length, comments: appointments.map(appointment => ({ name: appointment.patient.name, message: appointment.feedback, rating: appointment.rating, })), } } private getAppointmentsConsultationAvgDuration(appointments: Array): number { return ( +meanBy( appointments.filter( appointment => isNumber(appointment.consultation_duration_in_seconds) && appointment.consultation_duration_in_seconds > 0, ), 'consultation_duration_in_seconds', ).toFixed() || 0 ) } private async makeGeneralDataByRole( beginDate: Date, endDate: Date, countryCode: string, roles: string[], ): Promise { const params: [Date, Date, string, string[]] = [beginDate, endDate, countryCode, roles] return { appointments: await this.makeAppointmentReport({ beginDate, endDate, countryCode, roles }), consultation_average_duration: this.getAppointmentsConsultationAvgDuration( await this.appointmentRepository.getGeneralAppointmentsWithDuration(...params), ), ratings: await this.appointmentRepository.getGeneralTotalRatings(...params), available_times: await this.availabilityTimeRepository.getGeneralAvailability(...params), appointments_delayed: await this.appointmentRepository.getCountOfAppointmentsWithDelayedPatient(...params), appointments_by_type: await this.appointmentRepository.getGeneralTotalAppointmentsByType(...params), } } private async makeAppointmentReport(params: REPORT_APPOINTMENT_PARAMS): Promise { const report = await this.getAppointmentCount({ ...params, }) const analytical = {} const syntactic = [] const keys = ['scheduled', 'performed', 'cancelled_by_patient', 'cancelled_by_nutri'] let { beginDate } = params while (beginDate <= params.endDate) { const dayReport = keys.reduce( (previous, current) => ({ ...previous, [current]: report[`${current}_${beginDate.getTime()}`] }), {}, ) analytical[format(beginDate, 'yyyy-MM-dd')] = dayReport syntactic.push(dayReport) beginDate = addDays(beginDate, 1) } return { analytical, syntactic: syntactic.reduce( (syntactic: COUNT_APPOINTMENTS, value: COUNT_APPOINTMENTS) => Object.keys(syntactic).reduce( (result, key) => ({ ...result, [key]: syntactic[key] + value[key] }), syntactic, ), keys.reduce((previous, current) => ({ ...previous, [current]: 0 }), {}), ), } } private getAppointmentCount = async (params: REPORT_APPOINTMENT_PARAMS): Promise => !isNil(params.nutritionistId) ? this.appointmentRepository.getUserAppointmentsCount(params.beginDate, params.endDate, params.nutritionistId) : this.appointmentRepository.getGeneralAppointmentsCount( params.beginDate, params.endDate, params.countryCode, params.roles, ) private convertFromUTCScheduled = ({ beginDate, endDate, user, }: { beginDate: Date endDate: Date user: User }): { beginUserDate: Date; endUserDate: Date } => { const timezone = user.timezone || CountryHelper.getTimezoneByCountry(user.country_code) const beginUserDate = zonedTimeToUtc(startOfDay(beginDate), timezone) const endUserDate = zonedTimeToUtc(endOfDay(endDate), timezone) return { beginUserDate, endUserDate, } } private getRolesByAppointmentType(appointmentType: AppointmentType): Array { switch (appointmentType) { case 'marketplace': return ['marketplace'] case 'n2b': return ['user', 'nutritionist_manager'] default: return [] } } private getAppointmentTypeByRole(role: Roles): AppointmentType { switch (role) { case 'marketplace': return 'marketplace' case 'nutritionist_manager': case 'user': return 'n2b' default: return null } } }