import UsersManager from "../Users/Users"; import CompaniesManager from "../Companies/Companies"; import { NoPermissionMessage } from "../../helpers/NoPermissionMessage"; import { NotificationsManager } from "../Notifications/Notifications"; import { RepeatsAdapterMongoose } from "./Repeats.Adapter.Mongoose"; import { AddRepeatDisabledDateParams, CountRepeatsParams, CreateRepeatsParams, DeleteRepeatsByClientParams, DeleteRepeatsByCompanyParams, DeleteRepeatsByUserParams, DeleteRepeatsParams, FindRepeatsParams, IRepeat, IRepeatsManager, RepeatView, GetPendingRepeatsParams, PendingRepeatView, } from "./Repeats.Interface"; import { EmployeeRoles } from "../Employees/Employees.Interface"; import ServiceList from "../Services/Service"; import ClientsList from "../Clients/Clients"; import EmployeesList from "../Employees/Employee"; import UserList from "../Users/Users"; import dayjs from "dayjs"; import localeData from "dayjs/plugin/localeData"; import "dayjs/locale/es"; import weekday from "dayjs/plugin/weekday"; import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; import { isNull } from "../../helpers/IsNull"; import getAvatar from "../../helpers/getAvatar"; import MetricsList from "../Metrics/Metrics"; import AppointmentList from "../Appointments/Appointments"; import Discounts from "../Discounts/Discounts"; import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface"; dayjs.locale("es"); dayjs.extend(localeData); dayjs.extend(weekday); dayjs.extend(isSameOrAfter); dayjs.extend(isSameOrBefore); class RepeatsManager implements IRepeatsManager { repeats: RepeatsAdapterMongoose; constructor() { this.repeats = new RepeatsAdapterMongoose(); } private numberToTime(decimal: number): string { const hours = Math.floor(decimal); const minutes = Math.round((decimal - hours) * 60); const paddedMinutes = minutes.toString().padStart(2, "0"); return `${hours}:${paddedMinutes}hs`; } public async createRepeat(data: CreateRepeatsParams): Promise { //Primero tengo que verificar que el usuario que intenta registrar un empleado //tenga permisos para hacerlo y que sea un usuario valido. Este dato viene de //el campo data.sessionUser. const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } if ( !(await EmployeesList.checkPermission( companyCheck, data.sessionUser, EmployeeRoles.ADMIN )) ) { throw new Error(NoPermissionMessage()); } const canAdd = await MetricsList.canAddRepeat(companyCheck.ownerId); if (!canAdd) { throw new Error( "Ha alcanzado el limite de repeticiones permitidas de acuerdo a su plan." ); } const checkEmployee = await EmployeesList.employees.findOne({ _id: data.employeeId, }); if (!checkEmployee) { throw new Error("El empleado no existe"); } const employeeUser = await UsersManager.users.findOne({ _id: checkEmployee.userId, }); if (!employeeUser) { throw new Error("El empleado no tiene un usuario asociado"); } const checkService = await ServiceList.services.findOne({ _id: data.serviceId, }); if (!checkService) { throw new Error("El servicio no existe"); } const checkClient = await ClientsList.clients.findOne({ _id: data.clientId, }); if (!checkClient) { throw new Error("El cliente no existe"); } const checkRepeat = await this.repeats.findOne({ companyId: data.companyId, clientId: data.clientId, weekDay: data.weekDay, startHour: data.startHour, }); if (checkRepeat) { throw new Error("Ya existe una reserva previa."); } const newRepeat = await this.repeats.create({ ...data, ...{ length: checkService.length }, }); await MetricsList.addRepeat({ userId: companyCheck.ownerId, quantity: 1, }); const humanDay = dayjs().localeData().weekdays()[data.weekDay]; const humanHour = this.numberToTime(data.startHour); if (checkClient.userId) { await NotificationsManager.sendSystemNotification({ userId: String(checkClient.userId), subject: "Se ha creado una nueva reserva con renovación automática", message: `La organización ${ companyCheck.name } ha asignado una reserva con renovación automática todos los ${humanDay} a las ${humanHour} para ${ checkService.name } con ${UserList.getUserFullName(employeeUser)}.`, type: NotificationType.SYSTEM }); } await NotificationsManager.sendSystemNotification({ userId: String(checkEmployee.userId), subject: "Se ha creado una nueva reserva con renovación automática", message: `${ClientsList.getClientFullName( checkClient )} tiene una reservación con renovación automática todos los ${humanDay} a las ${humanHour} para ${ checkService.name } con vos.`, type: NotificationType.SYSTEM }); return newRepeat; } public async deleteRepeat(data: DeleteRepeatsParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const checkRepeat = await this.repeats.findOne({ _id: data.repeatId, }); if (!checkRepeat) { throw new Error("La repetición no existe"); } const companyCheck = await CompaniesManager.companies.findOne({ _id: String(checkRepeat.companyId), }); if (!companyCheck) { throw new Error("La compañia no existe"); } if ( !(await EmployeesList.checkPermission( companyCheck, data.sessionUser, EmployeeRoles.ADMIN )) ) { throw new Error(NoPermissionMessage()); } await this.repeats.delete(data.repeatId); await MetricsList.addRepeat({ userId: String(companyCheck.ownerId), quantity: -1 }); } public async deleteRepeatsByCompany(data: DeleteRepeatsByCompanyParams): Promise { await this.repeats.repeatsList.deleteMany({ companyId: data.companyId }); } public async deleteRepeatsByClient(data: DeleteRepeatsByClientParams): Promise { await this.repeats.repeatsList.deleteMany({ clientId: data.clientId }); } public async deleteRepeatsByUser(data: DeleteRepeatsByUserParams): Promise { //obtengo los clientes asociados al usuario. const clients = await ClientsList.clients.find({ userId: data.userId, }); for (const client of clients) { await this.deleteRepeatsByClient({ clientId: String(client.id), }); } } public async getRepeatsByClient(data: FindRepeatsParams): Promise { const returnValue: RepeatView[] = []; const repeats = await this.repeats.find(data); for (const repeat of repeats) { if (!repeat.id) { continue; } const companyCheck = await CompaniesManager.companies.findOne({ _id: repeat.companyId, }); if (!companyCheck) { continue; } const employeeCheck = await EmployeesList.employees.findOne({ _id: repeat.employeeId, }); if (!employeeCheck) { continue; } const employeeUser = await UsersManager.users.findOne({ _id: employeeCheck.userId, }); if (!employeeUser) { continue; } const serviceCheck = await ServiceList.services.findOne({ _id: repeat.serviceId, }); if (!serviceCheck) { continue; } const clientCheck = await ClientsList.clients.findOne({ _id: repeat.clientId, }); if (!clientCheck) { continue; } const discountData = await Discounts.getDiscountData( repeat.discountId, repeat.companyId ); returnValue.push({ id: repeat.id, weekDay: repeat.weekDay, weekDayName: dayjs().localeData().weekdays()[repeat.weekDay], startHour: repeat.startHour, startHourFormatted: this.numberToTime(repeat.startHour), length: repeat.length, companyId: repeat.companyId, companyName: companyCheck.name, employeeId: repeat.employeeId, employeeName: UserList.getUserFullName(employeeUser), employeeCalendarColor: isNull(employeeCheck.calendarColor, "#000000"), employeeAvatar: getAvatar( employeeCheck.userId, employeeUser.avatar, UserList.getUserFullName(employeeUser) ), serviceId: repeat.serviceId, serviceName: serviceCheck.name, discountId: isNull(repeat.discountId, ""), discountName: discountData.name, discountType: discountData.type, discountValue: discountData.value, discountCode: discountData.code, clientId: repeat.clientId, clientName: ClientsList.getClientFullName(clientCheck), }); } return returnValue; } public async countRepeats(data: CountRepeatsParams): Promise { let count = 0; const countDateStart = dayjs(data.countDate); const countDateEnd = countDateStart.clone().add(data.heatMapFraction, "minutes"); const weekDay = dayjs(data.countDate).weekday() + 1; const weekStart = dayjs(data.countDate).startOf("week"); const repeats = await this.repeats.find({ companyId: data.companyId, employeeId: data.employeeId, weekDay: weekDay, }); for (const repeat of repeats) { if (!repeat.id) { continue; } const repeatHour = Math.floor(repeat.startHour); const repeatMinutes = (repeat.startHour - repeatHour) * 60; const repeatWeekDay = weekDay - 1; if (repeatWeekDay < 0) { continue; } const repeatDateStart = weekStart .add(repeatWeekDay, "d") .startOf("day") .hour(repeatHour) .minute(repeatMinutes) .second(0) .millisecond(0); //Si la fecha esta deshabilitada no tengo que contarla. if (repeat.disabledDates) { if (this.existDisabledDate(repeatDateStart.toDate(), repeat.disabledDates)) { continue; } } //TODO: Si el turno ya esta creado no tengo que contarlo... const searchDate = repeatDateStart.clone().toDate(); //Solo hago push si la repeticion no tiene el turno creado. const appointmentCreated = await AppointmentList.Appointments.findOne({ companyId: data.companyId, clientId: String(repeat.clientId), serviceId: String(repeat.serviceId), employeeId: String(repeat.employeeId), start: searchDate, }); if (appointmentCreated) { continue; } const repeatDateEnd = repeatDateStart.clone().add(repeat.length, "minutes"); if ( repeatDateStart.isSameOrBefore(countDateStart) && repeatDateEnd.isSameOrAfter(countDateEnd) ) { count++; } } return count; } public async getPendingRepeatsCurrentWeek(data: GetPendingRepeatsParams): Promise { const returnValue: PendingRepeatView[] = []; const weekStart = dayjs().startOf("week"); const filters: any = {}; if (data.companyId) { const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } filters.companyId = data.companyId; } if (data.filterBySessionUser) { const employeeQuery: any = { userId: data.sessionUser }; if (data.companyId) { employeeQuery.companyId = data.companyId; } const sessionEmployees = await EmployeesList.employees.find(employeeQuery); if (sessionEmployees && sessionEmployees.length > 0) { filters.employeeId = { $in: sessionEmployees.map(e => e.id) }; } else { // If user is not an employee, they can't have personal repeats return []; } } else if (data.employeeId) { filters.employeeId = data.employeeId; } const repeats = await this.repeats.find(filters); for (const repeat of repeats) { if (!repeat.id) { continue; } const repeatHour = Math.floor(repeat.startHour); const repeatMinutes = Math.round((repeat.startHour - repeatHour) * 60); // dayjs() .weekday() logic: 0 to 6 // repeat.weekDay is likely 1-7 or 0-6? // In countRepeats: const weekDay = dayjs(data.countDate).weekday() + 1; // and repeat.weekDay is checked against it. So repeat.weekDay corresponds to weekday()+1 const repeatWeekDay = repeat.weekDay - 1; if (repeatWeekDay < 0 || repeatWeekDay > 6) { continue; // Should not happen if data is correct } const repeatDateStart = weekStart .clone() .add(repeatWeekDay, "d") .startOf("day") .hour(repeatHour) .minute(repeatMinutes) .second(0) .millisecond(0); // Si la fecha es en el pasado, no la mostramos como pendiente para crear if (repeatDateStart.isBefore(dayjs().startOf("day"))) { continue; } const searchDate = repeatDateStart.toDate(); // Si la fecha esta deshabilitada no tengo que devolverla if (repeat.disabledDates && this.existDisabledDate(searchDate, repeat.disabledDates)) { continue; } // Check if appointment is already created const appointmentCreated = await AppointmentList.Appointments.findOne({ companyId: String(repeat.companyId), clientId: String(repeat.clientId), serviceId: String(repeat.serviceId), employeeId: String(repeat.employeeId), start: searchDate, }); if (appointmentCreated) { continue; } // Prepare the view const employeeCheck = await EmployeesList.employees.findOne({ _id: repeat.employeeId }); if (!employeeCheck) continue; const employeeUser = await UsersManager.users.findOne({ _id: employeeCheck.userId }); if (!employeeUser) continue; const serviceCheck = await ServiceList.services.findOne({ _id: repeat.serviceId }); if (!serviceCheck) continue; const clientCheck = await ClientsList.clients.findOne({ _id: repeat.clientId }); if (!clientCheck) continue; const discountData = await Discounts.getDiscountData(repeat.discountId, repeat.companyId); const companyCheck = await CompaniesManager.companies.findOne({ _id: repeat.companyId }); returnValue.push({ id: repeat.id, weekDay: repeat.weekDay, weekDayName: dayjs().localeData().weekdays()[repeat.weekDay], startHour: repeat.startHour, startHourFormatted: this.numberToTime(repeat.startHour), length: repeat.length, companyId: repeat.companyId, companyName: companyCheck ? companyCheck.name : "", employeeId: repeat.employeeId, employeeName: UserList.getUserFullName(employeeUser), employeeCalendarColor: isNull(employeeCheck.calendarColor, "#000000"), employeeAvatar: getAvatar(employeeCheck.userId, employeeUser.avatar, UserList.getUserFullName(employeeUser)), serviceId: repeat.serviceId, serviceName: serviceCheck.name, discountId: isNull(repeat.discountId, ""), discountName: discountData.name, discountType: discountData.type, discountValue: discountData.value, discountCode: discountData.code, clientId: repeat.clientId, clientName: ClientsList.getClientFullName(clientCheck), pendingDate: searchDate }); } // Sort by pendingDate returnValue.sort((a, b) => a.pendingDate.getTime() - b.pendingDate.getTime()); return returnValue; } public existDisabledDate(checkDate: Date, disabledDates: Date[]): boolean { return disabledDates.some((fecha) => fecha.getTime() === checkDate.getTime()); } public async addDisabledDate(data: AddRepeatDisabledDateParams): Promise { const repeat = await this.repeats.findOne({ _id: data.repeatId, }); if (!repeat) { throw new Error("La repetición no existe"); } const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const companyCheck = await CompaniesManager.companies.findOne({ _id: String(repeat.companyId), }); if (!companyCheck) { throw new Error("La compañia no existe"); } if ( !(await EmployeesList.checkPermission( companyCheck, data.sessionUser, EmployeeRoles.ADMIN )) ) { throw new Error(NoPermissionMessage()); } if (!repeat.disabledDates) { repeat.disabledDates = []; } if (this.existDisabledDate(data.disableDate, repeat.disabledDates)) { console.log("La fecha ya estaba deshabilitada"); return; } repeat.disabledDates.push(new Date(data.disableDate)); await repeat.save(); } } const RepeatsList = new RepeatsManager(); export default RepeatsList;