import { EmployeesAdapterMongoose } from "./Employees.Adapter.Mongoose"; import { CreateEmployeeParams, IEmployee, IEmployeesManager, EmployeeRoles, ValidateEmployeeParams, PaginateEmployeesParams, PaginateEmployeesResults, CompanyEmployeesView, FindEmployeesParams, FindEmployeesByIdParams, UpdateEmployeeRolesParams, UpdateEmployeeParams, DeleteEmployeesByCompany, DeleteEmployeeParams, RemoveCollaboratorParams, } from "./Employees.Interface"; import UsersManager from "../Users/Users"; import { ICompanyDocument } from "../Companies/Companies.Adapter.Mongoose"; import CompaniesManager from "../Companies/Companies"; import { NoPermissionMessage } from "../../helpers/NoPermissionMessage"; import { joinStrings } from "../../helpers/String"; import { isNull } from "../../helpers/IsNull"; import getAvatar from "../../helpers/getAvatar"; import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model"; import MetricsList from "../../Models/Metrics/Metrics"; import { NotificationsManager } from "../Notifications/Notifications"; import EmployeesServicesList from "../EmployeesServices/EmployeesServices"; import HeatMapList from "../HeatMap/HeatMap"; import SchedulesList from "../Schedules/Schedules"; import AppointmentList from "../Appointments/Appointments"; import { IncompleteCollaboratorView } from "./Employees.Interface"; import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface"; class EmployeeManager implements IEmployeesManager { employees: EmployeesAdapterMongoose; constructor() { this.employees = new EmployeesAdapterMongoose(); } public async deleteEmployee(data: DeleteEmployeeParams): Promise { await this.employees.employeeList.deleteOne({ _id: data.employeeId }); } public async deleteEmployeesByCompany(data: DeleteEmployeesByCompany): Promise { await this.employees.employeeList.deleteMany({ companyId: data.companyId }); } public async checkRoleById( companyId: string, employeeId: string, role: EmployeeRoles ): Promise { //Check if role exist in roles array. const check = await this.employees.findOne({ companyId: companyId, userId: employeeId, removed: { $ne: true }, } as any); if (!check) { return false; } if (check && check.roles) { return check.roles.includes(role); } return false; } public async checkPermission( company: ICompanyDocument, employeeId: string, role: EmployeeRoles ): Promise { //Si el data.sessionUser es el owner de la empresa tiene permiso. if (String(company.ownerId) == String(employeeId)) { return true; } //Si el data.sessionUser tiene el rol admin de la empresa tiene permiso. return await this.checkRoleById(String(company._id), employeeId, role); } public async createEmployee(data: CreateEmployeeParams): Promise { //Primero tengo que verificar que el usuario que intenta registrar un empleado //tenga permisos para hacerlo y que sea un usuario valido. Este dato viene de //el campo data.sessionUser. const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } if (!(await this.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) { throw new Error(NoPermissionMessage()); } const userCheck = await UsersManager.users.findOne({ _id: data.userId }); if (!userCheck) { throw new Error("El usuario no existe"); } const employeeCheck = await this.employees.findOne({ companyId: data.companyId, userId: data.userId, }); // Reactivation: if employee exists and is removed, reactivate instead of creating duplicate if (employeeCheck && employeeCheck.removed === true) { // Check quota before reactivation const canAdd = await MetricsList.canAddEmployee(companyCheck.ownerId); if (!canAdd) { throw new Error( "Ha alcanzado el limite de colaboradores permitidos de acuerdo a su plan." ); } // Clear removal metadata employeeCheck.removed = false; employeeCheck.removedAt = undefined; employeeCheck.removedBy = undefined; // Refresh profileSnapshot from current User employeeCheck.profileSnapshot = { firstName: userCheck.firstName || undefined, lastName: userCheck.lastName || undefined, email: userCheck.email || undefined, avatar: userCheck.avatar || undefined, }; // Reset invitation state for full re-acceptance flow employeeCheck.hostOk = false; employeeCheck.guestOk = false; await employeeCheck.save(); // Increment metrics await MetricsList.addEmployee({ userId: companyCheck.ownerId, quantity: 1, }); // Send invitation notification await NotificationsManager.sendSystemNotification({ userId: String(data.userId), subject: "Su usuario ha sido vinculado a una organización", message: `La compañía ${companyCheck.name} lo ha vinculado como colaborador. Es necesario que acepte la invitación para poder formar parte de esta organización.`, type: NotificationType.EMPLOYEE, code: String(employeeCheck._id), }); return employeeCheck; } if (employeeCheck) { if (!employeeCheck.guestOk) { throw new Error("La invitación para este usuario ya está pendiente"); } throw new Error("El usuario ya pertenece a la compañía"); } const canAdd = await MetricsList.canAddEmployee(companyCheck.ownerId); if (!canAdd) { throw new Error( "Ha alcanzado el limite de colaboradores permitidos de acuerdo a su plan." ); } const newEmplyee = await this.employees.create(data); await NotificationsManager.sendSystemNotification({ userId: String(data.userId), subject: "Su usuario ha sido vinculado a una organización", message: `La compañía ${companyCheck.name} lo ha vinculado como colaborador. Es necesario que acepte la invitación para poder formar parte de esta organización.`, type: NotificationType.EMPLOYEE, code: String((newEmplyee as any)._id) }); await MetricsList.addEmployee({ userId: companyCheck.ownerId, quantity: 1, }); return newEmplyee; } public async updateEmployeeRoles(data: UpdateEmployeeRolesParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } const checkPermission = await this.checkPermission( companyCheck, data.sessionUser, EmployeeRoles.ADMIN ); if (!checkPermission) { throw new Error(NoPermissionMessage()); } const employee = await this.employees.findOne({ _id: data.employeeId, }); if (!employee) { throw new Error("Ha ocurrido un error al validar el empleado."); } if (String(employee.userId) == data.sessionUser) { if (String(companyCheck.ownerId) != data.sessionUser) { throw new Error(NoPermissionMessage()); } } employee.roles = data.roles; await employee.save(); } //No exponer este metodo. public async validateFull(data: ValidateEmployeeParams): Promise { const employee = await this.employees.findOne({ _id: data.employeeId, }); if (!employee) { throw new Error("Ha ocurrido un error al validar el empleado."); } employee.hostOk = true; employee.guestOk = true; await employee.save(); } public async validateGuest(data: ValidateEmployeeParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const employee = await this.employees.findOne({ _id: data.employeeId, }); if (!employee) { throw new Error("Ha ocurrido un error al validar el empleado."); } if (String(sessionUser.id) != employee.userId) { throw new Error("Solo el destinatario puede aceptar la vinculación."); } employee.guestOk = true; const checkCompany = await CompaniesManager.companies.findOne({ _id: employee.companyId, }); if (!checkCompany) { throw new Error("La organización no existe"); } await employee.save(); await NotificationsManager.sendSystemNotification({ userId: String(employee.userId), subject: "Has aceptado la invitación!", message: `Ahora que haz aceptado la invitación ya estas habilitado para trabajar con ${checkCompany.name}.`, type: NotificationType.EMPLOYEE, code: String(employee._id) }); } public async rejectGuest(data: ValidateEmployeeParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const employee = await this.employees.findOne({ _id: data.employeeId, }); if (!employee) { throw new Error("Ha ocurrido un error al validar el empleado."); } if (String(sessionUser.id) != employee.userId) { throw new Error("Solo el destinatario puede rechazar la vinculación."); } const checkCompany = await CompaniesManager.companies.findOne({ _id: employee.companyId, }); if (!checkCompany) { throw new Error("La organización no existe"); } await this.employees.employeeList.deleteOne({ _id: data.employeeId }); await NotificationsManager.sendSystemNotification({ userId: String(checkCompany.ownerId), subject: "Invitación rechazada", message: `El usuario ${UsersManager.getUserFullName(sessionUser)} ha rechazado la invitación para unirse a ${checkCompany.name}.`, type: NotificationType.EMPLOYEE, code: String(employee._id) }); } public async validateHost(data: ValidateEmployeeParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const employee = await this.employees.findOne({ _id: data.employeeId, }); if (!employee) { throw new Error("Ha ocurrido un error al validar el empleado."); } //Si el data.sessionUser tiene el rol admin de la empresa tiene permiso. const checkPermission = await this.checkRoleById( employee.companyId, data.sessionUser, EmployeeRoles.ADMIN ); if (!checkPermission) { throw new Error(NoPermissionMessage()); } const employUserCheck = await UsersManager.users.findOne({ _id: employee.userId, }); if (!employUserCheck) { throw new Error("El usuario no existe"); } const companyCheck = await CompaniesManager.companies.findOne({ _id: employee.companyId, }); if (!companyCheck) { throw new Error("La organización no existe"); } employee.hostOk = true; await employee.save(); await NotificationsManager.sendSystemNotification({ userId: String(data.sessionUser), subject: "Has habilitado a un colaborador!", message: `Se ha enviado una notificación a ${UsersManager.getUserFullName( employUserCheck )} para formar parte de ${companyCheck.name}.`, type: NotificationType.EMPLOYEE, code: String(employee._id) }); } public async paginate(filters: PaginateEmployeesParams): Promise { return await this.employees.paginate(filters); } public async findByCompanyId(data: FindEmployeesParams): Promise { const employees = await this.employees.find({ ...data, removed: { $ne: true } } as any); const view = await Promise.all( employees.map(async (employee) => { const userData = await UsersManager.users.findOne({ _id: employee.userId }); const strFirstName = userData ? isNull(userData.firstName, "") : ""; const strLastName = userData ? isNull(userData.lastName, "") : ""; const strFullName = joinStrings([strFirstName, strLastName], " "); const strAvatar = userData ? getAvatar(userData.id, userData.avatar, strFullName) : strFullName; const boolGuestOk = employee.guestOk || false; const boolHostOk = employee.hostOk || false; const boolFullOk = boolGuestOk && boolHostOk; return { id: isNull(employee.id, ""), companyId: employee.companyId, userId: employee.userId, firstName: strFirstName, lastName: strLastName, fullName: strFullName, avatar: strAvatar, email: userData ? userData.email : "", roles: employee.roles ? employee.roles : [], guestOk: boolGuestOk, hostOk: boolHostOk, fullOk: boolFullOk, calendarColor: isNull(employee.calendarColor, "#ff9900"), }; }) ); return view; } public async findById(data: FindEmployeesByIdParams): Promise { const employee = await this.employees.findOne({ _id: data.id }); if (!employee) { throw new Error("El colaborador no existe"); } const userData = await UsersManager.users.findOne({ _id: employee.userId }); const strFirstName = userData ? isNull(userData.firstName, "") : ""; const strLastName = userData ? isNull(userData.lastName, "") : ""; const strFullName = joinStrings([strFirstName, strLastName], " "); const strAvatar = userData ? getAvatar(userData.id, userData.avatar, strFullName) : strFullName; const boolGuestOk = employee.guestOk || false; const boolHostOk = employee.hostOk || false; const boolFullOk = boolGuestOk && boolHostOk; return { id: isNull(employee.id, ""), companyId: employee.companyId, userId: employee.userId, firstName: strFirstName, lastName: strLastName, fullName: strFullName, avatar: strAvatar, email: userData ? userData.email : "", roles: employee.roles ? employee.roles : [], guestOk: boolGuestOk, hostOk: boolHostOk, fullOk: boolFullOk, calendarColor: isNull(employee.calendarColor, "#ff9900"), }; } public async textObjectFilter(data: FindEmployeesParams): Promise { const employees = await this.employees.find({ ...data, removed: { $ne: true } } as any); const formattedResult: TextObjectFilterResult[] = []; for (const colaborador of employees) { const userInfo = await UsersManager.users.findOne({ _id: colaborador.userId }); if (!userInfo) { continue; } const userFullName = joinStrings([userInfo.firstName, userInfo.lastName], " "); const strAvatar = userInfo ? getAvatar(userInfo.id, userInfo.avatar, userFullName) : userFullName; formattedResult.push({ id: isNull(colaborador.id, ""), name: userFullName, image: strAvatar, }); } return formattedResult; } public async update(data: UpdateEmployeeParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error( "No se ha encontrado el usuario o no tiene permisos para realizar esta acción" ); } const employee = await this.employees.findOne({ _id: data.id, }); if (!employee) { throw new Error("Ha ocurrido un error al validar el empleado."); } const companyCheck = await CompaniesManager.companies.findOne({ _id: employee.companyId }); if (!companyCheck) { throw new Error("La organización no existe"); } //Si el data.sessionUser tiene el rol admin de la empresa o es owner tiene permiso. const checkPermission = await this.checkPermission( companyCheck, data.sessionUser, EmployeeRoles.ADMIN ); if (!checkPermission) { throw new Error(NoPermissionMessage()); } if (data.calendarColor) { employee.calendarColor = data.calendarColor; } if (data.limit) { employee.limit = data.limit; } await employee.save(); } public async getIncompleteSetup(sessionUser: string): Promise { const incompleteCollaborators: IncompleteCollaboratorView[] = []; const userCompanies = await CompaniesManager.getByUserId({ sessionUser }); for (const company of userCompanies) { if (company.isAdmin || company.isOwner) { const employees = await this.findByCompanyId({ companyId: company.id } as any); for (const employee of employees) { if (employee.hostOk && employee.guestOk) { const services = await EmployeesServicesList.find({ employeeId: employee.id } as any); const schedules = await SchedulesList.find({ employeeId: employee.id, companyId: company.id } as any); const missingServices = services.length === 0; const missingSchedules = schedules.length === 0; if (missingServices || missingSchedules) { incompleteCollaborators.push({ employeeId: employee.id, companyId: company.id, companyName: company.name, name: employee.fullName, missingServices, missingSchedules }); } } } } } return incompleteCollaborators; } public async removeCollaboratorFromCompany(data: RemoveCollaboratorParams): Promise { // Validate session user 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" ); } // Validate company exists const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } // Validate target employee exists const employee = await this.employees.findOne({ _id: data.employeeId, }); if (!employee) { throw new Error("El colaborador no existe"); } // Check if already removed if (employee.removed) { throw new Error("El colaborador ya fue eliminado de la organización"); } // Validate permission (admin or owner) if ( !(await this.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN)) ) { throw new Error(NoPermissionMessage()); } // Self-removal prevention if (String(employee.userId) === data.sessionUser) { throw new Error("No puedes eliminarte a ti mismo de la organización"); } // Owner protection if (employee.roles && employee.roles.includes(EmployeeRoles.OWNER)) { throw new Error("No se puede eliminar al propietario de la organización"); } // Query future appointments for this employee const futureAppointments = await AppointmentList.Appointments.AppointmentList.find({ companyId: data.companyId, employeeId: data.employeeId, start: { $gt: new Date() }, }); // If there are future appointments, require a replacement if (futureAppointments && futureAppointments.length > 0) { if (!data.replacementEmployeeId) { throw new Error( "El colaborador tiene turnos futuros asignados. Se requiere un empleado de reemplazo." ); } // Validate replacement employee const replacementEmployee = await this.employees.findOne({ _id: data.replacementEmployeeId, }); if (!replacementEmployee) { throw new Error("El empleado de reemplazo no existe"); } if (String(replacementEmployee.companyId) !== data.companyId) { throw new Error("El empleado de reemplazo no pertenece a esta organización"); } if (replacementEmployee.removed) { throw new Error("El empleado de reemplazo fue eliminado de la organización"); } if (String(replacementEmployee.userId) === data.sessionUser) { throw new Error( "No puedes designarte como reemplazo de ti mismo" ); } // Reassign future appointments for (const appointment of futureAppointments) { appointment.employeeId = data.replacementEmployeeId; await appointment.save(); } // Reassign active repeats const RepeatsList = (await import("../Repeats/Repeats")).default; const activeRepeats = await RepeatsList.repeats.find({ companyId: data.companyId, employeeId: data.employeeId, }); for (const repeat of activeRepeats) { // Access the underlying Mongoose model to get save() capability await RepeatsList.repeats.findOne({ _id: repeat.id }).then(async (doc) => { if (doc) { doc.employeeId = data.replacementEmployeeId!; await doc.save(); } }); } } // Snapshot user profile const targetUser = await UsersManager.users.findOne({ _id: employee.userId }); if (targetUser) { employee.profileSnapshot = { firstName: targetUser.firstName || undefined, lastName: targetUser.lastName || undefined, email: targetUser.email || undefined, avatar: targetUser.avatar || undefined, }; } // Mark employee as removed employee.removed = true; employee.removedAt = new Date(); employee.removedBy = data.sessionUser; await employee.save(); // Cleanup: delete employee services and heatmap data await EmployeesServicesList.deleteEmployeeServiceByEmployee({ employeeId: data.employeeId, }); await HeatMapList.deleteHeatMapByEmployee({ employeeId: data.employeeId, }); // Decrement metrics await MetricsList.addEmployee({ userId: companyCheck.ownerId, quantity: -1, }); } } export { EmployeeManager }; const EmployeesList = new EmployeeManager(); export default EmployeesList;