3063 lines
112 KiB
TypeScript
3063 lines
112 KiB
TypeScript
import { AppointmentsAdapterMongoose } from "./Appointments.Adapter.Mongoose";
|
|
import {
|
|
ApplyAppointmentDiscountParams,
|
|
APPOINTMENT_NOTIFICATION_TYPE,
|
|
AppointmentAdminByClientView,
|
|
AppointmentAdminByDateView,
|
|
AppointmentEvent,
|
|
AppointmentEventByClient,
|
|
AppointmentNotificationIntent,
|
|
ChangeEmployeeToOwnerParams,
|
|
CountAppointmentsByMonthParams,
|
|
CreateAppointmentParams,
|
|
CreateImmediateAppointmentNotificationJobsParams,
|
|
DeleteAppointmentDiscountParams,
|
|
DeleteAppointmentParams,
|
|
DeleteAppointmentsByCompanyParams,
|
|
FindAppointmentsByUserParams,
|
|
FindAppointmentsByCollaboratorParams,
|
|
FindAppointmentsByUserPaginatedParams,
|
|
FindAppointmentsByCollaboratorPaginatedParams,
|
|
PaginatedAppointmentEventByClientResult,
|
|
FindAppointmentSchedulesParams,
|
|
FindAppointmentsParams,
|
|
GetAppointmentEventParams,
|
|
GetAvailableDatesParams,
|
|
GetAvailableDatesResult,
|
|
HeatMapConfig,
|
|
IAppointment,
|
|
IAppointmentsManager,
|
|
PaginateAppointmentsParams,
|
|
PaginateAppointmentsResults,
|
|
SendAppointmentNotificationParams,
|
|
SendWapSystemNotificationParams,
|
|
SetPaymentStatusParams,
|
|
UpdateAppointmentParams,
|
|
MoveAppointmentParams, ChangeServiceParams,
|
|
} from "./Appointments.Interface";
|
|
import UsersManager from "../Users/Users";
|
|
import ClientsManager from "../Clients/Clients";
|
|
import CompaniesManager from "../Companies/Companies";
|
|
import EmployeesList from "../Employees/Employee";
|
|
import ServicesManager from "../Services/Service";
|
|
import { HeatMapItem, IHeatMap } from "../../Models/HeatMap/HeatMap.Interface";
|
|
import HeatMapList from "../HeatMap/HeatMap";
|
|
import dayjs from "dayjs";
|
|
import "dayjs/locale/es";
|
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
|
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
|
|
import weekday from "dayjs/plugin/weekday";
|
|
|
|
import { IClientDocument } from "../../Models/Clients/Clients.Adapter.Mongoose";
|
|
import { isNull } from "../../helpers/IsNull";
|
|
import { joinStrings } from "../../helpers/String";
|
|
import getAvatar from "../../helpers/getAvatar";
|
|
import ServiceList from "../Services/Service";
|
|
import { EmployeeRoles } from "../../Models/Employees/Employees.Interface";
|
|
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
|
|
import SchedulesEnabledList from "../../Models/SchedulesEnabled/SchedulesEnabled";
|
|
import SchedulesList from "../../Models/Schedules/Schedules";
|
|
import MetricsList from "../../Models/Metrics/Metrics";
|
|
import { TextObjectFilterResult } from "../TextObjectFilter.model";
|
|
import { NotificationsManager } from "../Notifications/Notifications";
|
|
import SystemNoficationsList from "../SystemNotifications/SystemNotification";
|
|
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
|
|
import { PlanFeatures } from "../Plans/Plans.interface";
|
|
import { formatCurrency } from "../../helpers/formatCurrency";
|
|
import RepeatsList from "../Repeats/Repeats";
|
|
import { IUserDocument } from "../Users/Users.Adapter.Mongoose";
|
|
import UserList from "../Users/Users";
|
|
import { DiscountType } from "../Discounts/Discounts.Interface";
|
|
import Discounts from "../Discounts/Discounts";
|
|
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
|
|
import Templates from "../Templates/Templates";
|
|
import { CollaboratorSchedulesView, ScheduleItem } from "../Schedules/Schedules.Interface";
|
|
import ClientAccount from "../ClientAccounts/ClientAccount";
|
|
import SchedulesDisabledList from "../SchedulesDisabled/SchedulesDisabled";
|
|
import DiscountsEmail from "../DiscountsEmail/DiscountsEmail";
|
|
import SchedulesOverridesList from "../SchedulesOverrides/SchedulesOverrides";
|
|
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
|
|
import { NotificationJobService } from "../NotificationPolicies/NotificationJobService";
|
|
|
|
dayjs.locale("es");
|
|
dayjs.extend(isSameOrAfter);
|
|
dayjs.extend(isSameOrBefore);
|
|
dayjs.extend(weekday);
|
|
|
|
class AppointmentManager implements IAppointmentsManager {
|
|
Appointments: AppointmentsAdapterMongoose;
|
|
private jobService: NotificationJobService;
|
|
|
|
constructor() {
|
|
this.Appointments = new AppointmentsAdapterMongoose();
|
|
this.jobService = new NotificationJobService();
|
|
}
|
|
|
|
private isScheduleAvailableForService(schedule: ScheduleItem, serviceId?: string): boolean {
|
|
if (!serviceId || !schedule.serviceScope || schedule.serviceScope === "all") {
|
|
return true;
|
|
}
|
|
|
|
return schedule.serviceScope === "specific" && (schedule.serviceIds || []).includes(serviceId);
|
|
}
|
|
|
|
public async setPaymentStatus(data: SetPaymentStatusParams): 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 appointment = await this.Appointments.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const companyCheck = await CompaniesManager.companies.findOne({
|
|
_id: String(appointment.companyId),
|
|
});
|
|
|
|
if (!companyCheck) {
|
|
throw new Error("La compañia no existe");
|
|
}
|
|
|
|
if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
|
|
throw new Error(NoPermissionMessage());
|
|
}
|
|
|
|
appointment.payment = data.status;
|
|
await appointment.save();
|
|
}
|
|
|
|
public async countAppointmentsByMonth(data: CountAppointmentsByMonthParams): Promise<number> {
|
|
const startMonth = dayjs(new Date(data.year, data.month, 1)).startOf("month");
|
|
const endMonth = dayjs(new Date(data.year, data.month, 1)).endOf("month");
|
|
|
|
const appointments = await this.Appointments.findByDateRange(
|
|
startMonth.toDate(),
|
|
endMonth.toDate(),
|
|
data.companyId
|
|
);
|
|
|
|
return appointments.length;
|
|
}
|
|
|
|
public async changeEmployeeToOwner(data: ChangeEmployeeToOwnerParams): Promise<void> {
|
|
const org = await CompaniesManager.companies.findOne({
|
|
_id: data.companyId,
|
|
});
|
|
|
|
if (!org) {
|
|
throw new Error("La compañia no existe");
|
|
}
|
|
|
|
const appointment = await this.Appointments.AppointmentList.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: String(appointment.employeeId),
|
|
});
|
|
|
|
if (!employee) {
|
|
throw new Error("El empleado no existe");
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
if (!userEmployee) {
|
|
throw new Error("El empleado no tiene usuario asociado");
|
|
}
|
|
|
|
const employeeOwner = await EmployeesList.employees.findOne({
|
|
companyId: String(org._id),
|
|
userId: String(org.ownerId),
|
|
});
|
|
|
|
if (!employeeOwner) {
|
|
throw new Error("El dueño de la compañia no tiene colaborador asociado");
|
|
}
|
|
|
|
const appClient = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!appClient) {
|
|
throw new Error("El cliente no existe");
|
|
}
|
|
|
|
const appService = await ServicesManager.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!appService) {
|
|
throw new Error("El servicio no existe");
|
|
}
|
|
|
|
appointment.employeeId = String(employeeOwner.id);
|
|
appointment.comments = String(
|
|
appointment.comments + " Turno transferido de " + UsersManager.getUserFullName(userEmployee)
|
|
).trimStart();
|
|
await appointment.save();
|
|
|
|
//Si el turno es para el dia actual o posterior, enviar notificacion al dueño de la compañia
|
|
if (dayjs(appointment.dateDay).isSameOrAfter(dayjs().startOf("day"))) {
|
|
await SystemNoficationsList.createNotification({
|
|
userId: String(org.ownerId),
|
|
subject: "Turno transferido",
|
|
message: `El turno del dia ${dayjs(appointment.dateDay).format(
|
|
"DD/MM/YYYY"
|
|
)} de ${UsersManager.getUserFullName(userEmployee)} del servicio ${
|
|
appService.name
|
|
} para el cliente ${ClientsManager.getClientFullName(
|
|
appClient
|
|
)} ha sido transferido al dueño de la compañia.`,
|
|
});
|
|
}
|
|
|
|
//Actualizar el heatmap.
|
|
const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);
|
|
|
|
const serviceCheck = await ServicesManager.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
companyId: String(appointment.companyId),
|
|
});
|
|
|
|
if (!serviceCheck) {
|
|
throw new Error("El servicio no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
const availabilityConfig: HeatMapConfig = {
|
|
companyId: String(appointment.companyId),
|
|
serviceId: String(appointment.serviceId),
|
|
employeeId: String(appointment.employeeId),
|
|
heatMapDate: dayjs(appointment.start).startOf("day").toDate(),
|
|
start: dayjs(appointment.start).toDate(),
|
|
fraction: defaultFraction,
|
|
length: isNull<number>(appointment.length, 1),
|
|
serviceLimit: serviceCheck.limit,
|
|
serviceLength: serviceCheck.length,
|
|
};
|
|
|
|
await this.updateHeatMap(availabilityConfig);
|
|
}
|
|
|
|
public async deleteAppointmentsByCompany(data: DeleteAppointmentsByCompanyParams): Promise<void> {
|
|
await this.Appointments.AppointmentList.deleteMany({
|
|
companyId: data.companyId,
|
|
});
|
|
}
|
|
|
|
private getAppointmentTime(appointmentTime: number | undefined): number {
|
|
if (appointmentTime) {
|
|
return Math.abs(appointmentTime);
|
|
}
|
|
return parseInt(`${process.env.APPOINTMEENT_TIME}`) || 24;
|
|
}
|
|
|
|
private validateAppointmentTime(aTime: number | undefined, start: string | Date): boolean {
|
|
let appointmentTime = this.getAppointmentTime(aTime);
|
|
|
|
const checkDate = dayjs(start).clone().add(-appointmentTime, "hours");
|
|
const nowDate = dayjs().clone();
|
|
|
|
if (nowDate.isBefore(checkDate)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private getCancellationTime(cancellationTime: number | undefined): number {
|
|
if (cancellationTime) {
|
|
return Math.abs(cancellationTime);
|
|
}
|
|
return parseInt(`${process.env.CANCELLATION_TIME}`) || 24;
|
|
}
|
|
|
|
private validateCancellationTime(aTime: number | undefined, start: string | Date): boolean {
|
|
let cancellationTime = this.getCancellationTime(aTime);
|
|
|
|
const checkDate = dayjs(start).clone().add(-cancellationTime, "hours");
|
|
const nowDate = dayjs().clone();
|
|
|
|
if (nowDate.isBefore(checkDate)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async createAppointment(data: CreateAppointmentParams): Promise<IAppointment> {
|
|
const { validation = true, notification = true } = data;
|
|
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");
|
|
}
|
|
|
|
const serviceCheck = await ServicesManager.services.findOne({
|
|
_id: data.serviceId,
|
|
companyId: data.companyId,
|
|
});
|
|
|
|
if (!serviceCheck) {
|
|
throw new Error("El servicio no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
let servicePrice = serviceCheck.price;
|
|
|
|
if (!data.price) {
|
|
data.price = servicePrice;
|
|
}
|
|
|
|
if (!data.price) {
|
|
throw new Error("Error al determinar el valor del turno.");
|
|
}
|
|
|
|
data.length = isNull<number>(data.length, 1);
|
|
|
|
if (!data.length) {
|
|
throw new Error("Error al determinar la duración del turno.");
|
|
}
|
|
|
|
const employeeCheck = await EmployeesList.employees.findOne({
|
|
companyId: data.companyId,
|
|
_id: data.employeeId,
|
|
});
|
|
|
|
if (!employeeCheck) {
|
|
throw new Error("El empleado no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
if (!this.validateAppointmentTime(companyCheck.appointmentTime, data.start) && validation) {
|
|
throw new Error(
|
|
`No es posible reservar el turno con menos de ${this.getAppointmentTime(
|
|
companyCheck.appointmentTime
|
|
)} horas de anticipación.`
|
|
);
|
|
}
|
|
|
|
let checkClient = {} as IClientDocument | null;
|
|
|
|
if (data.userId) {
|
|
checkClient = await ClientsManager.clients.findOne({
|
|
userId: data.userId,
|
|
companyId: data.companyId,
|
|
});
|
|
|
|
if (!checkClient) {
|
|
//Si el cliente no existe hay que crearlo.
|
|
checkClient = await ClientsManager.createClientByUser({
|
|
userId: data.userId,
|
|
companyId: data.companyId,
|
|
sessionUser: data.sessionUser,
|
|
});
|
|
|
|
if (!checkClient) {
|
|
throw new Error("No se ha podido crear el cliente");
|
|
}
|
|
}
|
|
|
|
if (!checkClient.status) {
|
|
throw new Error("El cliente no se encuentra activo.");
|
|
}
|
|
} else if (data.clientId) {
|
|
checkClient = await ClientsManager.clients.findOne({
|
|
_id: data.clientId,
|
|
});
|
|
|
|
if (!checkClient) {
|
|
throw new Error("El cliente no existe.");
|
|
}
|
|
}
|
|
|
|
if (!checkClient) {
|
|
throw new Error("Ha ocurrido un error al identificar el cliente.");
|
|
}
|
|
|
|
if (checkClient.banned) {
|
|
throw new Error(`La reserva no se pudo completar. Por favor comuniquese con ${companyCheck.name}`);
|
|
}
|
|
|
|
const checkAppointment = await this.Appointments.findOne({
|
|
companyId: data.companyId,
|
|
serviceId: data.serviceId,
|
|
employeeId: data.employeeId,
|
|
clientId: String(checkClient.id),
|
|
start: dayjs(data.start).toDate(),
|
|
});
|
|
|
|
if (checkAppointment) {
|
|
throw new Error("Ya existe una reserva para la fecha y horario seleccionados");
|
|
}
|
|
|
|
const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);
|
|
|
|
const availabilityConfig: HeatMapConfig = {
|
|
companyId: data.companyId,
|
|
serviceId: data.serviceId,
|
|
employeeId: data.employeeId,
|
|
heatMapDate: dayjs(data.start).startOf("day").toDate(),
|
|
start: dayjs(data.start).toDate(),
|
|
fraction: defaultFraction,
|
|
length: isNull<number>(data.length, 1),
|
|
serviceLimit: serviceCheck.limit,
|
|
serviceLength: serviceCheck.length,
|
|
};
|
|
|
|
if (validation) {
|
|
const checkAvailable = await this.checkAvailability(availabilityConfig, data);
|
|
|
|
if (checkAvailable === false) {
|
|
throw new Error("No hay disponibilidad para el horario seleccionado.");
|
|
}
|
|
|
|
const checkSchedules = await this.checkSchedules(availabilityConfig);
|
|
|
|
if (checkSchedules === false) {
|
|
throw new Error("No hay disponibilidad para el dia o el horario seleccionado.");
|
|
}
|
|
|
|
const checkDisabled = await this.checkSchedulesDisabled(availabilityConfig);
|
|
|
|
if (!checkDisabled) {
|
|
throw new Error("El profesional no se encuentra disponible en el horario seleccionado.");
|
|
}
|
|
}
|
|
|
|
data.comments = isNull<string>(data.comments, "");
|
|
|
|
const appointmentReserved = await MetricsList.reserveAppointment({
|
|
userId: String(companyCheck.ownerId),
|
|
companyId: data.companyId,
|
|
quantity: 1,
|
|
});
|
|
|
|
if (!appointmentReserved) {
|
|
throw new Error("Ha alcanzado el limite de turnos permitidos de acuerdo a su plan.");
|
|
}
|
|
|
|
let appointmentCreated = false;
|
|
|
|
try {
|
|
const start = dayjs(data.start).clone();
|
|
const end = dayjs(data.start)
|
|
.clone()
|
|
.add(serviceCheck.length * isNull<number>(data.length, 1), "minutes");
|
|
|
|
const startHour = start.clone().hour() + start.clone().minute() / 60;
|
|
const endHour = end.clone().hour() + end.clone().minute() / 60;
|
|
|
|
const newAppointment = await this.Appointments.create({
|
|
companyId: data.companyId,
|
|
serviceId: data.serviceId,
|
|
employeeId: data.employeeId,
|
|
clientId: String(checkClient._id),
|
|
userId: checkClient.userId ? String(checkClient.userId) : undefined,
|
|
start: dayjs(data.start).toDate(),
|
|
startHour: startHour,
|
|
endHour: endHour,
|
|
dateDay: dayjs(data.start).clone().startOf("day").toDate(),
|
|
price: data.price,
|
|
length: data.length,
|
|
comments: data.comments,
|
|
sessionUser: data.sessionUser,
|
|
});
|
|
appointmentCreated = true;
|
|
|
|
if (data.repeatId) {
|
|
const repeat = await RepeatsList.repeats.findOne({
|
|
_id: data.repeatId,
|
|
});
|
|
|
|
if (repeat && repeat.discountId) {
|
|
const discount = await Discounts.discounts.discountsList.findOne({
|
|
_id: repeat.discountId,
|
|
});
|
|
|
|
if (discount) {
|
|
await this.applyDiscount({
|
|
appointmentId: String(newAppointment.id),
|
|
discountId: String(discount._id),
|
|
companyId: String(newAppointment.companyId),
|
|
sessionUser: String(data.sessionUser),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
await MetricsList.metrics.addAppointment({
|
|
userId: companyCheck.ownerId,
|
|
companyId: data.companyId,
|
|
quantity: 1,
|
|
});
|
|
|
|
await this.updateHeatMap(availabilityConfig);
|
|
|
|
await this.tryApplyDiscounts(newAppointment);
|
|
|
|
if (notification && checkClient.userId) {
|
|
//Envio la notificacion al cliente.
|
|
await NotificationsManager.sendSystemNotification({
|
|
userId: String(checkClient.userId),
|
|
subject: "Turno reservado",
|
|
message: `Se ha reservado un turno para ${serviceCheck.name} el día ${dayjs(
|
|
data.start
|
|
).format("DD/MM/YYYY")} a las ${dayjs(data.start).format("HH:mm")}`,
|
|
type: NotificationType.APPOINTMENT,
|
|
code: String((newAppointment as any)._id)
|
|
});
|
|
|
|
//Create jobs for email and whatsapp notifications
|
|
if (newAppointment.id) {
|
|
const emailContent = await this.tryToSendNotification({
|
|
appointmentId: String(newAppointment.id),
|
|
sessionUser: String(companyCheck.ownerId),
|
|
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
|
|
channel: "email",
|
|
});
|
|
const wapContent = await this.tryToSendNotification({
|
|
appointmentId: String(newAppointment.id),
|
|
sessionUser: String(companyCheck.ownerId),
|
|
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
|
|
channel: "whatsapp",
|
|
});
|
|
const clientPhoneNumber = await this.getOptionalClientWapNumber(checkClient);
|
|
|
|
await this.createCreationNotificationJobs({
|
|
appointmentId: String(newAppointment.id),
|
|
companyId: String(companyCheck._id),
|
|
clientId: String(checkClient._id),
|
|
clientUserId: checkClient.userId ? String(checkClient.userId) : undefined,
|
|
clientEmail: checkClient.email,
|
|
clientPhoneNumber,
|
|
companyOwnerId: String(companyCheck.ownerId),
|
|
companyName: companyCheck.name,
|
|
appointmentStart: new Date(data.start),
|
|
emailMessage: emailContent.message,
|
|
wapMessage: wapContent.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
//Envio la notificacion al profesional.
|
|
await NotificationsManager.sendSystemNotification({
|
|
userId: String(employeeCheck.userId),
|
|
subject: "Turno reservado",
|
|
message: `${ClientsManager.getClientFullName(checkClient)} ha reservado un turno para ${
|
|
serviceCheck.name
|
|
} el día ${dayjs(data.start).format("DD/MM/YYYY")} a las ${dayjs(data.start).format(
|
|
"HH:mm"
|
|
)}`,
|
|
type: NotificationType.APPOINTMENT,
|
|
code: String((newAppointment as any)._id)
|
|
});
|
|
|
|
return newAppointment;
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (!appointmentCreated) {
|
|
await MetricsList.releaseAppointment({
|
|
userId: String(companyCheck.ownerId),
|
|
companyId: data.companyId,
|
|
quantity: 1,
|
|
});
|
|
}
|
|
throw new Error("Ha ocurrido un error al crear el turno.");
|
|
}
|
|
}
|
|
|
|
private async tryApplyDiscounts(appointment: IAppointment) {
|
|
//Intenta aplicar un descuento al turno si esta especificado en DiscountsEmail
|
|
// y la fecha coincide con el rango especificado.
|
|
|
|
try {
|
|
//Obtener el mail del cliente.
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
let email = client.email;
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: String(client.userId),
|
|
});
|
|
|
|
if (userClient) {
|
|
email = userClient.email;
|
|
}
|
|
|
|
//Obtener los descuentos del cliente.
|
|
const existDiscount = await DiscountsEmail.discountsEmail.findOne({
|
|
email: email,
|
|
companyId: String(appointment.companyId),
|
|
searchDate: appointment.start,
|
|
});
|
|
|
|
if (!existDiscount) {
|
|
return;
|
|
}
|
|
|
|
//Ahora aplicamos el descuento
|
|
await this.applyDiscount({
|
|
appointmentId: String(appointment.id),
|
|
discountId: String(existDiscount.discountId),
|
|
companyId: String(appointment.companyId),
|
|
sessionUser: String(appointment.userId),
|
|
});
|
|
} catch (err) {
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
public async paginate(filters: PaginateAppointmentsParams): Promise<PaginateAppointmentsResults> {
|
|
return await this.Appointments.paginate(filters);
|
|
}
|
|
|
|
|
|
public async moveAppointmentForce(data: MoveAppointmentParams): Promise<void> {
|
|
await validateSessionUser({ sessionUser: data.sessionUser || "" });
|
|
|
|
const appointment: any = await this.Appointments.findOne({
|
|
_id: data.id,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
await validatePermissionsByCompany({ companyId: String(appointment.companyId), sessionUser: data.sessionUser || "" });
|
|
|
|
const serviceCheck = await ServicesManager.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
companyId: String(appointment.companyId),
|
|
});
|
|
|
|
if (!serviceCheck) {
|
|
throw new Error("El servicio no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
// Get HeatMapConfig of the original date before changing it
|
|
const originalConfig = this.getHeatMapConfig(appointment, serviceCheck.length);
|
|
|
|
// Update the appointment's start times
|
|
const newStart = dayjs(data.newStart).clone();
|
|
appointment.start = newStart.toDate();
|
|
appointment.dateDay = newStart.clone().startOf("day").toDate();
|
|
appointment.startHour = newStart.clone().hour() + newStart.clone().minute() / 60;
|
|
|
|
const end = newStart.clone().add(serviceCheck.length * (appointment.length ? appointment.length : 1), "minutes");
|
|
appointment.endHour = end.clone().hour() + end.clone().minute() / 60;
|
|
|
|
// Save appointment (directly, bypassing notifications & checks)
|
|
await appointment.save();
|
|
|
|
// Compute the new HeatMapConfig
|
|
const newConfig = this.getHeatMapConfig(appointment, serviceCheck.length);
|
|
|
|
// Update both heatmaps
|
|
await this.updateHeatMap(originalConfig);
|
|
await this.updateHeatMap(newConfig);
|
|
}
|
|
|
|
public async changeServiceForce(data: ChangeServiceParams): Promise<void> {
|
|
await validateSessionUser({ sessionUser: data.sessionUser || "" });
|
|
|
|
const appointment: any = await this.Appointments.findOne({
|
|
_id: data.id,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
await validatePermissionsByCompany({ companyId: String(appointment.companyId), sessionUser: data.sessionUser || "" });
|
|
|
|
const oldServiceCheck = await ServicesManager.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
companyId: String(appointment.companyId),
|
|
});
|
|
|
|
if (!oldServiceCheck) {
|
|
throw new Error("El servicio original no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
const newServiceCheck = await ServicesManager.services.findOne({
|
|
_id: data.newServiceId,
|
|
companyId: String(appointment.companyId),
|
|
});
|
|
|
|
if (!newServiceCheck) {
|
|
throw new Error("El nuevo servicio no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
const newEmployeeCheck = await EmployeesList.employees.findOne({
|
|
_id: data.newEmployeeId,
|
|
companyId: String(appointment.companyId),
|
|
});
|
|
|
|
if (!newEmployeeCheck) {
|
|
throw new Error("El nuevo profesional no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
// Get HeatMapConfig of the original appointment before changing it
|
|
const originalConfig = this.getHeatMapConfig(appointment, oldServiceCheck.length);
|
|
|
|
// Update appointment details
|
|
appointment.serviceId = data.newServiceId;
|
|
appointment.employeeId = data.newEmployeeId;
|
|
appointment.price = newServiceCheck.price;
|
|
|
|
// Recalculate endHour based on new service length
|
|
const startHour = appointment.startHour;
|
|
const newEndHour = startHour + (newServiceCheck.length * (appointment.length || 1)) / 60;
|
|
appointment.endHour = newEndHour;
|
|
|
|
// Save appointment
|
|
await appointment.save();
|
|
|
|
// If there is a discount, reapply it to update the finalPrice, else just save.
|
|
if (appointment.discountId) {
|
|
await this.applyDiscount({
|
|
appointmentId: String(appointment._id),
|
|
discountId: String(appointment.discountId),
|
|
companyId: String(appointment.companyId),
|
|
sessionUser: data.sessionUser
|
|
});
|
|
}
|
|
|
|
// Compute the new HeatMapConfig
|
|
const newConfig = this.getHeatMapConfig(appointment, newServiceCheck.length);
|
|
|
|
// Update both heatmaps
|
|
await this.updateHeatMap(originalConfig);
|
|
await this.updateHeatMap(newConfig);
|
|
}
|
|
|
|
public buildHeatMapEmpty(config: HeatMapConfig): IHeatMap {
|
|
const heatMapData: Array<HeatMapItem> = [];
|
|
const startDate = dayjs(config.heatMapDate).clone().startOf("day");
|
|
const endDate = dayjs(config.heatMapDate).clone().endOf("day");
|
|
let currentDate = startDate.clone();
|
|
|
|
while (currentDate.isBefore(endDate)) {
|
|
heatMapData.push({
|
|
keyDate: currentDate.toDate(),
|
|
count: 0,
|
|
});
|
|
|
|
currentDate = currentDate.clone().add(config.fraction, "minutes");
|
|
}
|
|
|
|
return {
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
serviceId: config.serviceId,
|
|
heatMap: heatMapData,
|
|
heatMapDate: startDate.clone().toDate(),
|
|
};
|
|
}
|
|
|
|
public async getHeatMapToCheckAvailability(config: HeatMapConfig): Promise<IHeatMap> {
|
|
//TODO: debe devolver un heatmap unificado entre todos los servicios del empleado.
|
|
//Podria crear un heatmap cada 15 minutos. Siendo esta la menor unidad de reserva.
|
|
//Y luego rellenarlo segun cada servicio sobre el que tenga reservas hechas el empleado.
|
|
|
|
const globalHeatMap = this.buildHeatMapEmpty({
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
serviceId: "", //no importa el servicio. es global.
|
|
heatMapDate: config.heatMapDate,
|
|
start: config.start,
|
|
fraction: config.fraction, //minima fracción de tiempo
|
|
length: 1,
|
|
serviceLimit: -1, //no lo usa
|
|
serviceLength: 15,
|
|
});
|
|
|
|
//console.table(globalHeatMap.heatMap);
|
|
|
|
//Busco todos los heatmaps para todos los servicios.
|
|
let heatMaps = await HeatMapList.find({
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
heatMapDate: config.heatMapDate,
|
|
});
|
|
|
|
if (!globalHeatMap.heatMap) {
|
|
return globalHeatMap;
|
|
}
|
|
|
|
for (let i = 0; i < globalHeatMap.heatMap.length; i++) {
|
|
const currentHeatMap = globalHeatMap.heatMap[i];
|
|
|
|
for (const serviceItem of heatMaps) {
|
|
if (!serviceItem.heatMap) {
|
|
continue;
|
|
}
|
|
const serviceHeatMap = serviceItem.heatMap[i];
|
|
currentHeatMap.count += serviceHeatMap.count;
|
|
}
|
|
const repeatsCount = await RepeatsList.countRepeats({
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
countDate: currentHeatMap.keyDate,
|
|
heatMapFraction: config.fraction,
|
|
});
|
|
currentHeatMap.count += repeatsCount;
|
|
}
|
|
|
|
//console.table(globalHeatMap.heatMap);
|
|
|
|
return globalHeatMap;
|
|
}
|
|
public async getHeatMap(config: HeatMapConfig): Promise<IHeatMap> {
|
|
const appointments = await this.Appointments.find({
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
serviceId: config.serviceId,
|
|
dateDay: config.heatMapDate,
|
|
});
|
|
|
|
let heatMap = this.buildHeatMapEmpty(config);
|
|
|
|
if (!heatMap.heatMap) {
|
|
throw new Error("El mapa de calor no existe");
|
|
}
|
|
|
|
for (const appointment of appointments) {
|
|
const endDateTime = dayjs(appointment.start)
|
|
.clone()
|
|
.add(config.serviceLength * isNull<number>(appointment.length, 1), "minutes");
|
|
const startIndex = this.getHeatMapPosition(heatMap, dayjs(appointment.start).clone().toDate());
|
|
|
|
for (let i = startIndex; i < heatMap.heatMap.length; i++) {
|
|
const heatMapItem = heatMap.heatMap[i];
|
|
|
|
const currentDate = dayjs(heatMap.heatMap[i].keyDate).clone();
|
|
|
|
if (currentDate.isSameOrAfter(endDateTime)) {
|
|
break;
|
|
}
|
|
|
|
heatMapItem.count += 1;
|
|
}
|
|
}
|
|
|
|
return heatMap;
|
|
}
|
|
|
|
public async updateHeatMap(config: HeatMapConfig): Promise<void> {
|
|
const heatMap = await this.getHeatMap(config);
|
|
|
|
if (!heatMap.heatMap) {
|
|
throw new Error("El mapa de calor no existe");
|
|
}
|
|
|
|
await HeatMapList.deleteHeatMapByDate({
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
serviceId: config.serviceId,
|
|
heatMapDate: dayjs(config.start).startOf("day").toDate(),
|
|
});
|
|
|
|
await HeatMapList.createHeatMap({
|
|
companyId: config.companyId,
|
|
employeeId: config.employeeId,
|
|
serviceId: config.serviceId,
|
|
heatmap: heatMap,
|
|
heatMapDate: dayjs(config.start).startOf("day").toDate(),
|
|
});
|
|
}
|
|
|
|
public async checkSchedulesDisabled(data: HeatMapConfig): Promise<boolean> {
|
|
const check = await SchedulesDisabledList.exists({
|
|
employeeId: data.employeeId,
|
|
companyId: data.companyId,
|
|
dateDay: dayjs(data.start).toDate(),
|
|
});
|
|
|
|
if (check) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async checkSchedules(data: HeatMapConfig): Promise<boolean> {
|
|
const start = dayjs(data.start);
|
|
const end = start.clone().add(data.serviceLength * isNull<number>(data.length, 1), "minutes");
|
|
const weekDay = start.weekday() + 1;
|
|
const checkFrom = start.format("HH:mm");
|
|
const checkTo = end.format("HH:mm");
|
|
|
|
//chequear si hay un horario excepcional.
|
|
const hasOverride = await SchedulesOverridesList.findOne({
|
|
employeeId: data.employeeId,
|
|
companyId: data.companyId,
|
|
date: dayjs(data.start).startOf("day").toDate(),
|
|
});
|
|
|
|
if (!hasOverride) {
|
|
//chequear que la fecha este habilitada.
|
|
const check = await SchedulesEnabledList.exists({
|
|
employeeId: data.employeeId,
|
|
companyId: data.companyId,
|
|
dateDay: dayjs(data.start).startOf("day").toDate(),
|
|
});
|
|
|
|
if (!check) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let checkColaborator = false;
|
|
|
|
if (hasOverride) {
|
|
checkColaborator = await SchedulesOverridesList.isAvailable({
|
|
employeeId: data.employeeId,
|
|
companyId: data.companyId,
|
|
dateDay: dayjs(data.start).startOf("day").toDate(),
|
|
from: checkFrom,
|
|
to: checkTo,
|
|
serviceId: data.serviceId,
|
|
});
|
|
} else {
|
|
//chequear que el colaborador este disponible.
|
|
checkColaborator = await SchedulesList.isAvailable({
|
|
employeeId: data.employeeId,
|
|
companyId: data.companyId,
|
|
weekDay: weekDay,
|
|
from: checkFrom,
|
|
to: checkTo,
|
|
serviceId: data.serviceId,
|
|
});
|
|
}
|
|
|
|
if (!checkColaborator) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async checkAvailability(config: HeatMapConfig, data: CreateAppointmentParams): Promise<boolean> {
|
|
const heatMap = await this.getHeatMapToCheckAvailability(config);
|
|
|
|
if (!heatMap.heatMap) {
|
|
return false;
|
|
}
|
|
|
|
const endDateTime = dayjs(data.start)
|
|
.clone()
|
|
.add(config.serviceLength * isNull<number>(data.length, 1), "minutes");
|
|
const startIndex = this.getHeatMapPosition(heatMap, dayjs(data.start).clone().toDate());
|
|
|
|
if (startIndex < 0) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = startIndex; i < heatMap.heatMap.length; i++) {
|
|
const heatMapItem = heatMap.heatMap[i];
|
|
|
|
const currentDate = dayjs(heatMap.heatMap[i].keyDate).clone();
|
|
|
|
if (currentDate.isSame(endDateTime)) {
|
|
break;
|
|
}
|
|
|
|
/* console.log(
|
|
"heatmapCount:",
|
|
heatMapItem.count,
|
|
"serviceLimit:",
|
|
config.serviceLimit,
|
|
"check:",
|
|
heatMapItem.count >= config.serviceLimit
|
|
); */
|
|
|
|
if (heatMapItem.count >= config.serviceLimit) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private getHeatMapPosition(heatMap: IHeatMap, date: Date): number {
|
|
if (!heatMap.heatMap) {
|
|
return -1;
|
|
}
|
|
for (let i = 0; i < heatMap.heatMap.length; i++) {
|
|
const currentDate = dayjs(heatMap.heatMap[i].keyDate).clone();
|
|
if (currentDate.isSame(dayjs(date).clone())) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public async findAppointmentsAdminByDate(
|
|
data: FindAppointmentsParams
|
|
): Promise<AppointmentAdminByDateView> {
|
|
const returnData: AppointmentEvent[] = [];
|
|
|
|
const filterData: FindAppointmentsParams = {};
|
|
|
|
if (data.dateDay) {
|
|
filterData.dateDay = dayjs(data.dateDay).clone().startOf("day").toDate();
|
|
}
|
|
|
|
if (data.companyId) {
|
|
filterData.companyId = data.companyId;
|
|
}
|
|
|
|
if (data.employeeId) {
|
|
filterData.employeeId = data.employeeId;
|
|
}
|
|
|
|
const appointments = await this.Appointments.find(filterData);
|
|
|
|
for (const appointment of appointments) {
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
continue;
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: String(appointment.employeeId),
|
|
});
|
|
|
|
if (!employee) {
|
|
continue;
|
|
}
|
|
|
|
// Historical read: Employee lookup includes removed employees (historical reads allow them)
|
|
// No snapshot fallback needed here as employee.calendarColor is used directly
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(
|
|
appointment.discountId,
|
|
appointment.companyId
|
|
);
|
|
|
|
returnData.push({
|
|
appointmentId: isNull<string>(appointment.id, ""),
|
|
clientId: String(appointment.clientId),
|
|
serviceId: String(appointment.serviceId),
|
|
employeeId: String(appointment.employeeId),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
repeatId: "",
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
payment: appointment.payment,
|
|
});
|
|
}
|
|
|
|
const searchWeekDay = dayjs(data.dateDay).clone().weekday() + 1;
|
|
|
|
//Agregar las repeticiones.
|
|
const repeticiones = await RepeatsList.repeats.find({
|
|
companyId: data.companyId,
|
|
weekDay: searchWeekDay,
|
|
});
|
|
|
|
for (const repeticion of repeticiones) {
|
|
const repClient = await ClientsManager.clients.findOne({
|
|
_id: String(repeticion.clientId),
|
|
});
|
|
|
|
if (!repClient) {
|
|
continue;
|
|
}
|
|
|
|
let userRepClient: IUserDocument | null = null;
|
|
|
|
if (repClient.userId) {
|
|
userRepClient = await UsersManager.users.findOne({
|
|
_id: String(repClient.userId),
|
|
});
|
|
|
|
if (!userRepClient) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const repEmployee = await EmployeesList.employees.findOne({
|
|
_id: String(repeticion.employeeId),
|
|
});
|
|
|
|
if (!repEmployee) {
|
|
continue;
|
|
}
|
|
|
|
const repService = await ServicesManager.services.findOne({
|
|
_id: String(repeticion.serviceId),
|
|
});
|
|
|
|
if (!repService) {
|
|
continue;
|
|
}
|
|
|
|
const searchHour = Math.floor(repeticion.startHour);
|
|
const searchMinute = (repeticion.startHour - searchHour) * 60;
|
|
const searchDJS = dayjs(data.dateDay)
|
|
.clone()
|
|
.startOf("day")
|
|
.hour(searchHour)
|
|
.minute(searchMinute);
|
|
const searchDate = searchDJS.clone().toDate();
|
|
|
|
if (searchDJS.isSameOrBefore(dayjs())) {
|
|
continue;
|
|
}
|
|
|
|
//Solo hago push si la repeticion no tiene el turno creado.
|
|
const appointmentCreated = await this.Appointments.findOne({
|
|
companyId: data.companyId,
|
|
clientId: String(repeticion.clientId),
|
|
serviceId: String(repeticion.serviceId),
|
|
employeeId: String(repeticion.employeeId),
|
|
start: searchDate,
|
|
});
|
|
|
|
if (appointmentCreated) {
|
|
continue;
|
|
}
|
|
|
|
//Tengo que chequear que la fecha no este en disabledDates.
|
|
const checkDisabledDate = dayjs(data.dateDay)
|
|
.clone()
|
|
.startOf("day")
|
|
.hour(searchHour)
|
|
.minute(searchMinute);
|
|
|
|
if (repeticion.disabledDates) {
|
|
if (RepeatsList.existDisabledDate(checkDisabledDate.toDate(), repeticion.disabledDates)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const discountData = await Discounts.getDiscountData(repeticion.discountId, repeticion.companyId);
|
|
|
|
returnData.push({
|
|
appointmentId: "",
|
|
clientId: String(repeticion.clientId),
|
|
serviceId: String(repeticion.serviceId),
|
|
employeeId: String(repeticion.employeeId),
|
|
startTime: repeticion.startHour,
|
|
endTime: repeticion.startHour + repService.length / 60,
|
|
title: ClientsManager.getClientFullName(repClient),
|
|
avatar: getAvatar(
|
|
String(repClient.userId),
|
|
userRepClient ? userRepClient.avatar : "",
|
|
ClientsManager.getClientFullName(repClient)
|
|
),
|
|
color: isNull<string>(repEmployee.calendarColor, "#ff9900"),
|
|
repeatId: String(repeticion.id),
|
|
payment: false,
|
|
discountId: isNull<string>(undefined, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
});
|
|
}
|
|
|
|
//get min startTime hour from returnData
|
|
let minStartTime = 24;
|
|
for (const event of returnData) {
|
|
if (event.startTime < minStartTime) {
|
|
minStartTime = event.startTime;
|
|
}
|
|
}
|
|
|
|
return {
|
|
events: returnData,
|
|
from: Math.floor(minStartTime),
|
|
to: 24,
|
|
};
|
|
}
|
|
|
|
public async getAppointmentEvent(data: GetAppointmentEventParams): Promise<AppointmentEventByClient> {
|
|
const sessionUser = UserList.users.findOne({
|
|
_id: data.sessionUser,
|
|
});
|
|
|
|
if (!sessionUser) {
|
|
throw new Error("El usuario no existe");
|
|
}
|
|
|
|
const appointment = await this.Appointments.AppointmentList.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
throw new Error("El cliente no existe");
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: String(appointment.employeeId),
|
|
});
|
|
|
|
if (!employee) {
|
|
throw new Error("El colaborador no existe");
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
// Historical read snapshot fallback: use Employee snapshot if User is deleted
|
|
const collaboratorFirstName = userEmployee
|
|
? isNull<string>(userEmployee.firstName, "")
|
|
: isNull<string>(employee.profileSnapshot?.firstName, "");
|
|
const collaboratorLastName = userEmployee
|
|
? isNull<string>(userEmployee.lastName, "")
|
|
: isNull<string>(employee.profileSnapshot?.lastName, "");
|
|
const collaboratorFullName = joinStrings([collaboratorFirstName, collaboratorLastName], " ");
|
|
const collaboratorAvatar = userEmployee
|
|
? getAvatar(employee.userId, userEmployee.avatar, collaboratorFullName)
|
|
: (employee.profileSnapshot?.avatar || collaboratorFullName);
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
throw new Error("El servicio no existe");
|
|
}
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(appointment.discountId, appointment.companyId);
|
|
|
|
let ctaBalance = 0;
|
|
try {
|
|
ctaBalance = await ClientAccount.getAvailableAmount({
|
|
clientId: appointment.clientId,
|
|
sessionUser: data.sessionUser,
|
|
});
|
|
} catch (e) {
|
|
// Ignore error if user does not have payments plan
|
|
}
|
|
|
|
return {
|
|
id: appointment.id,
|
|
companyId: String(appointment.companyId),
|
|
clientId: appointment.clientId,
|
|
clientName: clientUserName,
|
|
serviceId: appointment.serviceId,
|
|
serviceName: service.name,
|
|
serviceDescription: service.description,
|
|
collaboratorId: employee.id,
|
|
collaboratorUserId: String(employee.userId),
|
|
collaboratorName: collaboratorFullName,
|
|
collaboratorAvatar: collaboratorAvatar,
|
|
appointmentDate: dayjs(appointment.start).toISOString(),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
comments: appointment.comments,
|
|
price: appointment.price,
|
|
length: appointment.length,
|
|
payment: appointment.payment,
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
clientCtaCteBalance: ctaBalance,
|
|
};
|
|
}
|
|
|
|
public async findAppointmentsAdminByClient(
|
|
data: FindAppointmentsParams
|
|
): Promise<AppointmentAdminByClientView> {
|
|
const returnData: AppointmentEventByClient[] = [];
|
|
|
|
let filterData: Object = {
|
|
dateDay: {
|
|
$gte: dayjs(new Date()).clone().startOf("day").toDate(),
|
|
},
|
|
companyId: data.companyId,
|
|
clientId: data.clientId,
|
|
};
|
|
|
|
if (data._id) {
|
|
filterData = {
|
|
_id: data._id,
|
|
};
|
|
}
|
|
|
|
const appointments = await this.Appointments.AppointmentList.find(filterData);
|
|
|
|
for (const appointment of appointments) {
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
continue;
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: String(appointment.employeeId),
|
|
});
|
|
|
|
if (!employee) {
|
|
continue;
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
if (!userEmployee) {
|
|
continue;
|
|
}
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
continue;
|
|
}
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const employeeFirstName = isNull<string>(userEmployee.firstName, "");
|
|
const employeeLastName = isNull<string>(userEmployee.lastName, "");
|
|
const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");
|
|
|
|
const employeeAvatar = userEmployee
|
|
? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(
|
|
appointment.discountId,
|
|
appointment.companyId
|
|
);
|
|
|
|
returnData.push({
|
|
id: appointment.id,
|
|
companyId: String(appointment.companyId),
|
|
clientId: appointment.clientId,
|
|
clientName: clientUserName,
|
|
serviceId: appointment.serviceId,
|
|
serviceName: service.name,
|
|
serviceDescription: service.description,
|
|
collaboratorId: employee.id,
|
|
collaboratorUserId: String(employee.userId),
|
|
collaboratorName: employeeFullName,
|
|
collaboratorAvatar: employeeAvatar,
|
|
appointmentDate: dayjs(appointment.start).toISOString(),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
comments: appointment.comments,
|
|
price: appointment.price,
|
|
length: appointment.length,
|
|
payment: appointment.payment,
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
clientCtaCteBalance: 0,
|
|
});
|
|
}
|
|
|
|
return {
|
|
events: returnData,
|
|
};
|
|
}
|
|
|
|
public async findAppointmentsByUser(
|
|
data: FindAppointmentsByUserParams
|
|
): Promise<AppointmentAdminByClientView> {
|
|
const returnData: AppointmentEventByClient[] = [];
|
|
|
|
let filterData: Object = {
|
|
dateDay: {
|
|
$gte: dayjs(new Date()).clone().startOf("day").toDate(),
|
|
},
|
|
userId: data.sessionUser,
|
|
};
|
|
|
|
const appointments = await this.Appointments.AppointmentList.find(filterData).sort({
|
|
start: 1,
|
|
});
|
|
|
|
for (const appointment of appointments) {
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
continue;
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: String(appointment.employeeId),
|
|
});
|
|
|
|
if (!employee) {
|
|
continue;
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
if (!userEmployee) {
|
|
continue;
|
|
}
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
continue;
|
|
}
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const employeeFirstName = isNull<string>(userEmployee.firstName, "");
|
|
const employeeLastName = isNull<string>(userEmployee.lastName, "");
|
|
const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");
|
|
|
|
const employeeAvatar = userEmployee
|
|
? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(
|
|
appointment.discountId,
|
|
appointment.companyId
|
|
);
|
|
|
|
returnData.push({
|
|
id: appointment.id,
|
|
companyId: String(appointment.companyId),
|
|
clientId: appointment.clientId,
|
|
clientName: clientUserName,
|
|
serviceId: appointment.serviceId,
|
|
serviceName: service.name,
|
|
serviceDescription: service.description,
|
|
collaboratorId: employee.id,
|
|
collaboratorUserId: String(employee.userId),
|
|
collaboratorName: employeeFullName,
|
|
collaboratorAvatar: employeeAvatar,
|
|
appointmentDate: dayjs(appointment.start).toISOString(),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
comments: appointment.comments,
|
|
price: appointment.price,
|
|
length: appointment.length,
|
|
payment: appointment.payment,
|
|
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
clientCtaCteBalance: 0,
|
|
});
|
|
}
|
|
|
|
return {
|
|
events: returnData,
|
|
};
|
|
}
|
|
|
|
public async findAppointmentsByCollaborator(
|
|
data: FindAppointmentsByCollaboratorParams
|
|
): Promise<AppointmentAdminByClientView> {
|
|
const returnData: AppointmentEventByClient[] = [];
|
|
|
|
const employeeProfiles = await EmployeesList.employees.find({
|
|
userId: data.sessionUser,
|
|
});
|
|
|
|
if (!employeeProfiles || employeeProfiles.length === 0) {
|
|
return { events: returnData };
|
|
}
|
|
|
|
const employeeIds = employeeProfiles.map((e: any) => e.id || e._id);
|
|
|
|
let filterData: any = {
|
|
dateDay: {
|
|
$gte: dayjs(new Date()).clone().startOf("day").toDate(),
|
|
},
|
|
employeeId: { $in: employeeIds },
|
|
};
|
|
|
|
const appointments = await this.Appointments.AppointmentList.find(filterData).sort({
|
|
start: 1,
|
|
});
|
|
|
|
for (const appointment of appointments) {
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
continue;
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = employeeProfiles.find((e: any) => String(e.id || e._id) === String(appointment.employeeId));
|
|
|
|
if (!employee) {
|
|
continue;
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
if (!userEmployee) {
|
|
continue;
|
|
}
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
continue;
|
|
}
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const employeeFirstName = isNull<string>(userEmployee.firstName, "");
|
|
const employeeLastName = isNull<string>(userEmployee.lastName, "");
|
|
const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");
|
|
|
|
const employeeAvatar = userEmployee
|
|
? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(
|
|
appointment.discountId,
|
|
appointment.companyId
|
|
);
|
|
|
|
returnData.push({
|
|
id: appointment.id,
|
|
companyId: String(appointment.companyId),
|
|
clientId: appointment.clientId,
|
|
clientName: clientUserName,
|
|
serviceId: appointment.serviceId,
|
|
serviceName: service.name,
|
|
serviceDescription: service.description,
|
|
collaboratorId: String(employee.id || (employee as any)._id),
|
|
collaboratorUserId: String(employee.userId),
|
|
collaboratorName: employeeFullName,
|
|
collaboratorAvatar: employeeAvatar,
|
|
appointmentDate: dayjs(appointment.start).toISOString(),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
comments: appointment.comments,
|
|
price: appointment.price,
|
|
length: appointment.length,
|
|
payment: appointment.payment,
|
|
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
clientCtaCteBalance: 0,
|
|
});
|
|
}
|
|
|
|
return {
|
|
events: returnData,
|
|
};
|
|
}
|
|
|
|
|
|
public async findAppointmentsByUserPaginated(
|
|
data: FindAppointmentsByUserPaginatedParams
|
|
): Promise<PaginatedAppointmentEventByClientResult> {
|
|
const returnData: AppointmentEventByClient[] = [];
|
|
|
|
let filterData: Object = {
|
|
dateDay: {
|
|
$gte: dayjs(new Date()).clone().startOf("day").toDate(),
|
|
},
|
|
userId: data.sessionUser,
|
|
};
|
|
|
|
const total = await this.Appointments.AppointmentList.countDocuments(filterData);
|
|
const limit = data.limit || 10;
|
|
const page = data.page || 1;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const appointments = await this.Appointments.AppointmentList.find(filterData)
|
|
.sort({ start: 1 })
|
|
.skip(skip)
|
|
.limit(limit);
|
|
|
|
for (const appointment of appointments) {
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
continue;
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: String(appointment.employeeId),
|
|
});
|
|
|
|
if (!employee) {
|
|
continue;
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
if (!userEmployee) {
|
|
continue;
|
|
}
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
continue;
|
|
}
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const employeeFirstName = isNull<string>(userEmployee.firstName, "");
|
|
const employeeLastName = isNull<string>(userEmployee.lastName, "");
|
|
const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");
|
|
|
|
const employeeAvatar = userEmployee
|
|
? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(
|
|
appointment.discountId,
|
|
appointment.companyId
|
|
);
|
|
|
|
returnData.push({
|
|
id: appointment.id,
|
|
companyId: String(appointment.companyId),
|
|
clientId: appointment.clientId,
|
|
clientName: clientUserName,
|
|
serviceId: appointment.serviceId,
|
|
serviceName: service.name,
|
|
serviceDescription: service.description,
|
|
collaboratorId: employee.id,
|
|
collaboratorUserId: String(employee.userId),
|
|
collaboratorName: employeeFullName,
|
|
collaboratorAvatar: employeeAvatar,
|
|
appointmentDate: dayjs(appointment.start).toISOString(),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
comments: appointment.comments,
|
|
price: appointment.price,
|
|
length: appointment.length,
|
|
payment: appointment.payment,
|
|
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
clientCtaCteBalance: 0,
|
|
});
|
|
}
|
|
|
|
return {
|
|
data: returnData,
|
|
page,
|
|
pages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
|
|
|
|
public async findAppointmentsByCollaboratorPaginated(
|
|
data: FindAppointmentsByCollaboratorPaginatedParams
|
|
): Promise<PaginatedAppointmentEventByClientResult> {
|
|
const returnData: AppointmentEventByClient[] = [];
|
|
|
|
const employeeProfiles = await EmployeesList.employees.find({
|
|
userId: data.sessionUser,
|
|
});
|
|
|
|
if (!employeeProfiles || employeeProfiles.length === 0) {
|
|
return { data: returnData, page: data.page || 1, pages: 0 };
|
|
}
|
|
|
|
const employeeIds = employeeProfiles.map((e: any) => e.id || e._id);
|
|
|
|
let filterData: any = {
|
|
dateDay: {
|
|
$gte: dayjs(new Date()).clone().startOf("day").toDate(),
|
|
},
|
|
employeeId: { $in: employeeIds },
|
|
};
|
|
|
|
const total = await this.Appointments.AppointmentList.countDocuments(filterData);
|
|
const limit = data.limit || 10;
|
|
const page = data.page || 1;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const appointments = await this.Appointments.AppointmentList.find(filterData)
|
|
.sort({ start: 1 })
|
|
.skip(skip)
|
|
.limit(limit);
|
|
|
|
for (const appointment of appointments) {
|
|
const client = await ClientsManager.clients.findOne({
|
|
_id: String(appointment.clientId),
|
|
});
|
|
|
|
if (!client) {
|
|
continue;
|
|
}
|
|
|
|
const userClient = await UsersManager.users.findOne({
|
|
_id: client.userId ? String(client.userId) : undefined,
|
|
});
|
|
|
|
const employee = employeeProfiles.find((e: any) => String(e.id || e._id) === String(appointment.employeeId));
|
|
|
|
if (!employee) {
|
|
continue;
|
|
}
|
|
|
|
const userEmployee = await UsersManager.users.findOne({
|
|
_id: String(employee.userId),
|
|
});
|
|
|
|
if (!userEmployee) {
|
|
continue;
|
|
}
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
continue;
|
|
}
|
|
|
|
const clientName = joinStrings([client.firstName, client.lastName], " ");
|
|
const clientUserName = userClient
|
|
? joinStrings([userClient.firstName, userClient.lastName], clientName)
|
|
: joinStrings([client.firstName, client.lastName], " ");
|
|
const clientAvatar = userClient
|
|
? getAvatar(client.userId, userClient.avatar, clientUserName)
|
|
: clientUserName;
|
|
|
|
const employeeFirstName = isNull<string>(userEmployee.firstName, "");
|
|
const employeeLastName = isNull<string>(userEmployee.lastName, "");
|
|
const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");
|
|
|
|
const employeeAvatar = userEmployee
|
|
? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
|
|
: clientUserName;
|
|
|
|
const discountData = await Discounts.getDiscountData(
|
|
appointment.discountId,
|
|
appointment.companyId
|
|
);
|
|
|
|
returnData.push({
|
|
id: appointment.id,
|
|
companyId: String(appointment.companyId),
|
|
clientId: appointment.clientId,
|
|
clientName: clientUserName,
|
|
serviceId: appointment.serviceId,
|
|
serviceName: service.name,
|
|
serviceDescription: service.description,
|
|
collaboratorId: String(employee.id || (employee as any)._id),
|
|
collaboratorUserId: String(employee.userId),
|
|
collaboratorName: employeeFullName,
|
|
collaboratorAvatar: employeeAvatar,
|
|
appointmentDate: dayjs(appointment.start).toISOString(),
|
|
startTime: appointment.startHour,
|
|
endTime: appointment.endHour,
|
|
title: isNull<string>(clientName, clientUserName),
|
|
avatar: clientAvatar,
|
|
color: isNull<string>(employee.calendarColor, "#ff9900"),
|
|
comments: appointment.comments,
|
|
price: appointment.price,
|
|
length: appointment.length,
|
|
payment: appointment.payment,
|
|
|
|
discountId: isNull<string>(appointment.discountId, ""),
|
|
discountName: isNull<string>(discountData.name, ""),
|
|
discountCode: isNull<string>(discountData.code, ""),
|
|
discountValue: isNull<number>(discountData.value, 0),
|
|
discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
|
|
clientCtaCteBalance: 0,
|
|
});
|
|
}
|
|
|
|
return {
|
|
data: returnData,
|
|
page,
|
|
pages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
|
|
|
|
|
|
public async updateAppointment(data: UpdateAppointmentParams): Promise<void> {
|
|
const { notification = true, validation = false } = data;
|
|
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 checkAppointment = await this.Appointments.findOne({
|
|
_id: data.id,
|
|
});
|
|
|
|
if (!checkAppointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const companyCheck = await CompaniesManager.companies.findOne({
|
|
_id: String(checkAppointment.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 serviceCheck = await ServicesManager.services.findOne({
|
|
_id: String(checkAppointment.serviceId),
|
|
companyId: String(checkAppointment.companyId),
|
|
});
|
|
|
|
if (!serviceCheck) {
|
|
throw new Error("El servicio no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
const appointmentLength = data.length ? data.length : checkAppointment.length;
|
|
|
|
const start = dayjs(data.start).clone();
|
|
const end = dayjs(data.start)
|
|
.clone()
|
|
.add(serviceCheck.length * isNull<number>(appointmentLength, 1), "minutes");
|
|
|
|
const startHour = start.clone().hour() + start.clone().minute() / 60;
|
|
const endHour = end.clone().hour() + end.clone().minute() / 60;
|
|
const dateDay = dayjs(data.start).clone().startOf("day").toDate();
|
|
|
|
const originalConfig = this.getHeatMapConfig(checkAppointment, serviceCheck.length);
|
|
const originalDateDay = dayjs(checkAppointment.dateDay).clone();
|
|
|
|
const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);
|
|
|
|
const availabilityConfig: HeatMapConfig = {
|
|
companyId: String(checkAppointment.companyId),
|
|
serviceId: String(checkAppointment.serviceId),
|
|
employeeId: String(checkAppointment.employeeId),
|
|
heatMapDate: dayjs(data.start).startOf("day").toDate(),
|
|
start: dayjs(data.start).toDate(),
|
|
fraction: defaultFraction, //companyCheck.heatMapFraction ? companyCheck.heatMapFraction : defaultFraction,
|
|
length: isNull<number>(appointmentLength, 1),
|
|
serviceLimit: serviceCheck.limit,
|
|
serviceLength: serviceCheck.length,
|
|
};
|
|
|
|
const availabilityData = {
|
|
companyId: String(checkAppointment.companyId),
|
|
employeeId: String(checkAppointment.employeeId),
|
|
serviceId: String(checkAppointment.serviceId),
|
|
start: dayjs(data.start).toDate(),
|
|
length: isNull<number>(appointmentLength, 1),
|
|
dateDay: dateDay,
|
|
price: 0,
|
|
comments: "",
|
|
sessionUser: data.sessionUser,
|
|
};
|
|
|
|
const newStart = dayjs(data.start).toDate();
|
|
|
|
if (validation) {
|
|
const checkAvailable = await this.checkAvailability(availabilityConfig, availabilityData);
|
|
|
|
//Solo chequeo disponibilidad si se cambia el start o el length del turno, ya que es una modificacion.
|
|
if (newStart !== checkAppointment.start || data.length !== checkAppointment.length) {
|
|
if (checkAvailable === false) {
|
|
throw new Error("No hay disponibilidad para el horario seleccionado");
|
|
}
|
|
}
|
|
}
|
|
|
|
checkAppointment.start = newStart;
|
|
checkAppointment.startHour = startHour;
|
|
checkAppointment.endHour = endHour;
|
|
checkAppointment.dateDay = dateDay;
|
|
|
|
if (data.price) {
|
|
checkAppointment.price = data.price;
|
|
}
|
|
|
|
if (data.length) {
|
|
checkAppointment.length = data.length;
|
|
}
|
|
|
|
if (data.comments) {
|
|
checkAppointment.comments = data.comments;
|
|
}
|
|
|
|
try {
|
|
await checkAppointment.save();
|
|
|
|
const checkClient = await ClientsManager.clients.findOne({
|
|
_id: String(checkAppointment.clientId),
|
|
});
|
|
|
|
if (!checkClient) {
|
|
throw new Error("El cliente no existe");
|
|
}
|
|
|
|
const employeeCheck = await EmployeesList.employees.findOne({
|
|
_id: String(checkAppointment.employeeId),
|
|
});
|
|
|
|
if (!employeeCheck) {
|
|
throw new Error("El colaborador no existe");
|
|
}
|
|
|
|
if (notification && checkClient.userId) {
|
|
//Envio la notificacion al cliente.
|
|
await NotificationsManager.sendSystemNotification({
|
|
userId: String(checkClient.userId),
|
|
subject: "Turno actualizado",
|
|
message: `Se ha actualizado un turno para el día ${dayjs(data.start).format(
|
|
"DD/MM/YYYY"
|
|
)} a las ${dayjs(data.start).format("HH:mm")}`,
|
|
type: NotificationType.APPOINTMENT,
|
|
code: String(checkAppointment._id)
|
|
});
|
|
|
|
//Cancel existing pending jobs before creating new ones for the rescheduled appointment
|
|
if (checkAppointment.id) {
|
|
await this.jobService.cancelByAppointment(String(checkAppointment.id));
|
|
|
|
//Create jobs for email and whatsapp notifications
|
|
await this.jobService.createJob({
|
|
companyId: String(companyCheck._id),
|
|
clientId: String(checkClient._id),
|
|
appointmentId: String(checkAppointment.id),
|
|
type: "update" as const,
|
|
appointmentStart: new Date(data.start),
|
|
});
|
|
}
|
|
}
|
|
|
|
//Envio la notificacion al profesional.
|
|
await NotificationsManager.sendSystemNotification({
|
|
userId: String(employeeCheck.userId),
|
|
subject: "Turno actualizado",
|
|
message: `Se ha actualizado un turno para el cliente ${ClientsManager.getClientFullName(
|
|
checkClient
|
|
)} para el día ${dayjs(data.start).format("DD/MM/YYYY")} a las ${dayjs(data.start).format(
|
|
"HH:mm"
|
|
)}`,
|
|
type: NotificationType.APPOINTMENT,
|
|
code: String(checkAppointment._id)
|
|
});
|
|
|
|
//Actualiza el heatmap de la fecha nueva.
|
|
await this.updateHeatMap(availabilityConfig);
|
|
|
|
if (!originalDateDay.isSame(dateDay)) {
|
|
//actualizar el heatmap de la fecha original
|
|
await this.updateHeatMap(originalConfig);
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
throw new Error("Ha ocurrido un error al actualizar el turno.");
|
|
}
|
|
}
|
|
|
|
private getHeatMapConfig(
|
|
appointment: IAppointment,
|
|
serviceLength: number,
|
|
heatMapDate?: Date,
|
|
start?: Date
|
|
): HeatMapConfig {
|
|
const hmDate = heatMapDate ? heatMapDate : dayjs(appointment.start).startOf("day").toDate();
|
|
const hmStart = start ? start : dayjs(appointment.start).toDate();
|
|
|
|
const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);
|
|
|
|
const config: HeatMapConfig = {
|
|
companyId: appointment.companyId,
|
|
serviceId: appointment.serviceId,
|
|
employeeId: appointment.employeeId,
|
|
heatMapDate: hmDate,
|
|
start: hmStart,
|
|
fraction: defaultFraction,
|
|
length: isNull<number>(appointment.length, 1),
|
|
serviceLimit: 1, //el service limit no importa si no voy a validar si hay lugar.
|
|
serviceLength: serviceLength,
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
private async createCancellationNotificationJobs(data: {
|
|
appointmentId: string;
|
|
companyId: string;
|
|
clientId: string;
|
|
clientUserId?: string;
|
|
clientEmail?: string;
|
|
clientPhoneNumber?: string;
|
|
companyOwnerId: string;
|
|
companyName: string;
|
|
emailMessage: string;
|
|
wapMessage: string;
|
|
}): Promise<void> {
|
|
const systemSubject = `Turno cancelado en ${data.companyName}`;
|
|
|
|
await this.jobService.createJob({
|
|
companyId: data.companyId,
|
|
clientId: data.clientId,
|
|
appointmentId: data.appointmentId,
|
|
type: "cancellation",
|
|
scheduledAt: new Date(),
|
|
payload: {
|
|
email: data.clientEmail,
|
|
phoneNumber: data.clientPhoneNumber,
|
|
userId: data.clientUserId,
|
|
companyOwnerId: data.companyOwnerId,
|
|
subject: systemSubject,
|
|
message: data.emailMessage,
|
|
emailSubject: "TurnosXpress :: Turno cancelado",
|
|
emailMessage: data.emailMessage,
|
|
wapMessage: data.wapMessage,
|
|
systemSubject,
|
|
systemMessage: data.emailMessage,
|
|
},
|
|
});
|
|
}
|
|
|
|
private async createCreationNotificationJobs(data: {
|
|
appointmentId: string;
|
|
companyId: string;
|
|
clientId: string;
|
|
clientUserId?: string;
|
|
clientEmail?: string;
|
|
clientPhoneNumber?: string;
|
|
companyOwnerId: string;
|
|
companyName: string;
|
|
appointmentStart: Date;
|
|
emailMessage: string;
|
|
wapMessage: string;
|
|
}): Promise<void> {
|
|
const systemSubject = `Turno reservado en ${data.companyName}`;
|
|
|
|
await this.jobService.createJob({
|
|
companyId: data.companyId,
|
|
clientId: data.clientId,
|
|
appointmentId: data.appointmentId,
|
|
type: "creation",
|
|
appointmentStart: data.appointmentStart,
|
|
payload: {
|
|
email: data.clientEmail,
|
|
phoneNumber: data.clientPhoneNumber,
|
|
userId: data.clientUserId,
|
|
companyOwnerId: data.companyOwnerId,
|
|
subject: systemSubject,
|
|
message: data.emailMessage,
|
|
emailSubject: "TurnosXpress :: Alta de turno",
|
|
emailMessage: data.emailMessage,
|
|
wapMessage: data.wapMessage,
|
|
systemSubject,
|
|
systemMessage: data.emailMessage,
|
|
},
|
|
});
|
|
}
|
|
|
|
private async getOptionalClientWapNumber(client: IClientDocument): Promise<string | undefined> {
|
|
try {
|
|
return await ClientsManager.getClientWapNumber(client);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
public async deleteAppointment(data: DeleteAppointmentParams): Promise<void> {
|
|
const { validation = false, notification = false } = data;
|
|
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 checkAppointment = await this.Appointments.findOne({
|
|
_id: data.id,
|
|
});
|
|
|
|
if (!checkAppointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
if (checkAppointment.payment === true) {
|
|
throw new Error("No se puede cancelar un turno que se encuentra pagado.");
|
|
}
|
|
|
|
const companyCheck = await CompaniesManager.companies.findOne({
|
|
_id: String(checkAppointment.companyId),
|
|
});
|
|
|
|
if (!companyCheck) {
|
|
throw new Error("La compañia no existe");
|
|
}
|
|
|
|
//Chequeo el empleado.
|
|
const employeeCheck = await EmployeesList.employees.findOne({
|
|
_id: String(checkAppointment.employeeId),
|
|
});
|
|
|
|
if (!employeeCheck) {
|
|
throw new Error("El colaborador no existe");
|
|
}
|
|
|
|
//Si el sessionUser es el cliente, si puede borrarlo.
|
|
const checkClient = await ClientsManager.clients.findOne({
|
|
_id: String(checkAppointment.clientId),
|
|
});
|
|
|
|
if (!checkClient) {
|
|
throw new Error("El cliente no existe");
|
|
}
|
|
|
|
//TODO: Check cancellationTime.
|
|
if (
|
|
!this.validateCancellationTime(companyCheck.cancellationTime, checkAppointment.start) &&
|
|
validation
|
|
) {
|
|
throw new Error(
|
|
`No se puede cancelar el turno con menos de ${this.getCancellationTime(
|
|
companyCheck.cancellationTime
|
|
)} horas de anticipación.`
|
|
);
|
|
}
|
|
|
|
if (String(checkClient.userId) !== data.sessionUser) {
|
|
//Solo valida si el usuario que intenta borrar no es el cliente.
|
|
if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
|
|
throw new Error(NoPermissionMessage());
|
|
}
|
|
}
|
|
|
|
const serviceCheck = await ServicesManager.services.findOne({
|
|
_id: String(checkAppointment.serviceId),
|
|
companyId: String(checkAppointment.companyId),
|
|
});
|
|
|
|
if (!serviceCheck) {
|
|
throw new Error("El servicio no existe o no pertenece a la compañia.");
|
|
}
|
|
|
|
try {
|
|
const config = this.getHeatMapConfig(checkAppointment, serviceCheck.length);
|
|
|
|
// Cancel pending notification jobs for this appointment before deleting it
|
|
await this.jobService.cancelByAppointment(String(checkAppointment._id));
|
|
|
|
if (notification) {
|
|
const emailContent = await this.tryToSendNotification({
|
|
appointmentId: String(checkAppointment._id),
|
|
sessionUser: data.sessionUser,
|
|
type: APPOINTMENT_NOTIFICATION_TYPE.CANCELLATION,
|
|
channel: "email",
|
|
});
|
|
const wapContent = await this.tryToSendNotification({
|
|
appointmentId: String(checkAppointment._id),
|
|
sessionUser: data.sessionUser,
|
|
type: APPOINTMENT_NOTIFICATION_TYPE.CANCELLATION,
|
|
channel: "whatsapp",
|
|
});
|
|
|
|
await this.createCancellationNotificationJobs({
|
|
companyId: String(checkAppointment.companyId),
|
|
clientId: String(checkAppointment.clientId),
|
|
appointmentId: String(checkAppointment._id),
|
|
clientUserId: checkClient.userId ? String(checkClient.userId) : undefined,
|
|
clientEmail: checkClient.email,
|
|
clientPhoneNumber: await this.getOptionalClientWapNumber(checkClient),
|
|
companyOwnerId: String(companyCheck.ownerId),
|
|
companyName: companyCheck.name,
|
|
emailMessage: emailContent.message,
|
|
wapMessage: wapContent.message,
|
|
});
|
|
}
|
|
|
|
await this.Appointments.AppointmentList.deleteOne({ _id: data.id });
|
|
//Actualizar el heatmap.
|
|
await this.updateHeatMap(config);
|
|
|
|
//Enviamos la notificacion de cancelacion al profesional
|
|
await NotificationsManager.sendSystemNotification({
|
|
userId: String(employeeCheck.userId),
|
|
subject: "Turno Cancelado",
|
|
message: `Se ha cancelado un turno para el cliente ${ClientsManager.getClientFullName(
|
|
checkClient
|
|
)} el día ${dayjs(checkAppointment.start).format("DD/MM/YYYY")} a las ${dayjs(
|
|
checkAppointment.start
|
|
).format("HH:mm")}`,
|
|
type: NotificationType.APPOINTMENT,
|
|
code: String(checkAppointment._id)
|
|
});
|
|
} catch (error) {
|
|
console.log(error);
|
|
throw new Error("Ha ocurrido un error al eliminar el turno.");
|
|
}
|
|
}
|
|
|
|
public async getSchedules(data: FindAppointmentSchedulesParams): Promise<TextObjectFilterResult[]> {
|
|
const returnData: TextObjectFilterResult[] = [];
|
|
|
|
const dateDay = dayjs(data.dateDay).startOf("day");
|
|
const weekDay = dateDay.weekday() + 1;
|
|
|
|
const schedules: CollaboratorSchedulesView = await SchedulesList.findAllSchedulesByCollaborator({
|
|
companyId: data.companyId,
|
|
employeeId: data.employeeId,
|
|
weekDay: weekDay,
|
|
});
|
|
|
|
const hasOverride = await SchedulesOverridesList.findOne({
|
|
companyId: data.companyId,
|
|
employeeId: data.employeeId,
|
|
date: dateDay.toDate(),
|
|
});
|
|
|
|
const listOfSchedules = hasOverride ? hasOverride.schedules : schedules.schedules;
|
|
|
|
const service = await ServiceList.services.findOne({
|
|
_id: data.serviceId,
|
|
});
|
|
|
|
if (!service) {
|
|
console.log("El servicio no existe");
|
|
return [];
|
|
}
|
|
|
|
const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);
|
|
|
|
for (const schedule of listOfSchedules) {
|
|
if (schedule.disabled) {
|
|
continue;
|
|
}
|
|
|
|
if (!this.isScheduleAvailableForService(schedule, data.serviceId)) {
|
|
continue;
|
|
}
|
|
|
|
const scheduleDataFrom = schedule.from.split(":");
|
|
const scheduleDataTo = schedule.to.split(":");
|
|
const hourFrom = parseInt(scheduleDataFrom[0]);
|
|
const hourTo = parseInt(scheduleDataTo[0]);
|
|
const minutesFrom = parseInt(scheduleDataFrom[1]);
|
|
const minutesTo = parseInt(scheduleDataTo[1]);
|
|
const startHour = dateDay.clone().hour(hourFrom).minute(minutesFrom);
|
|
const endHour = dateDay.clone().hour(hourTo).minute(minutesTo);
|
|
|
|
let currentHour = startHour.clone();
|
|
while (currentHour.isBefore(endHour)) {
|
|
const availabilityConfig: HeatMapConfig = {
|
|
companyId: data.companyId,
|
|
serviceId: data.serviceId,
|
|
employeeId: data.employeeId,
|
|
heatMapDate: dayjs(dateDay).startOf("day").toDate(),
|
|
start: dayjs(currentHour).toDate(),
|
|
fraction: defaultFraction,
|
|
length: 1,
|
|
serviceLimit: service.limit,
|
|
serviceLength: service.length,
|
|
};
|
|
|
|
const available = await this.checkAvailability(availabilityConfig, {
|
|
companyId: data.companyId,
|
|
serviceId: data.serviceId,
|
|
employeeId: data.employeeId,
|
|
userId: "",
|
|
start: dayjs(currentHour).toDate(),
|
|
startHour: currentHour.hour(),
|
|
dateDay: dateDay.toDate(),
|
|
length: 1,
|
|
price: 0,
|
|
sessionUser: "",
|
|
});
|
|
|
|
if (available) {
|
|
returnData.push({
|
|
id: currentHour.toISOString(),
|
|
name:
|
|
currentHour.format("HH:mm") +
|
|
"hs. - " +
|
|
currentHour.add(service.length, "minute").format("HH:mm") +
|
|
"hs.",
|
|
});
|
|
}
|
|
|
|
currentHour = currentHour
|
|
.clone()
|
|
.add(isNull<number>(service.length, defaultFraction), "minutes");
|
|
}
|
|
}
|
|
|
|
return returnData;
|
|
}
|
|
|
|
private async tryToSendNotification(
|
|
data: SendAppointmentNotificationParams
|
|
): Promise<AppointmentNotificationIntent> {
|
|
const checkAppointment = await this.Appointments.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!checkAppointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const companyCheck = await CompaniesManager.companies.findOne({
|
|
_id: String(checkAppointment.companyId),
|
|
});
|
|
|
|
if (!companyCheck) {
|
|
throw new Error("La compañia no existe");
|
|
}
|
|
|
|
const hasSystemToken = this.hasValidSystemToken(data.systemToken);
|
|
|
|
if (!hasSystemToken) {
|
|
if (!data.sessionUser) {
|
|
throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
|
|
}
|
|
|
|
if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
|
|
throw new Error(NoPermissionMessage());
|
|
}
|
|
}
|
|
|
|
const checkClient = await ClientsManager.clients.findOne({
|
|
_id: String(checkAppointment.clientId),
|
|
});
|
|
|
|
if (!checkClient) {
|
|
throw new Error("El cliente no existe");
|
|
}
|
|
|
|
const checkEmployee = await EmployeesList.employees.findOne({
|
|
_id: String(checkAppointment.employeeId),
|
|
});
|
|
|
|
if (!checkEmployee) {
|
|
throw new Error("El profesional no existe");
|
|
}
|
|
|
|
const checkEmployeeUser = await UsersManager.users.findOne({
|
|
_id: String(checkEmployee.userId),
|
|
});
|
|
|
|
if (!checkEmployeeUser) {
|
|
throw new Error("El profesional no tiene usuario asociado");
|
|
}
|
|
|
|
const checkService = await ServiceList.services.findOne({
|
|
_id: String(checkAppointment.serviceId),
|
|
});
|
|
|
|
if (!checkService) {
|
|
throw new Error("El servicio no existe");
|
|
}
|
|
|
|
let wapMessage = "";
|
|
|
|
if (data.type == APPOINTMENT_NOTIFICATION_TYPE.REMINDER) {
|
|
if (companyCheck.templateWapNotifId) {
|
|
const template = await Templates.templates.findOne({
|
|
id: String(companyCheck.templateWapNotifId),
|
|
companyId: String(companyCheck._id),
|
|
});
|
|
|
|
if (template) {
|
|
wapMessage = template.template;
|
|
}
|
|
}
|
|
|
|
if (!wapMessage) {
|
|
wapMessage = process.env.WAP_REMINDER ? process.env.WAP_REMINDER : "";
|
|
}
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---CLIENTE--->",
|
|
ClientsManager.getClientFullName(checkClient)
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---FECHA--->",
|
|
dayjs(checkAppointment.start).format("DD/MM/YYYY")
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---HORARIO--->",
|
|
dayjs(checkAppointment.start).format("HH:mm") + "hs."
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---DIRECCION--->",
|
|
CompaniesManager.getCompanyAddress(companyCheck)
|
|
);
|
|
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---PROFESIONAL--->",
|
|
UsersManager.getUserFullName(checkEmployeeUser)
|
|
);
|
|
|
|
wapMessage = wapMessage.replaceAll("<---SERVICIO--->", checkService.name);
|
|
wapMessage = wapMessage.replaceAll("<---PRECIO--->", formatCurrency(checkAppointment.price));
|
|
|
|
wapMessage = wapMessage.replaceAll("<---ORGANIZACION--->", companyCheck.name);
|
|
wapMessage = wapMessage.replaceAll("<---BR--->", "\r\n");
|
|
} else if (data.type == APPOINTMENT_NOTIFICATION_TYPE.CREATION) {
|
|
const templateId = data.channel === "email"
|
|
? companyCheck.templateEmailAltaId || companyCheck.templateWapAltaId
|
|
: companyCheck.templateWapAltaId;
|
|
|
|
if (templateId) {
|
|
const template = await Templates.templates.findOne({
|
|
id: String(templateId),
|
|
companyId: String(companyCheck._id),
|
|
});
|
|
|
|
if (template) {
|
|
wapMessage = template.template;
|
|
}
|
|
}
|
|
|
|
if (!wapMessage) {
|
|
wapMessage = data.channel === "email"
|
|
? process.env.EMAIL_NEW_APPOINTMENT || process.env.EMAIL_ALTA || process.env.WAP_ALTA || ""
|
|
: process.env.WAP_ALTA || "";
|
|
}
|
|
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---CLIENTE--->",
|
|
ClientsManager.getClientFullName(checkClient)
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---FECHA--->",
|
|
dayjs(checkAppointment.start).format("DD/MM/YYYY")
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---HORARIO--->",
|
|
dayjs(checkAppointment.start).format("HH:mm") + "hs."
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---DIRECCION--->",
|
|
CompaniesManager.getCompanyAddress(companyCheck)
|
|
);
|
|
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---PROFESIONAL--->",
|
|
UsersManager.getUserFullName(checkEmployeeUser)
|
|
);
|
|
|
|
wapMessage = wapMessage.replaceAll("<---SERVICIO--->", checkService.name);
|
|
wapMessage = wapMessage.replaceAll("<---PRECIO--->", formatCurrency(checkAppointment.price));
|
|
|
|
wapMessage = wapMessage.replaceAll("<---ORGANIZACION--->", companyCheck.name);
|
|
wapMessage = wapMessage.replaceAll("<---BR--->", "\r\n");
|
|
} else if (data.type == APPOINTMENT_NOTIFICATION_TYPE.CANCELLATION) {
|
|
const templateId = data.channel === "email"
|
|
? companyCheck.templateEmailCancellationId || companyCheck.templateWapCancellationId
|
|
: companyCheck.templateWapCancellationId;
|
|
|
|
if (templateId) {
|
|
const template = await Templates.templates.findOne({
|
|
id: String(templateId),
|
|
companyId: String(companyCheck._id),
|
|
});
|
|
|
|
if (template) {
|
|
wapMessage = template.template;
|
|
}
|
|
}
|
|
|
|
if (!wapMessage) {
|
|
wapMessage = data.channel === "email"
|
|
? process.env.EMAIL_CANCELLATION || process.env.EMAIL_CANCELACION || process.env.WAP_CANCELLATION || process.env.WAP_CANCELACION || ""
|
|
: process.env.WAP_CANCELLATION || process.env.WAP_CANCELACION || "";
|
|
}
|
|
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---CLIENTE--->",
|
|
ClientsManager.getClientFullName(checkClient)
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---FECHA--->",
|
|
dayjs(checkAppointment.start).format("DD/MM/YYYY")
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---HORARIO--->",
|
|
dayjs(checkAppointment.start).format("HH:mm") + "hs."
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---DIRECCION--->",
|
|
CompaniesManager.getCompanyAddress(companyCheck)
|
|
);
|
|
wapMessage = wapMessage.replaceAll(
|
|
"<---PROFESIONAL--->",
|
|
UsersManager.getUserFullName(checkEmployeeUser)
|
|
);
|
|
wapMessage = wapMessage.replaceAll("<---SERVICIO--->", checkService.name);
|
|
wapMessage = wapMessage.replaceAll("<---PRECIO--->", formatCurrency(checkAppointment.price));
|
|
wapMessage = wapMessage.replaceAll("<---ORGANIZACION--->", companyCheck.name);
|
|
wapMessage = wapMessage.replaceAll("<---BR--->", "\r\n");
|
|
}
|
|
|
|
return {
|
|
message: wapMessage,
|
|
checkClient: checkClient,
|
|
companyCheck: companyCheck,
|
|
};
|
|
}
|
|
|
|
public async sendEmailNotification(data: SendAppointmentNotificationParams): Promise<void> {
|
|
if (!this.hasValidSystemToken(data.systemToken)) {
|
|
if (!data.sessionUser) {
|
|
throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
|
|
}
|
|
|
|
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 { message, checkClient, companyCheck } = await this.tryToSendNotification({
|
|
...data,
|
|
channel: "email",
|
|
});
|
|
|
|
const canSend = await PlanSubscriptionsList.checkFeature({
|
|
userId: String(companyCheck.ownerId),
|
|
feature: PlanFeatures.MAIL_NOTIFICATIONS,
|
|
});
|
|
|
|
if (!canSend) {
|
|
throw new Error("La organizacion no tiene un plan que soporte notificaciones por email");
|
|
}
|
|
|
|
const clientEmail = checkClient.email;
|
|
|
|
if (!clientEmail || clientEmail == "") {
|
|
throw new Error("El cliente no tiene un número telefónico asociado");
|
|
}
|
|
|
|
let subjectEmail = "";
|
|
|
|
if (data.type == APPOINTMENT_NOTIFICATION_TYPE.REMINDER) {
|
|
subjectEmail = "TurnosXpress :: Recordatorio";
|
|
} else if (data.type == APPOINTMENT_NOTIFICATION_TYPE.CREATION) {
|
|
subjectEmail = "TurnosXpress :: Alta de turno";
|
|
} else if (data.type == APPOINTMENT_NOTIFICATION_TYPE.CANCELLATION) {
|
|
subjectEmail = "TurnosXpress :: Turno cancelado";
|
|
}
|
|
|
|
//Enviar email...
|
|
await NotificationsManager.sendEmail({
|
|
email: clientEmail,
|
|
subject: subjectEmail,
|
|
message: message,
|
|
});
|
|
}
|
|
|
|
public async createImmediateNotificationJobs(data: CreateImmediateAppointmentNotificationJobsParams): Promise<void> {
|
|
if (![APPOINTMENT_NOTIFICATION_TYPE.CREATION, APPOINTMENT_NOTIFICATION_TYPE.REMINDER].includes(data.type)) {
|
|
throw new Error("El tipo de notificación no es válido para esta acción");
|
|
}
|
|
|
|
const emailContent = await this.tryToSendNotification({
|
|
appointmentId: data.appointmentId,
|
|
sessionUser: data.sessionUser,
|
|
type: data.type,
|
|
channel: "email",
|
|
});
|
|
const wapContent = await this.tryToSendNotification({
|
|
appointmentId: data.appointmentId,
|
|
sessionUser: data.sessionUser,
|
|
type: data.type,
|
|
channel: "whatsapp",
|
|
});
|
|
|
|
const checkAppointment = await this.Appointments.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!checkAppointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const checkClient = emailContent.checkClient;
|
|
const companyCheck = emailContent.companyCheck;
|
|
const isCreation = data.type === APPOINTMENT_NOTIFICATION_TYPE.CREATION;
|
|
const systemSubject = isCreation
|
|
? `Turno reservado en ${companyCheck.name}`
|
|
: `Recordatorio de turno en ${companyCheck.name}`;
|
|
|
|
await this.jobService.createImmediateJob({
|
|
companyId: String(checkAppointment.companyId),
|
|
clientId: String(checkAppointment.clientId),
|
|
appointmentId: String(checkAppointment._id),
|
|
type: data.type,
|
|
scheduledAt: new Date(),
|
|
payload: {
|
|
email: checkClient.email,
|
|
phoneNumber: await this.getOptionalClientWapNumber(checkClient),
|
|
userId: checkClient.userId ? String(checkClient.userId) : undefined,
|
|
companyOwnerId: String(companyCheck.ownerId),
|
|
subject: systemSubject,
|
|
message: emailContent.message,
|
|
emailSubject: isCreation ? "TurnosXpress :: Alta de turno" : "TurnosXpress :: Recordatorio",
|
|
emailMessage: emailContent.message,
|
|
wapMessage: wapContent.message,
|
|
systemSubject,
|
|
systemMessage: emailContent.message,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async sendWapNotification(data: SendAppointmentNotificationParams): Promise<void> {
|
|
if (!data.sessionUser) {
|
|
throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
|
|
}
|
|
|
|
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 { message, checkClient, companyCheck } = await this.tryToSendNotification({
|
|
...data,
|
|
channel: "whatsapp",
|
|
});
|
|
|
|
const canSend = await PlanSubscriptionsList.checkFeature({
|
|
userId: String(companyCheck.ownerId),
|
|
feature: PlanFeatures.WAP_NOTIFICATIONS,
|
|
});
|
|
|
|
if (!canSend) {
|
|
throw new Error("La organizacion no tiene un plan que soporte notificaciones por Whatsapp");
|
|
}
|
|
|
|
const wapNumber = await ClientsManager.getClientWapNumber(checkClient);
|
|
|
|
if (!wapNumber || wapNumber == "") {
|
|
throw new Error("El cliente no tiene un número telefónico asociado");
|
|
}
|
|
|
|
await NotificationsManager.sendWap({
|
|
phoneNumber: wapNumber,
|
|
message: message,
|
|
companyId: String(companyCheck._id),
|
|
sessionUser: data.sessionUser,
|
|
});
|
|
}
|
|
|
|
public async sendWapSystemNotification(data: SendWapSystemNotificationParams): Promise<void> {
|
|
if (!this.hasValidSystemToken(data.systemToken)) {
|
|
throw new Error("No tiene permisos para realizar esta acción");
|
|
}
|
|
|
|
const checkAppointment = await this.Appointments.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!checkAppointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const company = await CompaniesManager.companies.findOne({
|
|
_id: String(checkAppointment.companyId),
|
|
});
|
|
|
|
if (!company) {
|
|
throw new Error("La organización no existe");
|
|
}
|
|
|
|
const { message, checkClient, companyCheck } = await this.tryToSendNotification({
|
|
appointmentId: data.appointmentId,
|
|
sessionUser: String(company.ownerId),
|
|
systemToken: data.systemToken,
|
|
type: data.type,
|
|
});
|
|
|
|
const canSend = await PlanSubscriptionsList.checkFeature({
|
|
userId: String(companyCheck.ownerId),
|
|
feature: PlanFeatures.WAP_NOTIFICATIONS,
|
|
});
|
|
|
|
if (!canSend) {
|
|
throw new Error("La organizacion no tiene un plan que soporte notificaciones por Whatsapp");
|
|
}
|
|
|
|
const wapNumber = await ClientsManager.getClientWapNumber(checkClient);
|
|
|
|
if (!wapNumber || wapNumber == "") {
|
|
throw new Error("El cliente no tiene un número telefónico asociado");
|
|
}
|
|
|
|
await NotificationsManager.sendWap({
|
|
phoneNumber: wapNumber,
|
|
message: message,
|
|
companyId: String(companyCheck._id),
|
|
sessionUser: String(company.ownerId),
|
|
});
|
|
}
|
|
|
|
public async applyDiscount(data: ApplyAppointmentDiscountParams): Promise<void> {
|
|
await validateSessionUser({ sessionUser: data.sessionUser });
|
|
await validatePermissionsByCompany({
|
|
companyId: data.companyId,
|
|
sessionUser: data.sessionUser,
|
|
});
|
|
|
|
const appointment = await this.Appointments.AppointmentList.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
if (appointment.payment) {
|
|
throw new Error("No se puede aplicar un descuento a un turno pagado");
|
|
}
|
|
|
|
const service = await ServicesManager.services.serviceList.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
throw new Error("El servicio no existe");
|
|
}
|
|
|
|
const discount = await Discounts.discounts.discountsList.findOne({
|
|
_id: data.discountId,
|
|
companyId: data.companyId,
|
|
});
|
|
|
|
if (!discount) {
|
|
throw new Error("El descuento no existe");
|
|
}
|
|
|
|
let price = service.price;
|
|
|
|
if (service.discountId) {
|
|
//Como el servicio ya tiene un descuento, aplico el descuento del turno sobre el precio original.
|
|
//Ya que la idea es que los descuentos no sean acumulativos.
|
|
price = service.originalPrice;
|
|
}
|
|
|
|
if (discount.type == DiscountType.FIXED) {
|
|
price = price - discount.value;
|
|
} else if (discount.type == DiscountType.PERCENTAGE) {
|
|
price = price - price * (discount.value / 100);
|
|
}
|
|
|
|
appointment.discountId = data.discountId;
|
|
appointment.price = price;
|
|
await appointment.save();
|
|
}
|
|
|
|
public async deleteDiscount(data: DeleteAppointmentDiscountParams): Promise<void> {
|
|
await validateSessionUser({ sessionUser: data.sessionUser });
|
|
await validatePermissionsByCompany({
|
|
companyId: data.companyId,
|
|
sessionUser: data.sessionUser,
|
|
});
|
|
|
|
const appointment = await this.Appointments.AppointmentList.findOne({
|
|
_id: data.appointmentId,
|
|
});
|
|
|
|
if (!appointment) {
|
|
throw new Error("El turno no existe");
|
|
}
|
|
|
|
const service = await ServicesManager.services.serviceList.findOne({
|
|
_id: String(appointment.serviceId),
|
|
});
|
|
|
|
if (!service) {
|
|
throw new Error("El servicio no existe");
|
|
}
|
|
|
|
if (appointment.payment) {
|
|
throw new Error("No se puede eliminar el descuento de un turno pagado");
|
|
}
|
|
|
|
appointment.set("discountId", null);
|
|
appointment.price = service.originalPrice;
|
|
await appointment.save();
|
|
}
|
|
|
|
public async getAvailableDates(data: GetAvailableDatesParams): Promise<GetAvailableDatesResult> {
|
|
//Solamente validamos que sea un usuario registrado.
|
|
await validateSessionUser({ sessionUser: data.sessionUser });
|
|
|
|
const company = await CompaniesManager.companies.findOne({
|
|
_id: data.companyId,
|
|
});
|
|
|
|
if (!company) {
|
|
throw new Error("La organización no existe");
|
|
}
|
|
|
|
const employee = await EmployeesList.employees.findOne({
|
|
_id: data.employeeId,
|
|
});
|
|
|
|
if (!employee) {
|
|
throw new Error("El profesional no existe");
|
|
}
|
|
|
|
//Obtener rangos de fechas habilitados para la organizacion/profesional.
|
|
//cuto startDate este entre nuestro dateFrom y dateTo en base a la fecha de
|
|
//consulta y el rango de tiempo recibido por parametro.
|
|
|
|
const length = data.length > 60 ? 60 : data.length;
|
|
const dateFrom = dayjs().clone().startOf("week").add(-1, "days");
|
|
const dateTo = dayjs().clone().add(length, "days").endOf("day");
|
|
|
|
const schedulesEnabled = await SchedulesEnabledList.schedulesEnabled.schedulesEnabledList
|
|
.find({
|
|
companyId: String(data.companyId),
|
|
employeeId: String(data.employeeId),
|
|
startDate: { $gte: dateFrom.toDate(), $lte: dateTo.toDate() },
|
|
})
|
|
.exec();
|
|
|
|
const returnAvailableDates: Date[] = [];
|
|
let currentDate = dateFrom.clone().startOf("day");
|
|
|
|
while (currentDate.isSameOrBefore(dateTo)) {
|
|
const hasOverride = await SchedulesOverridesList.findOne({
|
|
companyId: String(data.companyId),
|
|
employeeId: String(data.employeeId),
|
|
date: currentDate.toDate(),
|
|
});
|
|
|
|
if (
|
|
hasOverride &&
|
|
hasOverride.schedules.some(
|
|
(schedule) =>
|
|
!schedule.disabled &&
|
|
(!schedule.serviceScope ||
|
|
schedule.serviceScope === "all" ||
|
|
(schedule.serviceScope === "specific" &&
|
|
(schedule.serviceIds || []).includes(data.serviceId)))
|
|
)
|
|
) {
|
|
returnAvailableDates.push(currentDate.toDate());
|
|
} else if (!hasOverride) {
|
|
// Check if the current date is within any enabled schedule range
|
|
let isEnabled = false;
|
|
if (schedulesEnabled) {
|
|
for (const scheduleItem of schedulesEnabled) {
|
|
const start = dayjs(scheduleItem.startDate).startOf("day");
|
|
const end = dayjs(scheduleItem.endDate).endOf("day");
|
|
if (currentDate.isSameOrAfter(start) && currentDate.isSameOrBefore(end)) {
|
|
isEnabled = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isEnabled) {
|
|
const schedule = await SchedulesList.schedules.schedulesList.findOne({
|
|
companyId: String(data.companyId),
|
|
employeeId: String(data.employeeId),
|
|
weekDay: currentDate.day(),
|
|
});
|
|
|
|
if (
|
|
schedule &&
|
|
schedule.schedules.some(
|
|
(scheduleItem) =>
|
|
!scheduleItem.disabled &&
|
|
(!scheduleItem.serviceScope ||
|
|
scheduleItem.serviceScope === "all" ||
|
|
(scheduleItem.serviceScope === "specific" &&
|
|
(scheduleItem.serviceIds || []).includes(data.serviceId)))
|
|
)
|
|
) {
|
|
returnAvailableDates.push(currentDate.toDate());
|
|
}
|
|
}
|
|
}
|
|
|
|
currentDate = currentDate.add(1, "day");
|
|
}
|
|
|
|
return {
|
|
availableDates: returnAvailableDates,
|
|
inRangeDates: [],
|
|
} as GetAvailableDatesResult;
|
|
}
|
|
|
|
private hasValidSystemToken(systemToken?: string): boolean {
|
|
const expectedSystemToken = process.env.SYSTEM_KEY || process.env.API_KEY;
|
|
return Boolean(systemToken && expectedSystemToken && systemToken === expectedSystemToken);
|
|
}
|
|
}
|
|
|
|
const AppointmentList = new AppointmentManager();
|
|
|
|
export default AppointmentList;
|