Encuestas
Resumen
Sección titulada «Resumen»El módulo cubre el flujo de autoservicio de encuestas de RRHH del usuario autenticado: ver las encuestas pendientes asignadas, abrir una para responderla, enviarla, y consultar el historial de encuestas ya completadas.
Acciones cubiertas:
- Ver el listado paginado de encuestas asignadas (pendientes).
- Ver el listado paginado de encuestas completadas (historial).
- Abrir una encuesta asignada y ver su estructura completa (factores, preguntas, opciones de respuesta y escalas).
- Responder y enviar una encuesta.
Prerrequisitos:
- Los endpoints bajo
/me/surveys/*requierenAuthorization: Bearer <jwt>(ver Convenciones). - Los endpoints bajo
/surveys/responses/*(estructura de la encuesta y envío de respuestas) son de acceso por token: no requierenAuthorization, eltokende la asignación es lo que autoriza el acceso. Aun así requieren el headerx-tenant-id, porque la resolución de tenant en el backend no depende del JWT. - No se encontró en el código ninguna acción de perfil de seguridad específica que condicione la visibilidad de este módulo (a diferencia de, por ejemplo,
PORTAL_VISUALIZACION_BALANCE_ANUALen Vacaciones). La disponibilidad del módulo depende únicamente de que el tenant lo tenga habilitado como producto (ver Convenciones → Permisos y visibilidad) y de que el usuario tenga asignaciones.
sequenceDiagram
participant App as App móvil
participant API as th-core-api
Note over App,API: Carga inicial de la pantalla Encuestas asignadas
App->>API: GET /me/surveys/assigned?page=1&limit=10
API-->>App: pendientes, ordenadas por fecha de cierre más próxima
Note over App,API: Tab Completadas
App->>API: GET /me/surveys/completed?page=1&limit=10
API-->>App: historial ordenado por fecha de completado, más reciente primero
Note over App,API: Usuario abre una encuesta asignada
App->>API: GET /me/surveys/:token
API-->>App: campaña, respondente y evaluado
App->>API: GET /surveys/responses/survey/:token
API-->>App: estructura completa, factores, preguntas, opciones y escalas
Note over App: Usuario responde cada pregunta según su tipo
App->>API: POST /surveys/responses/submit
API-->>App: confirmación de envío
Note over App,API: Refresco de listados tras enviar
App->>API: GET /me/surveys/assigned?page=1&limit=10
API-->>App: la encuesta ya no aparece en pendientes
App->>API: GET /me/surveys/completed?page=1&limit=10
API-->>App: la encuesta recién respondida aparece en el historial
Endpoints
Sección titulada «Endpoints»GET /me/surveys/assigned
Sección titulada «GET /me/surveys/assigned»Encuestas pendientes asignadas al usuario autenticado. Sólo incluye asignaciones con status = A cuya campaña está activa y dentro de su rango de fechas vigente (campaign.startDate <= hoy <= campaign.endDate). Ordenadas por proximidad a la fecha de cierre (las que cierran antes aparecen primero).
Query params:
| Param | Tipo | Obligatorio | Descripción |
|---|---|---|---|
page |
number | No (default 1) |
Página. |
limit |
number | No (default 10) |
Ítems por página. |
Response — PaginatedMyAssignedSurveyResponseDto:
| Campo | Tipo | Descripción |
|---|---|---|
items |
MyAssignedSurveyResponseDto[] |
Ver tabla siguiente. |
meta |
PaginationMetaDto |
totalItems, itemCount, itemsPerPage, totalPages, currentPage. |
MyAssignedSurveyResponseDto:
| Campo | Tipo | Descripción |
|---|---|---|
accessToken |
string | Token de acceso de la asignación. Se usa como :token en el resto de los endpoints de este módulo. |
status |
enum 'A' | 'F' |
Estado de la asignación. En este endpoint siempre llega A (el filtro del backend excluye las finalizadas). |
campaign.idCampaign |
number | Id de la campaña. |
campaign.name |
string | Nombre de la campaña. |
campaign.instructions |
string | Instrucciones de la campaña, mostrar antes del formulario. |
campaign.startDate / campaign.endDate |
date | Vigencia de la campaña. |
campaign.survey.idSurvey |
number | Id de la encuesta. |
campaign.survey.name / campaign.survey.description |
string | Nombre y descripción de la encuesta. |
campaign.survey.surveyType.name / .code |
string | Tipo de encuesta (p. ej. clima laboral, calidad de selección). |
respondent.username / .fullName / .mail / .avatar |
string | Usuario que debe responder (el autenticado). |
respondent.position.name / .shortName |
string | Cargo del respondente. |
evaluated.* |
igual forma que respondent |
Usuario evaluado por la encuesta. En encuestas de autoevaluación/clima suele coincidir con respondent; en encuestas de evaluación a terceros es otra persona. |
{ "items": [ { "accessToken": "8f2a91cd3e7b4a6f9c10", "status": "A", "campaign": { "idCampaign": 452, "name": "Encuesta de Clima Laboral 2026", "instructions": "Por favor completa la encuesta antes del 31 de julio. Tus respuestas son confidenciales.", "startDate": "2026-07-01T00:00:00.000Z", "endDate": "2026-07-31T23:59:59.000Z", "survey": { "idSurvey": 6, "idSurveyType": 3, "surveyType": { "idSurveyType": 3, "name": "ENCUESTA DE CLIMA LABORAL", "code": "CL" }, "name": "Clima Laboral", "description": "Evaluación anual del clima organizacional" } }, "respondent": { "username": "jperez", "fullName": "Juan Pérez", "name": "Juan", "lastName": "Pérez", "mail": "jperez@twiinshrm.com", "avatar": "https://cdn.twiinshrm.com/avatars/jperez.png", "position": { "id": 12, "name": "Analista de Sistemas", "shortName": "AN-SIS" } }, "evaluated": { "username": "jperez", "fullName": "Juan Pérez", "name": "Juan", "lastName": "Pérez", "mail": "jperez@twiinshrm.com", "avatar": "https://cdn.twiinshrm.com/avatars/jperez.png", "position": { "id": 12, "name": "Analista de Sistemas", "shortName": "AN-SIS" } } } ], "meta": { "totalItems": 3, "itemCount": 1, "itemsPerPage": 10, "totalPages": 1, "currentPage": 1 }}Sin errores específicos adicionales a los comunes.
GET /me/surveys/completed
Sección titulada «GET /me/surveys/completed»Historial de encuestas ya respondidas por el usuario autenticado (status = F), ordenado por fecha de completado descendente (más reciente primero).
Query params: page, limit — igual que en assigned.
Response — PaginatedMyCompletedSurveyResponseDto:
| Campo | Tipo | Descripción |
|---|---|---|
items |
MyCompletedSurveyResponseDto[] |
Ver tabla siguiente. |
meta |
PaginationMetaDto |
Igual forma que en assigned. |
MyCompletedSurveyResponseDto:
| Campo | Tipo | Descripción |
|---|---|---|
idCampaign |
number | Id de la campaña. |
campaignName |
string | Nombre de la campaña. |
surveyName |
string | Nombre de la encuesta. |
completedAt |
date | Fecha en que se completó. |
status |
string | Siempre F en este listado. |
{ "items": [ { "idCampaign": 441, "campaignName": "Encuesta de Calidad de Selección Q2 2026", "surveyName": "Calidad de la Selección", "completedAt": "2026-06-18T16:42:00.000Z", "status": "F" } ], "meta": { "totalItems": 1, "itemCount": 1, "itemsPerPage": 10, "totalPages": 1, "currentPage": 1 }}Sin errores específicos adicionales a los comunes. Este endpoint no expone las respuestas dadas ni un detalle por encuesta completada — sólo el resumen de la tabla anterior. No hay endpoint en este módulo para ver el detalle de una encuesta ya respondida.
GET /me/surveys/{token}
Sección titulada «GET /me/surveys/{token}»Detalle de una asignación por su token. Devuelve la misma forma que un ítem de assigned (campaign, respondent, evaluated), para la pantalla previa a responder.
Path param: token (string, accessToken de la asignación).
Response: MyAssignedSurveyResponseDto (objeto único, misma forma que un ítem de assigned, ver tabla arriba).
Errores específicos:
| Código | Cuándo ocurre |
|---|---|
404 |
Token inexistente o inválido. |
GET /surveys/responses/survey/{token}
Sección titulada «GET /surveys/responses/survey/{token}»Estructura completa de la encuesta para renderizar el formulario: factores, preguntas, opciones de respuesta y escalas. Acceso público por token (no requiere Authorization).
Path param: token (string).
Internamente valida primero la asignación (misma lógica que el endpoint de abajo) antes de devolver la estructura, así que puede fallar con los mismos errores de vigencia/estado.
Response — forma intencionada (derivada de las entidades Survey / SurveyFactor / SurveyQuestion / SurveyAnswerOption / SurveyScale / SurveyScaleValue; ver el aviso al inicio de esta página sobre el gap de serialización):
| Campo | Tipo | Descripción |
|---|---|---|
idSurvey |
number | Id de la encuesta. |
name / description |
string | Nombre y descripción. |
requiresPolicyAcceptance |
'S' | 'N' |
Si S, hay que mostrar policyText con checkbox de aceptación antes de habilitar el envío. |
policyText |
string | Texto de la política, cuando aplica. |
policyFile |
string | null | Nombre de archivo de política adjunta, cuando aplica. |
factors[] |
SurveyFactor[] |
Agrupadores de preguntas. |
factors[].idFactor |
number | Id del factor. |
factors[].factorType |
enum 'F' | 'V' | 'A' |
Ver tabla de enum en Lógica de cliente — determina si la escala vive a nivel de factor (F) o de pregunta (V). |
factors[].name / .description |
string | Nombre y descripción del factor (usarlo como encabezado de sección en el formulario). |
factors[].order |
number | Orden de presentación. |
factors[].scale |
SurveyScale | null |
Sólo poblada cuando factorType = 'F'. |
factors[].questions[] |
SurveyQuestion[] |
Preguntas del factor. |
questions[].idQuestion |
number | Id de la pregunta — es el questionId que se envía en el submit. |
questions[].questionType |
enum 'A' | 'R' | 'N' | 'U' |
Tipo de pregunta, ver tabla en Lógica de cliente. |
questions[].questionText |
string | Enunciado. |
questions[].description / .helpText |
string | Texto de ayuda adicional. |
questions[].isRequired |
'S' | 'N' |
Ver caution en Lógica de cliente — no coincide 1:1 con lo que el backend realmente exige al enviar. |
questions[].isMultiple |
'S' | 'N' |
Sólo relevante para questionType = 'R': si S, se pueden marcar varias opciones. |
questions[].isConditional |
'S' | 'N' |
Si S, la pregunta depende de la respuesta de otra. |
questions[].idParentQuestion |
number | null | Id de la pregunta de la que depende, cuando isConditional = 'S'. |
questions[].idTriggerAnswer |
number | null | Id de la opción de respuesta de la pregunta padre que activa mostrar esta pregunta. |
questions[].order |
number | Orden de presentación dentro del factor. |
questions[].answerOptions[] |
SurveyAnswerOption[] |
Sólo relevante para questionType = 'R'. |
answerOptions[].idAnswer |
number | Id de la opción — es el answerId que se envía en el submit. |
answerOptions[].answerText |
string | Texto de la opción. |
questions[].scale |
SurveyScale | null |
Sólo poblada cuando factorType = 'V' (escala a nivel de pregunta) y el tipo de pregunta es N o U. |
scale.values[] |
SurveyScaleValue[] |
Valores posibles de la escala. |
scale.values[].idValue |
number | Id del valor. |
scale.values[].label / .shortLabel |
string | Texto largo/corto a mostrar (p. ej. “Totalmente de acuerdo” / “TA”). |
scale.values[].numericValue |
number | Es el value numérico que se envía en el submit. |
scale.values[].order |
number | Orden de presentación. |
{ "idSurvey": 6, "name": "Clima Laboral", "description": "Evaluación anual del clima organizacional", "requiresPolicyAcceptance": "N", "policyText": "", "policyFile": null, "factors": [ { "idFactor": 21, "factorType": "V", "name": "Ambiente de trabajo", "description": "Percepción del ambiente y relaciones laborales", "order": 1, "scale": null, "questions": [ { "idQuestion": 301, "questionType": "U", "questionText": "¿Qué tan de acuerdo estás con el ambiente de tu equipo?", "description": "", "helpText": "", "isRequired": "S", "isMultiple": "N", "isConditional": "N", "idParentQuestion": null, "idTriggerAnswer": null, "order": 1, "answerOptions": [], "scale": { "idScale": 9, "name": "Escala de acuerdo 1-5", "values": [ { "idValue": 1, "label": "Totalmente en desacuerdo", "shortLabel": "TD", "numericValue": 1, "order": 1 }, { "idValue": 2, "label": "En desacuerdo", "shortLabel": "D", "numericValue": 2, "order": 2 }, { "idValue": 3, "label": "Neutral", "shortLabel": "N", "numericValue": 3, "order": 3 }, { "idValue": 4, "label": "De acuerdo", "shortLabel": "A", "numericValue": 4, "order": 4 }, { "idValue": 5, "label": "Totalmente de acuerdo", "shortLabel": "TA", "numericValue": 5, "order": 5 } ] } }, { "idQuestion": 302, "questionType": "R", "questionText": "¿Con qué beneficios te sientes más conforme?", "description": "Puedes marcar más de una opción", "helpText": "", "isRequired": "N", "isMultiple": "S", "isConditional": "N", "idParentQuestion": null, "idTriggerAnswer": null, "order": 2, "answerOptions": [ { "idAnswer": 1201, "answerText": "Horario flexible" }, { "idAnswer": 1202, "answerText": "Trabajo remoto" }, { "idAnswer": 1203, "answerText": "Capacitación" } ], "scale": null }, { "idQuestion": 303, "questionType": "A", "questionText": "¿Qué mejorarías del ambiente laboral?", "description": "", "helpText": "Respuesta abierta, opcional", "isRequired": "N", "isMultiple": "N", "isConditional": "N", "idParentQuestion": null, "idTriggerAnswer": null, "order": 3, "answerOptions": [], "scale": null } ] } ]}Errores específicos (heredados de la validación de la asignación subyacente):
| Código | Cuándo ocurre |
|---|---|
404 |
Token inexistente o inválido. |
400 |
"This survey has already been completed" — la asignación ya tiene status = F. |
400 |
"This survey campaign has expired" — campaign.endDate ya pasó. |
400 |
"This survey campaign has not started yet" — campaign.startDate todavía no llega. |
POST /surveys/responses/submit
Sección titulada «POST /surveys/responses/submit»Envía todas las respuestas de una asignación. Valida cada respuesta contra la estructura de la encuesta, calcula los puntajes por factor y marca la asignación como finalizada (status = F, completedAt = ahora) — todo en una única transacción. Acceso público por token, no requiere Authorization.
Body (SubmitResponseDto):
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
token |
string | Sí | Token de la asignación (el mismo accessToken usado en los GET anteriores). |
responses |
QuestionResponseDto[] |
Sí | Una entrada por pregunta respondida. |
policyAccepted |
boolean | Sólo si survey.requiresPolicyAcceptance = 'S' |
Debe ser true; si la encuesta exige política y no viene o es false, el backend responde 400. |
QuestionResponseDto:
| Campo | Tipo | Obligatorio | Cuándo usarlo |
|---|---|---|---|
questionId |
number | Sí | Id de la pregunta (idQuestion). |
answerId |
number | Sólo questionType = 'R' |
Id de la opción marcada (idAnswer). |
value |
number | Sólo questionType = 'N' o 'U' |
Debe ser exactamente uno de los numericValue de la escala de la pregunta. |
text |
string | Sólo questionType = 'A' |
Texto libre, no puede ser vacío/whitespace si la pregunta es obligatoria. |
{ "token": "8f2a91cd3e7b4a6f9c10", "responses": [ { "questionId": 301, "value": 4 }, { "questionId": 302, "answerId": 1201 }, { "questionId": 302, "answerId": 1202 }, { "questionId": 303, "text": "Más actividades de integración entre equipos." } ], "policyAccepted": true}Response (201):
{ "message": "Survey submitted successfully" }Errores específicos:
| Código | Cuándo ocurre |
|---|---|
404 |
Assignment o survey no encontrados (token inválido). |
400 |
"This survey has already been completed" / "...has expired" / "...has not started yet" — mismas validaciones de vigencia que en GET /surveys/responses/survey/{token}. |
400 |
"Policy acceptance is required for this survey" — la encuesta exige política y policyAccepted no llegó en true. |
400 |
"Question {id} is required" — falta una respuesta para una pregunta no condicional. |
400 |
"Question {id} requires a text response" — pregunta tipo A con text vacío. |
400 |
"Question {id} requires an answer selection" / "Invalid answer for question {id}" — pregunta tipo R sin answerId o con un answerId que no pertenece a esa pregunta. |
400 |
"Question {id} requires a numeric value" / "Invalid scale value for question {id}" — pregunta tipo N/U sin value o con un value que no está entre los numericValue de la escala. |
Endpoints existentes pero fuera del flujo recomendado
Sección titulada «Endpoints existentes pero fuera del flujo recomendado»| Endpoint | Motivo para no usarlo |
|---|---|
GET /surveys/responses/my-assignments |
Marcado @deprecated en el propio código, reemplazado por GET /me/surveys/assigned. Además, al no tener @TransformTo, sufre el mismo gap de serialización descrito arriba. |
GET /surveys/responses/assignment/{token} |
Redundante con GET /me/surveys/{token}, que devuelve la misma información ya transformada de forma segura. Este también carece de @TransformTo. |
Lógica de cliente
Sección titulada «Lógica de cliente»Estados de una asignación (AssignmentStatus)
Sección titulada «Estados de una asignación (AssignmentStatus)»| Valor | Significado | Texto sugerido |
|---|---|---|
A |
Asignada, pendiente de responder | Pendiente |
F |
Finalizada | Completada |
Tipos de pregunta (QuestionType) y cómo armar la respuesta
Sección titulada «Tipos de pregunta (QuestionType) y cómo armar la respuesta»| Valor | Tipo | Cómo renderizarla | Campo a enviar en responses[] |
|---|---|---|---|
A |
Texto abierto | Textarea de texto libre | text |
R |
Opción única o múltiple | Radio buttons si isMultiple = 'N'; checkboxes si isMultiple = 'S', listando answerOptions |
answerId |
N |
Escala numérica | Selector/slider con los scale.values de la pregunta (questionType = 'N' usa la escala del factor o de la pregunta según factorType) |
value (el numericValue elegido) |
U |
Escala única | Igual que N (selector de escala), semánticamente una única selección sobre la escala |
value |
Preguntas condicionales
Sección titulada «Preguntas condicionales»Usa isConditional, idParentQuestion e idTriggerAnswer sólo para decidir si se muestra u oculta una pregunta en el formulario (mostrarla cuando la respuesta dada a idParentQuestion sea idTriggerAnswer). No asumas que el backend la exige aunque se cumpla la condición — ver caution anterior.
Tipos de factor (FactorType)
Sección titulada «Tipos de factor (FactorType)»| Valor | Significado | Efecto en la estructura |
|---|---|---|
F |
Escala fija | La escala vive en factor.scale; aplica a todas las preguntas numéricas del factor. |
V |
Escala variable | La escala vive en question.scale, puede variar por pregunta dentro del factor. |
A |
Abierto | Factor sin escala asociada (preguntas de texto libre). |
Política de aceptación
Sección titulada «Política de aceptación»Si survey.requiresPolicyAcceptance = 'S', mostrar policyText (y el archivo policyFile si viene) con un checkbox de aceptación antes del formulario o antes del botón de enviar. Hasta que el checkbox esté marcado, no permitir enviar — el backend igualmente lo rechaza con 400 si policyAccepted no llega en true.
Validaciones previas a cada acción
Sección titulada «Validaciones previas a cada acción»- Antes de habilitar “Enviar”: todas las preguntas no condicionales deben tener una respuesta con el campo correcto para su tipo (
textno vacío paraA;answerIdválido paraR;valuedentro de los valores de la escala paraN/U) — replica en el cliente la lógica devalidateResponsespara evitar el roundtrip con error. - Si
requiresPolicyAcceptance = 'S', exigir el checkbox de política antes de enviar. - No reintentar automáticamente un envío que devolvió
400: son errores de validación de datos, no transitorios — mostrar elmessagepuntual al usuario.
Estados vacíos y casos borde
Sección titulada «Estados vacíos y casos borde»- Sin encuestas asignadas:
GET /me/surveys/assigneddevuelve{ items: [], meta: { totalItems: 0, ... } }— mostrar estado vacío (“No tienes encuestas pendientes”). - Sin encuestas completadas: mismo patrón en
GET /me/surveys/completed— mostrar estado vacío (“Aún no completaste ninguna encuesta”). - Encuesta ya respondida, se reintenta abrir el token:
GET /surveys/responses/survey/{token}yPOST /surveys/responses/submitdevuelven400con"This survey has already been completed". Tratarlo como “esta encuesta ya fue respondida” y volver al listado — no hay endpoint para ver el detalle de las respuestas ya enviadas. - Campaña vencida o no iniciada:
400con"...has expired"/"...has not started yet". ComoGET /me/surveys/assignedya filtra por vigencia de campaña, esto sólo debería darse si el usuario guardó un link/token viejo — mostrar un mensaje de “esta encuesta ya no está disponible” en vez de un error genérico. - Historial sin detalle:
MyCompletedSurveyResponseDtono trae las respuestas dadas — si el negocio pide ver el detalle de una encuesta completada, hoy no hay endpoint que lo resuelva; hay que pedirlo al equipo de backend.