Skip to main content

Perfil (features/profile)

Pantalla del usuario autenticado: datos de solo lectura, avatar (con recorte) y firma de mensajes. Incluye además la página global de mensajes destacados (profile/starred-messages). Los endpoints users/me/* y get-me pertenecen al módulo User del backend.

Rutas​

ProfileModule se carga bajo profile en shared/routes/content.routes.ts solo con AuthGuard (sin permissionGuard: es autoservicio).

RutaComponente / móduloGuardPermiso
profileProfilePageComponentAuthGuardninguno
profile/starred-messagesStarredMessagesModule (lazy) → StarredMessagesPageComponentAuthGuardninguno

Accesos: el desplegable de la cuenta en el header (shared/components/header/elements/my-account, enlace routerLink="/profile") y la entrada de menú starred-messages (PAGE_URLS.PROFILE_STARRED_MESSAGES, icono bookmark; el comentario del menú indica que la entidad está exenta de permisos en el backend).

:::caution Destino de redirección por permiso denegado permissionGuard redirige a /profile/profile-detail, ruta que no existe en ProfileRoutingModule. Ver Rutas. :::

Archivos​

ArchivoRol
profile.module.ts, profile-routing.module.tsDeclaran página y cropper; importan ReactiveFormsModule, FormsModule, SharedModule e ImageCropperComponent (standalone, ngx-image-cropper)
pages/profile-page/profile-page.component.{ts,html,scss}Resumen, datos, avatar y firma
components/avatar-cropper-dialog/avatar-cropper-dialog.component.*Modal con image-cropper redondo y deslizador de zoom
components/avatar-cropper-dialog/avatar-cropper-dialog.service.tsopen(file): Promise<File | null> sobre NgbModal
starred-messages/starred-messages.module.ts, starred-messages-routing.module.tsMódulo lazy de destacados
starred-messages/pages/starred-messages-page/*Listado global de destacados

Constantes y servicios externos: AVATAR_CROPPER_MODAL_OPTIONS en core/helpers/global/profile.consants.ts (modal centrado, backdrop: 'static', keyboard: false, size: 'md', windowClass: 'avatar-cropper-dialog-modal'; el nombre del archivo lleva el typo consants), UserAvatarsService.getAvatarUrl (devuelve user.avatarUrl o undefined), UserService y StarredMessageService.

Perfil​

ProfilePageComponent se suscribe a selectUser (slice auth) y rellena currentUser, fullName (firstname/firstName + lastName, con respaldo en username) y la firma original. Muestra:

  • Tarjeta resumen: avatar (foto si hay avatarUrl, si no iniciales calculadas de fullName), nombre, email, insignia del tipo de usuario (users.userTypes.<userType>) e insignia activo/inactivo (isActive !== false).
  • Tarjeta de datos: usuario, email, tipo de usuario y "miembro desde" (createdAt) si existe.
  • Tarjeta de firma.

Los datos personales no son editables aquí; para eso, un administrador usa Usuarios.

Firma​

Formulario con un único campo signature: Validators.maxLength(1000) (SIGNATURE_MAX_LENGTH, también como maxlength HTML) y contador profile.signature.charactersCount. Hay vista previa (signature-preview) con un mensaje de ejemplo y la firma debajo, o un texto de "vacío".

  • El botón guardar se deshabilita si el formulario es inválido, hay un guardado en curso o la firma (con trim) no cambió respecto de la original (isSignatureDirty). "Restablecer" aparece solo si hay cambios.
  • onSubmit() llama UserService.updateMySignature(signature) → PATCH /users/me/signature con { signature }. En éxito sincroniza el formulario con la respuesta, despacha UserActions.userProfileUpdateSuccess({ user: response.data }) y muestra el toast profile.signature.updateSuccess.
  • La firma se usa luego al enviar mensajes: en ChatConversationComponent el compositor tiene signatureEnabled (activo por defecto) y envía signature: true salvo en modo nota (ver Chat).

Avatar​

  1. El input de archivo (#profileAvatarInput) acepta solo image/jpeg (AVATAR_ALLOWED_TYPE) y hasta 2 MB (AVATAR_MAX_SIZE = 2 * 1024 * 1024). Si falla, muestra profile.avatar.errors.invalidType o tooLarge y no abre el recorte. El código lo describe como espejo de la validación del backend.
  2. AvatarCropperDialogService.open(file) abre el modal: recorte cuadrado y redondo (aspectRatio 1, roundCropper), salida blob JPEG calidad 90 redimensionada a 512 px de ancho, zoom entre 1 y 3 (MIN_SCALE, MAX_SCALE, paso 0,05) y arrastre de la imagen. onTransformChange conserva el desplazamiento al cambiar el zoom. Confirmar construye new File([blob], 'avatar.jpg', { type: 'image/jpeg' }) y cierra el modal con él; cancelar descarta y el servicio resuelve null. El botón confirmar se deshabilita hasta que existe un blob recortado.
  3. _uploadCroppedAvatar crea un URL.createObjectURL como vista previa local y llama UserService.updateMyAvatar(file) → PATCH /users/me/avatar con FormData (file). En éxito despacha userProfileUpdateSuccess con el usuario actual fusionado ({ ...currentUser, avatarUrl }) para no perder permisos ni flags. En error revoca la vista previa, limpia y muestra profile.avatar.errors.uploadFailed.
  4. ngOnDestroy revoca el object URL.

Mensajes destacados​

StarredMessagesPageComponent lista todos los mensajes que el agente marcó con estrella, de contacto e internos mezclados, del más nuevo al más viejo.

  • Carga: StarredMessageService.findGlobal(page, 20, messageType?) → GET /starred-messages con page, limit y messageType opcional. Respuesta StarredMessagePaginatedData (results, meta con page, limit, total, totalPages). Paginación "cargar más": loadMore() pide la página siguiente mientras meta.page < meta.totalPages y concatena.
  • Filtro por tipo con tres botones: todos, contact, internal (setFilter; volver a elegir el activo no hace nada).
  • Cada fila muestra insignia de tipo, remitente (getSenderName, con respaldo starredMessages.senderFallback), vista previa (nombre de archivo si hay adjunto, si no el cuerpo; miniatura si es imagen, icono según mimetype en otro caso) y fecha (date: 'short'). Los helpers getStarredSenderName, isStarredFileImage y getStarredFileIconClass viven en core/helpers/global/starred-message.constant.ts.
  • Actualización: escucha StarredMessageService.changed$ (emite tras un toggle confirmado, desde los chats) y recarga desde la página 1.
  • Clic en una fila (openItem): contact navega a /whaticket/chat con queryParams { chatId: <ticketId>, highlightMessageId }; internal navega a /internal-chat/conversations con { conversationId, highlightMessageId }. Si falta message.ticketId o conversationId muestra un toast de error starredMessages.page.openError y no navega.

Otros métodos de StarredMessageService (toggle, getIdsByScope, findByContact, findByInternalConversation, fetchMessageWindow) los usan los chats; es un servicio simple con RxJS, sin NgRx.

Estado y sockets​

Lee y escribe el slice auth (selectUser, acción userProfileUpdateSuccess, cuyo reducer reemplaza user). No hay efecto que escriba el usuario actualizado en secureStorage: la sesión almacenada se refresca la próxima vez que se llama get-me (por ejemplo al recargar). No usa sockets propios.

Pruebas​

SpecCubre
profile-page.component.spec.tsRechazo de archivo no JPG o mayor de 2 MB, cancelación del recorte, subida del archivo recortado y usuario fusionado en el store, vista previa tras confirmar, bandera de ocupado, fallo de subida
avatar-cropper-dialog.component.spec.tsZoom acotado a 1–3 con nuevo objeto transform, conservación del arrastre, confirmar con y sin blob, cancelar
avatar-cropper-dialog.service.spec.tsPasa el archivo al componente; resuelve el File o null al cancelar
starred-messages-page.component.spec.tsCarga inicial, error, recarga por changed$, filtro, vista previa, remitente, helpers de archivo y ramas de openItem

No hay spec de la página para la firma.

Gotchas e inconsistencias​

  • Solo JPEG: un PNG o WebP se rechaza en el cliente aunque sea una imagen válida.
  • La validación del tipo es por file.type (MIME informado por el navegador), no por extensión ni por contenido.
  • La página de perfil no consulta el backend al abrirse: depende del user del store, cargado al iniciar sesión con get-me.
  • El nombre del archivo de constantes es profile.consants.ts (typo existente en el repositorio).
  • Un fallo de subida de avatar muestra el error propio y, además, el toast del ErrorInterceptor (ver Auth, interceptores y RBAC).