Ir al contenido

Encuestas

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/* requieren Authorization: Bearer <jwt> (ver Convenciones).
  • Los endpoints bajo /surveys/responses/* (estructura de la encuesta y envío de respuestas) son de acceso por token: no requieren Authorization, el token de la asignación es lo que autoriza el acceso. Aun así requieren el header x-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_ANUAL en 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

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.

ResponsePaginatedMyAssignedSurveyResponseDto:

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.

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.

ResponsePaginatedMyCompletedSurveyResponseDto:

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.

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.

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.

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 Token de la asignación (el mismo accessToken usado en los GET anteriores).
responses QuestionResponseDto[] 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 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.

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

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.

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).

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.

  • Antes de habilitar “Enviar”: todas las preguntas no condicionales deben tener una respuesta con el campo correcto para su tipo (text no vacío para A; answerId válido para R; value dentro de los valores de la escala para N/U) — replica en el cliente la lógica de validateResponses para 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 el message puntual al usuario.
  • Sin encuestas asignadas: GET /me/surveys/assigned devuelve { 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} y POST /surveys/responses/submit devuelven 400 con "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: 400 con "...has expired" / "...has not started yet". Como GET /me/surveys/assigned ya 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: MyCompletedSurveyResponseDto no 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.