User request — especificación para desarrollo ← Todos los gadgets Ver la maqueta
Gadget 03 · User request

Encuestas de satisfacción de las capacitaciones

wc-laravel · WebClass Trainer (/trainer/home) Laravel 5.7 / Vue 2 Capacitador · Participante Borrador para revisión 2026-09-28

Que la encuesta que se toma después de cada capacitación le llegue a todos los que asistieron, que diga la verdad sobre quién ve las respuestas, y que sus números signifiquen lo que dicen: hoy abrir el enlace cuenta como respondió, una escala de 1 a 10 se guarda como 1 a 5, y la encuesta que se anuncia anónima guarda el nombre de cada uno.

1Lo que hay hoy

En el producto conviven seis sistemas de encuestas. El de las capacitaciones que se usa hoy es el de WebClass Trainer, en el repo Laravel:

PiezaDónde
La aplicaciónSPA Vue 2 en resources/js/components/trainer/*, servida por routes/web.php:623 (/trainer/home/{vue_capture?})
El backendapp/Http/Controllers/Api/trainer/TrainerController.php (6.767 líneas); rutas en routes/api.php:228-375
El formulario que responde el participantepollComponent.vue
Crear la capacitaciónmodal «Nueva Capacitación» repetido en statusComponent.vue, dashboardFourComponent.vue y crmComponent.vue
ReportesdashboardComponent.vue, statusLarav.vue + modalNotasDetail.vue; Excel en Exports/encuestasTraining.php
Buildwebpack.mix.js:55 — resources/js/trainer/trainer.js → public/js. No hace falta una entrada nueva

Los otros cinco no son de capacitaciones o están en desuso: el sistema de 2019 con código de 5 caracteres (TrainingController, tablas trainings, training_questions…), las encuestas globales y escolares del vanilla (home/encuesta_global, home/encuesta), la del SIMCE y las de comunicados. Este gadget toca sólo el del Trainer. No hay nada de SENCE ni de OTEC en ninguno de los dos repos.

El flujo, tal como está construido

  1. En Curriculum se arma el catálogo: área → tema → sesión (area_poll_type → webclass_productos → poll_type), y cada sesión tiene su encuesta (poll → poll_question).
  2. El capacitador crea la capacitación —colegio, sesión, capacitador, fecha, presencial o remota con token de Setmore—: una fila de training_result.
  3. Asigna participantes: del colegio (training_poll_alumnos) o externos, a mano o por Excel (training_poll_alumnos_externos).
  4. Inicia y termina (t_inicio, t_termino). Al terminar se genera un enlace corto, se copia al portapapeles, y un segundo después se manda correo sólo a los externos.
  5. El participante abre el enlace y responde. Lo interno pasa por el login del vanilla; lo externo, por un JWT en la URL.

2Lo que está mal, relevado en el código

No es una lista de gustos: cada punto está en un archivo y una línea. El mecanismo de los tres primeros se volvió a leer línea por línea antes de escribir esto (created() → saveResult, el texto de anonimato, y handleClick); el resto sale del mismo relevamiento y conviene confirmarlo al tocar cada método.

Los números no dicen lo que parecen

Abrir el enlace cuenta como «respondió»

Al cargar el formulario, pollComponent.vue:259 (created()) llama a _updateResult(), que hace POST /api/trainer/saveResult. Y TrainerController::saveResult (líneas 84-99) crea la fila de training_poll con el user_id y la IP —antes de que la persona conteste nada—.

De esa fila dependen tres cosas, y las tres quedan mal:

Se anuncia anónima y no lo es

pollComponent.vue:13: «sus respuestas son necesarias para nuestra mejora continua y son de carácter anónimo». Pero se guardan user_id e user_ip, y los reportes muestran nombre, RUT, correo y foto de cada respondente. Es una promesa incumplida hecha por escrito, y es lo más urgente de todo el documento: se arregla cumpliéndola o dejando de hacerla.

La escala de 1 a 10 se guarda de 1 a 5

La última pregunta se muestra con botones del 1 al 10 (pollComponent.vue:136-169), pero handleClick (:450-503) la reduce: 1-2→1, 3-4→2, … 9-10→5. Un 9 y un 10 quedan iguales; un 8 queda igual que un 7. La persona eligió entre diez y se guardó entre cinco. Además eso sólo pasa si la encuesta tiene 4 preguntas o más (:279): con menos, la última queda en 0 y el formulario no se puede enviar nunca.

A quién no le llega

Seguridad

3Quién lo usa

4Dónde vive

En el Trainer, sin módulo nuevo: el detalle de la capacitación reemplaza a los tres modales duplicados de «Nueva Capacitación» y a «Mis capacitaciones» (statusComponent.vue, hoy comentado en menuTrainer.vue:42 pero accesible por ruta). El formulario reemplaza a pollComponent.vue. Van en el bundle que ya existe (webpack.mix.js:55).

Una sola ruta de respuesta para internos y externos, con el token de la invitación (§8) y sin login. Hoy son dos caminos —el interno pasa por el login del vanilla y un auto-POST a /loginFromWebclass; el externo por JWT—, con dos pares de tablas y el reporte Excel mirando sólo uno. El login no agrega seguridad sobre un token al azar de una persona, y le cuesta la respuesta a quien no recuerda su clave en el teléfono.

5Pantalla 1 — Asignar (el capacitador)

La lista

Las capacitaciones con su sesión, colegio, fecha, capacitador, estado (programada · en curso · terminada) y, en una columna propia, «6 de 10 respondieron» con su barra. Es lo que el capacitador necesita saber de un vistazo: si tiene que insistir.

El detalle

Participantes

La encuesta

El envío

6Pantalla 2 — Responder (el participante)

7Resultados

En el mismo detalle de la capacitación, a la derecha:

8El modelo de datos

Una tabla nueva: la invitación nueva

Es la pieza que falta. Hoy «a quién se le envió», «quién abrió» y «quién respondió» se deducen de la existencia de filas en tablas distintas, y por eso se confunden.

CREATE TABLE training_poll_invitacion (
  id                  INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  training_result_id  INT UNSIGNED NOT NULL,
  usuario_id          INT UNSIGNED NULL,          -- con cuenta en WebClass
  usuario_externo_id  INT UNSIGNED NULL,          -- usuario_colegio_temporal
  token               CHAR(20) NOT NULL UNIQUE,   -- al azar, de UNA persona
  enviada_at          DATETIME NULL,
  abierta_at          DATETIME NULL,
  respondida_at       DATETIME NULL,
  recordatorios       TINYINT UNSIGNED NOT NULL DEFAULT 0,
  ultimo_recordatorio DATETIME NULL,
  borrador            JSON NULL,                  -- lo contestado a medias
  CHECK ((usuario_id IS NULL) <> (usuario_externo_id IS NULL))
);

Columnas nuevas en training_result nuevas

ColumnaQué guarda
anonima TINYINTLo que se le prometió al participante. Inmutable desde enviada_at.
destinatarios ENUM('asistentes','todos')Quién la recibe. Inmutable desde enviada_at.
enviada_at, cierra_at DATETIMECuándo salió y hasta cuándo se responde.
recordatorio, certificado TINYINTLas dos opciones.

El tipo de pregunta, explícito nuevo

poll_question suma tipo ('acuerdo5', 'escala10', 'sino', 'texto'), obligatoria y orden, y crearPregunta tiene que escribir poll_group_id. El formulario deja de adivinar el tipo por la posición: hoy «la última es la de caritas», y eso rompe cualquier encuesta de menos de 4 preguntas.

Cambios de comportamiento sobre lo que ya existe

9Seguridad, lo mínimo

10Fuera de alcance

11Criterios de aceptación

  1. Abrir el enlace y cerrarlo no suma una respuesta, no pone a nadie en el certificado y no bloquea la edición.
  2. Al terminar, la encuesta llega por correo a todos los destinatarios, con cuenta y sin ella, cada uno con su enlace.
  3. Quien no asistió no la recibe (si así está configurado); quien llegó tarde se marca y se le puede enviar.
  4. Pegar desde Excel marca, antes de agregar, los correos mal escritos, los repetidos y los que ya estaban.
  5. Cada participante muestra uno de cuatro estados —sin enviar, enviada, abrió sin responder, respondió— con su hora.
  6. El recordatorio no le llega a quien ya respondió.
  7. Una encuesta anónima no guarda quién dio cada respuesta, y el reporte no muestra nombres. Con menos de 3 respuestas no muestra resultados.
  8. El texto que lee el participante coincide con lo configurado.
  9. El anonimato y los destinatarios no se pueden cambiar después de enviar.
  10. Un 9 en la escala de 0 a 10 se guarda como 9.
  11. Cerrar el formulario a medias y volver a abrirlo conserva lo contestado.
  12. No se puede enviar incompleta; se marcan las preguntas que faltan. «No» en la duración exige comentario.
  13. Se responde una sola vez; el segundo envío se rechaza en el servidor, no sólo en la pantalla.
  14. Una encuesta de 3 preguntas se puede enviar.
  15. Nadie puede responder a nombre de otro cambiando un id en el request.

12Qué hay que decidir antes de programar

  1. ¿Anónima o con nombre, por defecto? La maqueta propone anónima, porque es lo que hoy se promete. Si el negocio necesita saber quién dijo qué —para llamar a un colegio descontento, por ejemplo—, la salida honesta es «con nombre», dicho así. Lo que no puede seguir es prometer lo primero y hacer lo segundo.
  2. ¿El certificado depende de responder la encuesta? Hoy sí, de hecho (y de abrirla). Recomendación: que dependa de la asistencia. Condicionarlo a responder presiona a contestar cualquier cosa para descargarlo, y sesga justo los números que se quieren medir. La maqueta lo entrega al terminar la encuesta porque es donde el participante está, pero el mismo certificado debería poder bajarse desde el correo aunque no responda.
  3. ¿Qué se hace con lo histórico de la escala? Está guardado de 1 a 5. O se muestra aparte, o se convierte a una escala común sabiendo que se perdió resolución.
  4. ¿Cuántos días por defecto? La maqueta usa 7.
  5. El proveedor de correo y su límite: hoy es la cola de Laravel (EnviarCorreoEncuestaEx). Mandarle a todos —no sólo a externos— multiplica el volumen.
  6. Las preguntas reales de cada sesión: la maqueta usa textos verosímiles, no los de producción. Con el tipo explícito, hay que asignarle uno a cada pregunta existente.
Sobre la maqueta

Es HTML autónomo y no guarda nada ni envía correos: al recargar vuelve al estado inicial. Las dos vistas —capacitador y participante— comparten el estado, así que lo que se responde en el teléfono aparece en los resultados al volver a la vista 1. El reloj está fijo en el lunes 28 de septiembre a las 10:12 y avanza unos minutos con cada acción.

Atajos para una reunión: ?cap=1 abre la capacitación terminada (con resultados), ?cap=2 la de hoy —para terminarla y enviar—, ?cap=3 la programada en un colegio temporal, y ?vista=participante abre el teléfono.

← Todos los gadgets