Ir al contenido

Mi Voz

El módulo permite al usuario autenticado enviar un caso (consulta, reclamo/queja, denuncia o sugerencia) a la empresa. El tenant configura de antemano qué categorías están activas y a qué correo se enruta cada una; el usuario sólo ve y elige entre las categorías configuradas, completa un formulario y lo envía. El backend no persiste el caso como un registro consultable por la app — lo envía por correo al destinatario configurado (excepto denuncias, que además quedan registradas en una tabla legacy de auditoría). No hay endpoint de historial: este módulo es de envío, no de seguimiento.

Acciones cubiertas:

  • Ver las categorías configuradas por el tenant (consulta, reclamo, denuncia, sugerencia).
  • Ver las subcategorías de una categoría, si el tenant las configuró.
  • Enviar un caso: título, descripción, adjuntos opcionales y, si aplica, subcategoría.

Prerrequisitos:

  • El usuario debe tener información organizacional activa (cargo, departamento, área, empresa). Sin ella, el backend rechaza el listado de categorías y el envío con un error genérico (ver Convenciones).
  • El módulo sólo debe mostrarse si el tenant configuró al menos una categoría activa. Si GET /v2/me/your-voice/categories devuelve un array vacío, la app debe ocultar el punto de entrada a “Mi Voz” por completo (ver “Estados vacíos” en Lógica de cliente).
  • Cada categoría debe tener un correo de destino configurado (a nivel de categoría o de subcategoría) para poder recibir envíos; si no lo tiene, el envío falla con 400 (ver Errores del endpoint de envío).
sequenceDiagram
    participant App as App móvil
    participant API as th-core-api

    Note over App,API: Carga inicial del punto de entrada a Mi Voz
    App->>API: GET /v2/me/your-voice/categories
    API-->>App: categorías activas configuradas por el tenant
    Note over App: Si el array viene vacío, ocultar el punto de entrada al módulo

    Note over App,API: Usuario elige una categoría (si hay más de una)
    App->>API: GET /v2/your-voice/categories/:id/subcategories
    API-->>App: subcategorías activas de esa categoría (puede venir vacío)

    Note over App: Formulario: título, descripción, adjuntos<br/>y subcategoría si aplica

    App->>API: POST /v2/me/your-voice/categories/:id/submit
    API-->>App: 204 sin cuerpo, caso enviado por correo
    Note over App: Mostrar pantalla de confirmación de envío

Lista las categorías activas configuradas para la empresa del usuario autenticado. Es la llamada que decide si el módulo se muestra o no.

No recibe query params.

Response — array de:

Campo Tipo Descripción
id number Id de la categoría, usado luego en submit y en el listado de subcategorías.
companyId number Empresa a la que pertenece la categoría.
type enum CategoryEnum Tipo de categoría (ver tabla de enum en Lógica de cliente).
email string | null Correo de destino por defecto de la categoría (informativo; el backend decide el destino real al enviar).
files string[] | null Adjuntos de referencia configurados a nivel de categoría (no son adjuntos del envío del usuario).
description string | null Descripción/instrucciones de la categoría, mostrada como subtítulo del formulario.
status enum CategoryStatusEnum active | inactive. Este endpoint sólo devuelve categorías active.
[
{
"id": 12,
"companyId": 4,
"type": "inquiry",
"email": "rrhh@ejemplo-tenant.com",
"files": null,
"description": "Envíanos tus preguntas sobre beneficios, nómina o políticas internas.",
"status": "active"
},
{
"id": 13,
"companyId": 4,
"type": "complaint",
"email": "etica@ejemplo-tenant.com",
"files": null,
"description": "Canal confidencial para denuncias de acoso, fraude o incumplimiento de código de ética.",
"status": "active"
}
]

Sin errores específicos adicionales a los comunes.

GET /v2/your-voice/categories/{id}/subcategories

Sección titulada «GET /v2/your-voice/categories/{id}/subcategories»

Lista las subcategorías activas de una categoría. No todas las categorías tienen subcategorías configuradas — es habitual que la respuesta venga vacía.

Path param: id (number, id de la categoría).

Response — array de:

Campo Tipo Descripción
id number Id de la subcategoría, se envía como subcategoryId en submit.
name string Nombre mostrado en el selector de subcategoría.
description string | null Texto legal/declaración asociado a la subcategoría (ver “Texto de subcategoría” en Lógica de cliente). Puede contener placeholders que la app debe resolver antes de mostrarlo.
email string | null Correo de destino específico de la subcategoría. Si viene, tiene prioridad sobre el email de la categoría al enrutar el envío.
files string[] | null Adjuntos de referencia de la subcategoría (por ejemplo, un reglamento en PDF), descargables desde el formulario.
[
{
"id": 27,
"name": "Acoso laboral",
"description": "Yo, {name}, identificado con {identification}, cargo {position}, declaro que la información proporcionada es verídica.",
"email": "etica@ejemplo-tenant.com",
"files": ["denuncias/reglamento-etica.pdf"]
},
{
"id": 28,
"name": "Fraude o corrupción",
"description": null,
"email": null,
"files": null
}
]

Sin errores específicos adicionales a los comunes.

POST /v2/me/your-voice/categories/{id}/submit

Sección titulada «POST /v2/me/your-voice/categories/{id}/submit»

Envía un caso a la categoría indicada. El backend arma y despacha un correo al destinatario configurado; si la categoría es de tipo complaint, además registra el caso en una tabla legacy de auditoría de denuncias. No devuelve el caso creado ni un id de seguimiento.

Path param: id (number, id de la categoría, obtenido de GET /v2/me/your-voice/categories).

Body (SubmitVoiceDto):

Campo Tipo Obligatorio Descripción
title string Título del caso. Máximo 255 caracteres.
description string Descripción detallada. Máximo 5000 caracteres (el formulario de referencia lo acota a 300 en UI, pero el backend acepta hasta 5000).
files string[] No Referencias a los archivos ya subidos (URLs/keys de storage), no el binario. Subir primero cada archivo con el flujo de adjuntos común y enviar aquí las referencias resultantes.
subcategoryId number No Id de la subcategoría elegida, si la categoría tiene subcategorías configuradas.
subcategoryText string No Texto de la subcategoría ya resuelto (placeholders reemplazados) que el usuario vio y aceptó. Máximo 5000 caracteres. Se envía como la “declaración” adjunta al correo — ver Lógica de cliente.
{
"title": "Consulta sobre días de permiso",
"description": "Buenos días, quisiera saber si los días de permiso por calamidad doméstica descuentan de mi saldo de vacaciones.",
"files": [],
"subcategoryId": null,
"subcategoryText": null
}

Ejemplo con subcategoría (denuncia):

{
"title": "Reporte de acoso laboral",
"description": "Detalle de los hechos ocurridos el 10 de julio de 2026 en el área de logística.",
"files": ["denuncias/jperez/evidencia-01.pdf"],
"subcategoryId": 27,
"subcategoryText": "Yo, Juan Pérez, identificado con 0102030405, cargo Analista de Logística, declaro que la información proporcionada es verídica."
}

Response: 204 No Content, sin cuerpo. La confirmación de envío es puramente el status HTTP — no hay id ni objeto de caso que persistir en el cliente.

Errores específicos:

Código Cuándo ocurre
400 La categoría (o su subcategoría, si aplica) no tiene ningún correo de destino configurado ("No recipient email configured for this category"). Tratar como configuración incompleta del tenant, no como error del usuario.
404 La categoría no existe (Category #{id} not found), o subcategoryId no existe (Subcategory #{id} not found).
400 subcategoryId pertenece a otra categoría distinta de id (Subcategory #{id} does not belong to category #{id}). No debería ocurrir si la app siempre pide subcategorías con el mismo id de categoría que luego envía.
Valor Texto sugerido Ícono sugerido
suggestion Sugerencia bombilla
report Queja llave/herramienta
inquiry Consulta signo de pregunta
complaint Denuncia escudo de alerta

El orden de presentación de referencia (cuando hay varias categorías configuradas) es: Sugerencia, Queja, Consulta, Denuncia.

Estado de la categoría (CategoryStatusEnum)

Sección titulada «Estado de la categoría (CategoryStatusEnum)»
Valor Significado
active Visible para el usuario. GET /v2/me/your-voice/categories sólo devuelve estas.
inactive No debe llegar al listado /me/...; si aparece por algún otro medio, no debe ofrecerse para envío.

Qué mostrar/ocultar según la respuesta de categorías

Sección titulada «Qué mostrar/ocultar según la respuesta de categorías»
  • Punto de entrada al módulo (botón/tarjeta “Mi Voz” en el menú): mostrar sólo si GET /v2/me/your-voice/categories devuelve al menos un elemento. Con el array vacío, no mostrar ni el punto de entrada ni un estado vacío dentro de una pantalla — el módulo entero debe estar ausente para ese tenant/usuario.
  • Selector de categoría: mostrar sólo si hay más de una categoría activa. Si sólo hay una, saltar directo al formulario de esa categoría (sin paso de selección).
  • Selector de subcategoría: mostrar sólo si GET /v2/your-voice/categories/{id}/subcategories devuelve al menos un elemento para la categoría elegida. Si viene vacío, ir directo a los campos del formulario (título/descripción/adjuntos) sin mostrar el bloque de subcategoría.
  • Campos del formulario (título, descripción, adjuntos): si la categoría tiene subcategorías, ocultarlos hasta que el usuario elija una subcategoría. Si no tiene subcategorías, mostrarlos de inmediato.

Texto de subcategoría y aceptación de términos

Sección titulada «Texto de subcategoría y aceptación de términos»
  • El campo description de una subcategoría puede traer placeholders que la app debe reemplazar por datos del perfil del usuario antes de mostrarlo y antes de enviarlo como subcategoryText:

    Placeholder Se reemplaza por
    {name} Nombre completo del usuario.
    {identification} Número de identificación/cédula.
    {position} Nombre del cargo.
    {area} Nombre del área.
    {city} Ciudad de la oficina.
  • Si subcategoryId está seleccionado y la categoría es complaint (denuncia), el formulario debe exigir una confirmación explícita (“Acepto los términos”) antes de habilitar el envío — el backend no valida esta aceptación, es una regla exclusiva del cliente. Para el resto de categorías no se exige esta confirmación aunque tengan subcategorías.

  • Si la subcategoría trae files, mostrarlos como adjuntos descargables de referencia (por ejemplo, un reglamento) — son distintos de los files que el usuario adjunta en su envío.

  • Botón “Enviar” deshabilitado hasta que: title no vacío, description no vacío (y dentro del límite que la app decida mostrar en UI, recomendado alinear con el límite real de 5000 del backend), y si la categoría tiene subcategorías, subcategoryId seleccionado.
  • Si la categoría es complaint y tiene subcategorías: además exigir la aceptación de términos mencionada arriba.
  • Adjuntos: subir cada archivo primero con el flujo de subida común de la app y enviar en files únicamente las referencias resultantes, nunca binarios en el body de submit.
  • Sin categorías configuradas: GET /v2/me/your-voice/categories devuelve [] — ocultar el módulo completo (ver arriba), no mostrar una pantalla vacía dentro de “Mi Voz”.
  • Categoría sin subcategorías: GET /v2/your-voice/categories/{id}/subcategories devuelve [] — comportamiento normal, no es un error; pasar directo al formulario.
  • Después de enviar: como la respuesta es 204 sin cuerpo, la pantalla de confirmación no puede mostrar un número de caso ni permitir “ver mi envío” — sólo confirmar el envío y ofrecer volver a enviar (a la misma categoría o, si hay varias, elegir otra).
  • Sin correo de destino configurado (400 en submit): mostrar como error de configuración del tenant (“Este canal no está disponible en este momento”), no como error de validación del formulario del usuario.

Fuente de verdad: los contratos se extraen de los controllers y DTOs de th-core-api (src/api-v2/your-voice/), y la lógica de cliente del comportamiento de referencia del portal web (th-app, app/components/my-voice/). Verifica cada endpoint contra el código antes de publicarlo.