Mi Voz
Resumen
Sección titulada «Resumen»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/categoriesdevuelve 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
Endpoints
Sección titulada «Endpoints»GET /v2/me/your-voice/categories
Sección titulada «GET /v2/me/your-voice/categories»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 | Sí | Título del caso. Máximo 255 caracteres. |
description |
string | Sí | 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. |
Lógica de cliente
Sección titulada «Lógica de cliente»Tipos de categoría (CategoryEnum)
Sección titulada «Tipos de categoría (CategoryEnum)»| 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/categoriesdevuelve 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}/subcategoriesdevuelve 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
descriptionde una subcategoría puede traer placeholders que la app debe reemplazar por datos del perfil del usuario antes de mostrarlo y antes de enviarlo comosubcategoryText: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
subcategoryIdestá seleccionado y la categoría escomplaint(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 losfilesque el usuario adjunta en su envío.
Validaciones previas al envío
Sección titulada «Validaciones previas al envío»- Botón “Enviar” deshabilitado hasta que:
titleno vacío,descriptionno 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,subcategoryIdseleccionado. - Si la categoría es
complainty 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 desubmit.
Estados vacíos y casos borde
Sección titulada «Estados vacíos y casos borde»- Sin categorías configuradas:
GET /v2/me/your-voice/categoriesdevuelve[]— 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}/subcategoriesdevuelve[]— comportamiento normal, no es un error; pasar directo al formulario. - Después de enviar: como la respuesta es
204sin 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 (
400ensubmit): 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.