Departamentos (features/departments)
Administración de los departamentos (colas de atención): nombre, color, saludo, reglas de asignación automática, usuarios que pertenecen a cada uno, exigencia de motivo de contacto y política de resolución automática. Es la única feature de este lote cuyo listado vive en NgRx, porque otras pantallas reutilizan la lista (chat, chat interno). En docs/backend/modules no existe aún una página del módulo departments; los endpoints se describen aquí según el frontend y el controlador del backend.
Ruta
DepartmentsModule se carga bajo departments (PAGE_URLS.DEPARTMENTS) en shared/routes/content.routes.ts con canActivate: [AuthGuard, permissionGuard] y data: { permission: 'DEPARTMENT_READ' }; DepartmentsRoutingModule repite el permissionGuard con el mismo permiso. Menú: sidebarMenu.departments, permission: 'DEPARTMENT_READ'.
Archivos
| Archivo | Rol |
|---|---|
departments.module.ts, departments-routing.module.ts | Declaran los cuatro componentes (sin NgSelectModule) |
pages/departments-page/* | Breadcrumb (sidebarMenu.departments, padre tickets.titleSidebar) y departments-departments-table |
components/departments-table/* | Lista desde el store, filtro local, acciones por fila; hospeda el filtro |
components/departments-filter/* | Panel con un único campo name |
components/department-modal/* | Modal con tres pestañas (departamento, usuarios y asignación, resolución del chat) |
shared/validators/notifyBeforeResolve.ts | Validador del grupo autoResolution, compartido con la pantalla de Configuración |
shared/components/rich-text-editor | Editor del saludo (app-rich-text-editor) |
Permisos
| Acrónimo | Efecto en la UI | En el backend |
|---|---|---|
DEPARTMENT_READ | Ruta y menú | get-all, get-one/:id, GET :id/users |
DEPARTMENT_CREATE | Botón "agregar" | create |
DEPARTMENT_UPDATE | Botón "editar" por fila | update/:id, PATCH :id/users |
DEPARTMENT_DELETE | Botón "eliminar" por fila | remove/:id |
(Columna del backend verificada en department.controller.ts con @RequirePermission.)
Servicios y endpoints
DepartmentService (core/services/api/department.service.ts, providedIn: 'root', base /departments):
| Método | Endpoint | Uso |
|---|---|---|
getAll() | GET /departments/get-all | Lista completa (data.results); la carga el efecto loadDepartments$ |
findDepartmentsForSelect(filter) | GET /departments/get-all con query | Opciones { value, label, color } y totalCount para selectores de otras pantallas (por ejemplo el panel de transferencia del chat) |
getById(id) | GET /departments/get-one/:id | Devuelve data.results[0] o null; no lo usa esta feature |
create(data) | POST /departments/create | Alta |
update(id, data) | PATCH /departments/update/:id | Edición (id = _id) |
remove(id) | DELETE /departments/remove/:id | Baja |
getDepartmentUsers(id) | GET /departments/:id/users | Usuarios del departamento con sus marcas |
updateDepartmentUsers(id, users) | PATCH /departments/:id/users | Body { users: DepartmentUserEntry[] } |
Modelos
core/interfaces/api/department.interface.ts:
| Interfaz | Campos |
|---|---|
Department | _id, tenantId, name, color, greetingMessage, autoAssignment, assignOfflineUsers, redistributeChats, contactReasonRequirement (none, optional, required), overrideGlobalResolution, autoResolution, createdAt?, updatedAt? |
AutoResolution | enabled, sendFarewellMessage, resolveInMinutes, notifyInMinutes, notificationMessage |
DepartmentForm | name y el resto opcionales (autoResolution parcial) |
DepartmentUser | userId, username, email, assignToDepartment, autoAssign |
DepartmentUserEntry | userId, assignToDepartment, autoAssign |
DepartmentState | departments, isLoading, error |
Estado NgRx
Slice departments (core/states/departments/): acciones load, create, update, delete con sus Success y Failure (DepartmentActions), reducer, selectores (selectDepartments, selectDepartmentsIsLoading, selectDepartmentsError) y DepartmentEffects, registrado en core/states/app.state.ts (ver Estado).
- El reducer pone
isLoadingenload, reemplaza la lista enloadSuccess, añade encreateSuccess, sustituye por_idenupdateSuccessy filtra endeleteSuccess. - Los efectos muestran toasts (
departments.toast.created|updated|deleted) y un toast de error genérico (departments.error.generic) en cualquierFailure. - Otras pantallas consumen el slice:
ChatListComponent,CreateGroupModalComponentyNewChatModalComponent(estos dos despachanloadDepartmentssi la lista está vacía).
Tabla y filtro
DepartmentsTableComponent despacha loadDepartments en ngOnInit y se suscribe a selectDepartments y selectDepartmentsIsLoading (con takeUntil). El filtro es local: _applyFilter compara name en minúsculas con includes sobre la lista del store; no hay paginación de servidor (count = tamaño filtrado). Columnas: color (cuadro con backgroundColor y tooltip del valor), nombre (ordenable), mensaje de saludo y acciones. El botón de recarga despacha loadDepartments.
DepartmentsFilterComponent emite filterApply / filterReset con { name } (recortado). Sus botones usan las claves labels.filter.apply y labels.filter.reset (copiadas de la feature de etiquetas).
Acciones: editar (DEPARTMENT_UPDATE) abre el modal con action = EDIT y department; eliminar (DEPARTMENT_DELETE) abre ConfirmDialogComponent (departments.modal.delete, departments.modal.deleteMessage, icono danger) y, si se confirma, despacha deleteDepartment({ id }). No hay botón "ver".
Modal (DepartmentModalComponent)
Se abre con backdrop: 'static', size: 'lg', centered, keyboard: false. Recibe action (ADD o EDIT) y department. Tres pestañas controladas por activeTab.
Pestaña departamento
| Campo | Validadores / control |
|---|---|
name | required, maxLength(100) |
color | input type="color", por defecto #000000 |
greetingMessage | app-rich-text-editor, maxLength RICH_TEXT_VALIDATIONS.MESSAGE.MAX_LENGTH (1000) |
Sobre el editor hay botones para insertar variables (GREETING_VARIABLES): {{greetings}}, {{username}}, {{fullName}}, {{phone}}, {{identifier}} y {{email}} (etiquetas en departments.variables.*). insertVariable usa greetingEditor.insertText y, sin editor, concatena al final del valor.
Pestaña usuarios y asignación
Deshabilitada en alta (muestra departments.tabs.usersAddHint). En edición contiene tres conmutadores globales (autoAssignment, assignOfflineUsers, redistributeChats) y una tabla de usuarios cargada la primera vez que se abre la pestaña (getDepartmentUsers; _usersLoaded evita repetir la petición). Cada fila tiene dos app-switch: assignToDepartment y autoAssign (este último deshabilitado si autoAssignment está apagado). Los cambios se mantienen en memoria en departmentUsers.
Pestaña resolución del chat
| Campo | Control / validadores |
|---|---|
contactReasonRequirement | select con none, optional, required (CONTACT_REASON_OPTIONS) |
overrideGlobalResolution | Conmutador; si está apagado todo el grupo autoResolution queda deshabilitado |
autoResolution.enabled | Conmutador; si está apagado se deshabilitan los dependientes |
autoResolution.sendFarewellMessage | Conmutador, por defecto true |
autoResolution.resolveInMinutes | required, min(1), por defecto 5 |
autoResolution.notifyInMinutes | min(1), por defecto 1 |
autoResolution.notificationMessage | Texto |
_watchResolutionToggles / _syncResolutionState habilitan o deshabilitan los controles con emitEvent: false. El validador de grupo notifyBeforeResolveValidator marca notifyNotBeforeResolve cuando notifyInMinutes >= resolveInMinutes, salvo si enabled === false (con la resolución apagada los tiempos no aplican). _buildPayload usa getRawValue(), de modo que envía también valores de controles deshabilitados.
Guardado
El botón guardar se deshabilita con departmentForm.invalid, isSaving o isLoadingUsers, y se oculta en la pestaña usuarios durante el alta.
- Pestañas departamento o resolución:
create(alta) oupdate(_id, payload)(edición) víaDepartmentServicedirectamente; en éxito_notifySuccess(toastdepartments.toast.created|updated) yactiveModal.close(resultado). - Pestaña usuarios (solo edición):
updatedel departamento y luegoupdateDepartmentUsers(id, entradas)con{ userId, assignToDepartment, autoAssign }; éxito → toastdepartments.toast.updatedy cierre.
Al terminar el modal (hidden, tanto por cierre como por cancelación) la tabla despacha loadDepartments y refresca desde el servidor.
Flujo
- Abrir
departments: el guard compruebaDEPARTMENT_READy la tabla despachaloadDepartments. - Alta: modal (pestañas departamento y resolución) →
create→ toast → recarga. - Edición: modal poblado desde la fila; la pestaña usuarios descarga los miembros bajo demanda.
- Baja: confirmación →
deleteDepartment→ efecto →remove→ reducer elimina la fila → toastdepartments.toast.deleted.
Sockets y utilidades
No usa sockets. Reutiliza shared-tables-ngx-datatable, shared-accordions-toogle-panel, ConfirmDialogComponent, app-switch y ToastrNotificationService (ver Compartido).
Pruebas
department-modal.component.spec.ts cubre: valores por defecto en alta, relleno en edición, títulos, habilitación y deshabilitación del grupo de resolución, validación notifyBeforeResolve (menor, igual, mayor, override apagado, resolveInMinutes requerido), insertVariable, create y update con su payload (incluyendo los tiempos), cierre del modal con el resultado y carga única de usuarios al cambiar a la pestaña. No hay specs para la tabla, el filtro, el reducer ni los efectos de departamentos.
Gotchas e inconsistencias
- Efectos de crear y actualizar sin uso desde la UI: el modal llama a
DepartmentService.create/updatedirectamente y notifica él mismo. Las accionescreateDepartmentyupdateDepartmenty sus efectos existen, pero solo el borrado pasa por NgRx; la lista se actualiza por la recarga posterior al cerrar el modal, no porcreateSuccess/updateSuccess. - Recarga en
hidden: ocurre también al cancelar, pidiendo la lista aunque no haya cambios. - Cambios de usuarios se pierden en la pestaña equivocada: los conmutadores por usuario se envían solo si se guarda desde la pestaña "usuarios"; guardar desde "departamento" o "resolución" envía solo el departamento (y a la inversa, guardar desde "usuarios" sí reenvía también el departamento).
- Notificaciones repartidas: el borrado notifica desde
deleteDepartmentSuccess$; el alta y la edición notifican desde el modal. - Etiquetas de filtro copiadas: usa
labels.filter.applyylabels.filter.reset. - El filtro y el orden son locales; con muchos departamentos la lista completa se carga siempre.
- Las variables del saludo (
{{...}}) se guardan como texto; su sustitución ocurre en el backend (message-template.service.ts).