Las acciones de tamizaje coordinan el flujo completo de evaluación: scoring local de las 188 respuestas, llamada a la ML API para clasificación (con fallback automático), y persistencia en PostgreSQL a través de Prisma.
Estas funciones están marcadas con "use server" y se invocan desde componentes React. No son endpoints HTTP — Next.js las serializa en el bundle del servidor.
procesarYGuardarTamizaje
export async function procesarYGuardarTamizaje(
estudianteId: string,
respuestas: number[],
{ skipML = false }: { skipML?: boolean } = {}
): Promise<{ semaforo: string; tipoCaso: string }>
Núcleo del tamizaje. Realiza el scoring local, consulta la ML API, y guarda el tamizaje y las respuestas crudas en la base de datos. Es la función base utilizada por guardarTamizaje e importaciones masivas.
Parámetros
| Parámetro | Tipo | Descripción |
|---|
estudianteId | string | ID del estudiante (CUID de Prisma) |
respuestas | number[] | Array de 188 respuestas con valores 1–5 |
skipML | boolean | Si es true, omite la llamada a la ML API y usa solo el scoring local. Útil para importaciones masivas. Valor por defecto: false |
Retorno
{ semaforo: string; tipoCaso: string }
semaforo y tipoCaso reflejan el resultado final — del modelo ML si estuvo disponible, o del scoring local como fallback.
Flujo interno
- Scoring local — Calcula puntuaciones brutas por escala usando
calcularResultado(). Siempre disponible, no depende de la ML API.
- Llamada ML (si
skipML es false) — Construye el payload con las escalas calculadas y hace POST /api/v1/clasificar con un timeout de 5 segundos. Si la API no está disponible o retorna error, continúa con el resultado del scoring local sin propagar el error.
- Persistencia — Guarda un registro
Tamizaje con todas las escalas y el resultado final.
- Respuestas crudas — Guarda las respuestas brutas en
RespuestasCuestionario con procesado: true.
Ejemplo
import { procesarYGuardarTamizaje } from "@/lib/actions/tamizaje"
// Desde una importación masiva, omitiendo el ML
const resultado = await procesarYGuardarTamizaje(
"cm1abc123",
[3, 2, 1, 4, 2, /* ... 183 más */],
{ skipML: true }
)
console.log(resultado.semaforo) // "VERDE"
console.log(resultado.tipoCaso) // "SIN_RIESGO"
guardarTamizaje
export async function guardarTamizaje(
estudianteId: string,
respuestas: number[],
esEstudiante?: boolean
): Promise<GuardarTamizajeResult>
Wrap de procesarYGuardarTamizaje con validación de entrada y redirección final. Diseñada para ser invocada desde el formulario del cuestionario.
Parámetros
| Parámetro | Tipo | Descripción |
|---|
estudianteId | string | ID del estudiante |
respuestas | number[] | Array de respuestas |
esEstudiante | boolean | Si true, redirige a /cuestionario/mi-sena/completado. Si false (por defecto), redirige a /cuestionario/{estudianteId}/completado |
Retorno
export type GuardarTamizajeResult = { error: string } | undefined
Retorna undefined en éxito (la función llama a redirect() antes de retornar). Retorna { error: string } si la validación falla o hay un error de base de datos.
Validaciones
- El array
respuestas debe tener exactamente TOTAL_REACTIVOS elementos (188).
- Cada respuesta debe estar en el rango
[1, 5].
Ejemplo
import { guardarTamizaje } from "@/lib/actions/tamizaje"
// Desde un componente de servidor o useActionState
const resultado = await guardarTamizaje(
"cm1abc123",
respuestasArray,
false // redirige a /cuestionario/cm1abc123/completado
)
if (resultado?.error) {
console.error(resultado.error)
// "Debe completar todos los reactivos antes de enviar."
// "Respuestas fuera de rango (1–5)."
// "Error al guardar el tamizaje. Intente de nuevo."
}
Modelo de datos: Tamizaje
Registro persistido en PostgreSQL con todas las escalas del SENA y el resultado de clasificación.
| Campo | Tipo | Descripción |
|---|
id | String | CUID autogenerado |
fecha | DateTime | Fecha y hora del tamizaje (automática) |
estudianteId | String | FK al estudiante |
inc | Float | Escala Inconsistencia |
neg | Float | Impresión negativa |
pos | Float | Impresión positiva |
glo_t … rec_t | Int | 6 índices globales (T) |
dep_t … obs_t | Int | 6 escalas de problemas interiorizados (T) |
ate_t … ant_t | Int | 6 escalas de problemas exteriorizados (T) |
sus_t, esq_t, ali_t | Int | 3 escalas de otros problemas (T) |
fam_t, esc_t, com_t | Int | 3 escalas contextuales (T) |
reg_t, bus_t | Int | 2 vulnerabilidades (T) |
aut_t, soc_t, cnc_t | Int | 3 recursos personales (T) |
tipoCaso | TipoCaso | Resultado de clasificación |
semaforo | Semaforo | Nivel de riesgo |
observaciones | String? | Texto descriptivo del resultado |
itemsCriticos | Json | Lista de ítems críticos activados |
Enums
TipoCaso
import type { TipoCaso } from "@/lib/enums"
| Valor | Descripción |
|---|
INCONSISTENCIA | Cuestionario con inconsistencias — solicitar repetición |
SIN_RIESGO | Sin indicadores de riesgo |
IMPRESION_POSITIVA | Sesgo de presentación positiva detectado |
IMPRESION_NEGATIVA | Impresión negativa con índices elevados |
CON_RIESGO | Riesgo emocional o conductual confirmado |
Semaforo
import type { Semaforo } from "@/lib/enums"
| Valor | Color | Acción recomendada |
|---|
VERDE | Verde | Seguimiento periódico normal |
AMARILLO | Amarillo | Revisión preventiva con orientador o psicólogo |
ROJO | Rojo | Programar cita con psicólogo |
ROJO_URGENTE | Rojo urgente | Atención inmediata |
Integración con la ML API
La acción procesarYGuardarTamizaje construye el payload para POST /api/v1/clasificar directamente desde los resultados del scoring local:
const mlUrl = process.env.ML_API_URL ?? "http://localhost:8000"
const mlRes = await fetch(`${mlUrl}/api/v1/clasificar`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
estudiante_id: estudianteId,
inc: pb.inc ?? 0,
neg: pb.neg ?? 0,
pos: pb.pos ?? 0,
// ... todas las escalas clínicas
items_criticos_count: scoring.itemsCriticos.length,
respuestas: respuestas.join(""),
edad: estudiante?.edad ?? 15,
sexo: estudiante?.sexo ?? "MASCULINO",
}),
signal: AbortSignal.timeout(5000), // timeout de 5 segundos
})
Si la ML API no está disponible (timeout, error de red, respuesta no-OK), la acción no falla. El resultado del scoring local ya calculado se usa directamente como semaforo y tipoCaso. Esto garantiza que los tamizajes siempre se guarden aunque el servicio ML esté caído.