Skip to main content

Contactos (features/contacts)

CRUD de contactos (clientes que escriben por los canales) del tenant: listado paginado en servidor con filtros, alta, edición, vista de solo lectura y conmutadores rápidos de "activo" y "bloqueado". Contraparte del backend: Contact. Las etiquetas que se asignan se gestionan en Etiquetas.

Ruta​

ContactsModule se carga en shared/routes/content.routes.ts bajo contacts (PAGE_URLS.CONTACTS) con canActivate: [AuthGuard, permissionGuard] y data: { permission: 'CONTACT_READ' }. ContactsRoutingModule repite permissionGuard con el mismo permiso en la ruta vacía (sin AuthGuard; ya lo aplica la ruta padre). Entrada de menú: sidebarMenu.contacts, con permission: 'CONTACT_READ' (core/helpers/ui/nav.constant.ts).

Archivos​

ArchivoRol
contacts.module.ts / contacts-routing.module.tsDeclaran los cuatro componentes; importan ReactiveFormsModule, FormsModule, NgbModalModule, NgbTooltipModule, NgSelectModule, SharedModule
pages/contacts-page/contacts-page.component.*Solo compone shared-breadcrumb (contacts.titleSidebar, ruta padre tickets.titleSidebar) y contacts-contacts-table
components/contacts-table/contacts-table.component.*Tabla, paginación, filtros aplicados, conmutadores y apertura del modal; hospeda el filtro
components/contacts-filter/contacts-filter.component.*Panel de filtros (incluye selector de etiquetas)
components/contact-modal-form/contact-modal-form.component.*Modal de alta, edición y vista
contacts-routing.module.spec.tsÚnica spec de la feature

Permisos​

AcrónimoEfecto
CONTACT_READRuta y menú
CONTACT_CREATEBotón "agregar" (contacts.modal.add)
CONTACT_UPDATEBotón "editar" por fila y los dos conmutadores (activo y bloqueado)

El botón "ver" no tiene restricción adicional. La UI no ofrece eliminar, aunque el servicio tiene removeContact y el backend expone DELETE remove/:id con CONTACT_DELETE.

Servicios y endpoints​

ContactService (core/services/api/contact.service.ts, providedIn: 'root') usa environment.apiUrl + API_URLS.CONTACTS (/contacts) + API_ENDPOINTS. Todas las mutaciones muestran un toast de éxito con res.message (título contacts.title).

MétodoEndpointDetalle
findContactsPaginated(page, limit, filter?)GET /contacts/get-allQuery page, limit más los filtros; devuelve { results, total } leyendo data.results y data.meta.total
createContact(data)POST /contacts/createUsado por el modal
updateContact(id, data)PATCH /contacts/update/:idid es el _id; lo usan el modal (payload completo) y los conmutadores (parche de un campo)
assignLabels(id, labelIds)PATCH /contacts/:id/assign-labelsBody { labelIds }. No lo usa esta feature: lo llama el chat (chat-conversation.component.ts)
removeContact(id)DELETE /contacts/remove/:idSin consumidores en la UI

Además, LabelService.findLabelsPaginated(1, 100, { isActive: true }) alimenta el selector de etiquetas del filtro y del modal (ver Etiquetas).

:::note Rutas del backend Contact lista rutas REST simplificadas (GET /contacts, POST /contacts/:id/labels). Las reales, verificadas en contact.controller.ts, siguen CRUD_ROUTES (get-all, create, update/:id, remove/:id) y :id/assign-labels con PATCH. Ver Endpoints. :::

Modelos​

core/interfaces/api/contact.interface.ts:

InterfazCampos
Contact_id, tenantId, externalId, username, phone?, firstname?, secondFirstname?, lastName?, secondLastName?, email?, profilePic?, metadata?: object[], isActive, isBlocked, labels: Label[], createdAt?, updatedAt?
ContactFormIgual sin _id/tenantId; labels?: string[] (ids), isActive?, isBlocked?
ContactFilterusername?, firstname?, lastName?, email?, phone?, isActive?, isBlocked?, labels?: string[]
ContactPaginatedDataresults, meta { total, page, limit }
ContactMetadataItemname, description (la usa el chat, no esta feature)

Constantes de validación en core/helpers/global/contact.constant.ts: EXTERNAL_ID 100, USERNAME 100, NAME 100, EMAIL 150 (longitudes máximas).

Listado y filtros​

ContactsTableComponent guarda currentPage, currentLimit (10) y currentFilter; fetchPage pide al servidor y fija count, page y limit en tableConfig (el ordenamiento es local, externalSorting: false). Recargar, aplicar o limpiar filtros reinician en la página 1; onLimitChange también.

Columnas: usuario (título móvil), nombre, segundo nombre, apellido, segundo apellido, teléfono (subtítulo móvil), email, etiquetas (insignias con el color de cada etiqueta), activo, bloqueado, creado y acciones. Los dos conmutadores (app-switch) solo se muestran con CONTACT_UPDATE; patchContactField guarda togglingId para deshabilitarlos durante la petición y, en éxito, aplica el parche a la fila con Object.assign (no recarga).

ContactsFilterComponent usa un objeto plano con ngModel (username, firstname, lastName, email, isActive, isBlocked, labels) y emite filterApply / filterReset. onApply recorta espacios, omite vacíos, convierte 'true'/'false' a booleano y calcula activeFiltersCount. Carga hasta 100 etiquetas activas en ngOnInit. No expone el filtro phone aunque ContactFilter lo declara.

Formulario (ContactModalFormComponent)​

Recibe action (ADD, EDIT, VIEW) y contact. En VIEW el formulario queda deshabilitado.

CampoValidadores
externalIdrequired, maxLength(100)
usernamerequired, maxLength(100)
phonepattern(/^\+?\d{6,15}$/) (opcional)
firstname, secondFirstname, lastName, secondLastNamemaxLength(100)
emailemail, maxLength(150)
profilePicsin validadores (input type="url")
metadatasin validadores; ver abajo
isActivepor defecto true
isBlockedpor defecto false
labelsids; ng-select múltiple sobre las etiquetas activas

metadata se edita en un textarea con JSON: onMetadataChange lo parsea y, si es válido, guarda un arreglo (un objeto suelto se envuelve en uno); si el texto está vacío guarda []; si el JSON es inválido, ignora el cambio en silencio y conserva el último valor válido.

buildBasePayload siempre envía externalId, username, isActive, isBlocked, labels y metadata; los demás campos solo si traen texto tras trim. En edición las etiquetas pobladas (objetos Label) se reducen a ids con typeof l === 'string' ? l : l._id.

Flujo​

  1. Abrir contacts: el guard comprueba CONTACT_READ y la tabla pide la primera página.
  2. Filtrar: el panel emite un ContactFilter y se vuelve a pedir la página 1.
  3. Alta o edición: modal (backdrop: 'static', tamaño lg) → createContact / updateContact → closed('saved') → recarga.
  4. Conmutar activo o bloqueado: updateContact(_id, { isActive }) o { isBlocked } → toast del backend → la fila se actualiza en sitio.

Estado, sockets y utilidades​

No usa NgRx ni sockets; el estado vive en los componentes. Reutiliza DEFAULT_NGX_DATATABLE_CONFIG, BUTTON_ACTIONS, FORMAT_FOR_DATES, app-switch, shared-tables-ngx-datatable y shared-breadcrumb (ver Compartido).

Pruebas​

Solo contacts-routing.module.spec.ts: verifica que la ruta vacía lleva data: { permission: 'CONTACT_READ' } y permissionGuard. No hay specs de los componentes ni de ContactService.

Gotchas e inconsistencias​

  • Cada conmutador dispara un toast de éxito del backend, porque updateContact lo muestra siempre.
  • El selector de etiquetas ve como máximo 100 etiquetas activas (página única); si el tenant tiene más, el resto no se puede elegir ni filtrar.
  • Las etiquetas inactivas ya asignadas a un contacto no aparecen como chips en el modal (se filtra contra las activas), pero sus ids siguen en el valor del control y se reenvían al guardar.
  • El modal no tiene control de isSubmitting; el botón solo depende de la validez, por lo que un doble clic puede enviar dos peticiones.
  • profilePic es un input type="url" sin validadores de Angular; no hay subida de archivos en esta feature.
  • ContactFilter.phone y ContactService.assignLabels / removeContact existen pero esta pantalla no los usa.
  • Todas las URLs usan _id del contacto.