feat: implement collaborator reactivation logic, add access denied handling, and improve metric filtering
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
UpdateEmployeeParams,
|
||||
DeleteEmployeesByCompany,
|
||||
DeleteEmployeeParams,
|
||||
RemoveCollaboratorParams,
|
||||
} from "./Employees.Interface";
|
||||
import UsersManager from "../Users/Users";
|
||||
import { ICompanyDocument } from "../Companies/Companies.Adapter.Mongoose";
|
||||
@@ -26,7 +27,9 @@ 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";
|
||||
|
||||
@@ -51,7 +54,11 @@ class EmployeeManager implements IEmployeesManager {
|
||||
role: EmployeeRoles
|
||||
): Promise<boolean> {
|
||||
//Check if role exist in roles array.
|
||||
const check = await this.employees.findOne({ companyId: companyId, userId: employeeId });
|
||||
const check = await this.employees.findOne({
|
||||
companyId: companyId,
|
||||
userId: employeeId,
|
||||
removed: { $ne: true },
|
||||
} as any);
|
||||
|
||||
if (!check) {
|
||||
return false;
|
||||
@@ -112,6 +119,53 @@ class EmployeeManager implements IEmployeesManager {
|
||||
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) {
|
||||
throw new Error("El usuario ya pertenece a la compañia");
|
||||
}
|
||||
@@ -346,7 +400,7 @@ class EmployeeManager implements IEmployeesManager {
|
||||
}
|
||||
|
||||
public async findByCompanyId(data: FindEmployeesParams): Promise<CompanyEmployeesView[]> {
|
||||
const employees = await this.employees.find(data);
|
||||
const employees = await this.employees.find({ ...data, removed: { $ne: true } } as any);
|
||||
|
||||
const view = await Promise.all(
|
||||
employees.map(async (employee) => {
|
||||
@@ -422,7 +476,7 @@ class EmployeeManager implements IEmployeesManager {
|
||||
}
|
||||
|
||||
public async textObjectFilter(data: FindEmployeesParams): Promise<TextObjectFilterResult[]> {
|
||||
const employees = await this.employees.find(data);
|
||||
const employees = await this.employees.find({ ...data, removed: { $ne: true } } as any);
|
||||
const formattedResult: TextObjectFilterResult[] = [];
|
||||
|
||||
for (const colaborador of employees) {
|
||||
@@ -522,8 +576,151 @@ class EmployeeManager implements IEmployeesManager {
|
||||
}
|
||||
return incompleteCollaborators;
|
||||
}
|
||||
|
||||
public async removeCollaboratorFromCompany(data: RemoveCollaboratorParams): Promise<void> {
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user