Files
turnosxpress/server/src/Models/Repeats/Repeats.ts
T

586 lines
20 KiB
TypeScript

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<IRepeat> {
//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<void> {
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<void> {
await this.repeats.repeatsList.deleteMany({ companyId: data.companyId });
}
public async deleteRepeatsByClient(data: DeleteRepeatsByClientParams): Promise<void> {
await this.repeats.repeatsList.deleteMany({ clientId: data.clientId });
}
public async deleteRepeatsByUser(data: DeleteRepeatsByUserParams): Promise<void> {
//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<RepeatView[]> {
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<string>(employeeCheck.calendarColor, "#000000"),
employeeAvatar: getAvatar(
employeeCheck.userId,
employeeUser.avatar,
UserList.getUserFullName(employeeUser)
),
serviceId: repeat.serviceId,
serviceName: serviceCheck.name,
discountId: isNull<string>(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<number> {
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<PendingRepeatView[]> {
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<string>(employeeCheck.calendarColor, "#000000"),
employeeAvatar: getAvatar(employeeCheck.userId, employeeUser.avatar, UserList.getUserFullName(employeeUser)),
serviceId: repeat.serviceId,
serviceName: serviceCheck.name,
discountId: isNull<string>(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<void> {
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;