# 05 — Flujos de negocio clave

---

## 5.1 El periodo como eje temporal

Casi ninguna consulta tiene sentido sin periodo. El periodo activo vive en
`$_SESSION[appId]['filtersSesion']['Periodo']` y se lee con `PeriodosModel::getSesionId()`.

- Se inicializa en el login (`Controller::filtersSesionValuesDefault()`), tomando el primer
  resultado de `PeriodosModel::getAllFiltersSesion()`.
- El usuario lo cambia con el selector que pinta `Controller::loadFiltersSesion()` en la barra
  superior, que hace POST a `home/changeFiltersSesion` y vuelve a la misma pantalla.
- `Config::$FILTERS_SESION` declara los filtros globales: `Estado` (default 1) y `Periodo`.
- Cuando un modelo tiene una columna llamada `Periodo`/`IdPeriodo` (o `Estado`/`IdEstado`) y la
  consulta no pasa `filtersGeneral = array()`, el ORM añade solo
  `FIND_IN_SET('<valor>', <Columna>)`.

Cada periodo define **ventanas de tiempo por actor**: `InicioEstudiantes`/`FinEstudiantes`,
`InicioDocentes`/`FinDocentes`, `InicioDirectores`/`FinDirectores`,
`InicioDirectoresNacionales`/`FinDirectoresNacionales`, `InicioPares`/`FinPares`,
más banderas `Aplicar*` que activan o desactivan cada instrumento.

`PublicController::verificarGestion()` se llama al principio de toda acción de evaluación:
si hoy está fuera de la ventana del rol, busca **otro periodo cuya ventana esté abierta**;
si lo encuentra cambia el filtro de sesión y recarga, y si no, redirige a `public/gestiones`,
que explica al usuario en qué punto del calendario está.

---

## 5.2 Las cuatro evaluaciones

Todas siguen el mismo patrón de dos pasos: **Selección** (genera la evaluación en blanco) →
**Evaluación** (guarda calificaciones y cierra).

### Paso 1 — "Generar" (acción `*Seleccion`)

1. Se leen las preguntas activas filtrando por `preguntas.Tipo` (y por `Metodologia` y/o `Area` según el caso), ordenadas por `Orden`.
2. Se inserta una fila en la tabla de evaluaciones con `EstadoEvaluacion = 1` (En proceso).
3. Se inserta **una fila por pregunta** en la tabla de respuestas con `Calificacion = 0`.

### Paso 2 — "Guardar" (acción `*Evaluacion`)

1. Por cada respuesta enviada se hace `editFromParameters(['Calificacion' => …], WHERE Id = …)`.
2. Se calcula `Resultado` = **media aritmética simple de las calificaciones > 0**.
   (Nota: `preguntas.Peso` existe pero **no se usa** en este promedio.)
3. Se genera un `Codigo` de comprobante: `substr(md5(rand()), 0, 6)`.
4. Se actualiza la evaluación con `EstadoEvaluacion = 2`, `Observaciones`, `Resultado`, `Codigo`.

### Diferencias por instrumento

| Instrumento | Acción | `preguntas.Tipo` | Tabla | Quién es quién |
|---|---|---|---|---|
| **Estudiante → docente/materia** | `estudiantesSeleccion` / `estudiantesEvaluacion` | 1 | `estudiantes_evaluaciones` | `IdEstudiante` = evaluador, `IdAsignaturaDocente` → `docentes_asignaturas` (identifica docente+materia+grupo) |
| **Autoevaluación** | `docentesAutoSeleccion` / `docentesAutoEvaluacion` | 2 | `docentes_evaluaciones` | `IdDocente` = el propio docente, **`IdDocentePar = 0`** |
| **Par académico** | `docentesSeleccion` / `docentesEvaluacion` | 4 | `docentes_evaluaciones` | ⚠️ `IdDocente` = **el evaluador** (usuario en sesión), `IdDocentePar` = el evaluado |
| **Director / Decano** | `directoresSeleccion` / `directoresEvaluacion` | 3 | `directores_evaluaciones` | `IdDirector`, `IdDocente` |
| **Líder Funciones Sustantivas** | idem | 5 (+ filtro `Area`) | `directores_evaluaciones` | El `Area` sale de `tipos_labor_docente` donde `IdDirector` = el líder |
| **Líder Docencia Virtual** | idem | 6 | `directores_evaluaciones` | |

Detalles particulares del flujo de director:
- Si `directores.Reemplazo` no está vacío, la evaluación se registra a nombre del director
  **reemplazado**, no del que está en sesión (delegación).
- El director puede marcar "No aplica" por docente: se guarda `EstadoEvaluacion = -1` y
  **no se generan respuestas**. Al guardar, si no hay ninguna calificación > 0 también queda en `-1`.

La bandera `Config::$config_metodologias` (`estudiantes`/`pares`/`directores`) decide si el
cuestionario se discrimina además por metodología P/V. En la instancia de Compensar solo está
activa para estudiantes.

---

## 5.3 Consolidación de resultados

`ConsolidadosController::evaluacionesAction()` — botón **"Consolidar"** del menú
*Consolidar Evaluaciones*. Es un proceso **destructivo e idempotente**: borra todo el consolidado
del periodo y lo vuelve a calcular para **todos** los docentes.

Requisito previo: los pesos deben existir en `configuraciones` con `Grupo = 'Pesos'`:
`PesoEstudiantes`, `PesoAutoevaluaciones`, `PesoDirectores` (valores en %).

Para cada docente:

```
TotalEstudiantes        = AVG(estudiantes_evaluaciones.Resultado)
                          sobre sus docentes_asignaturas, con EstadoEvaluacion = 2
                          (también se guardan los promedios separados P y V)

TotalDirector           = directores_evaluaciones.Resultado  del director jefe
                          (WHERE IdDirector = docentes.Jefe, EstadoEvaluacion = 2)

TotalInstrumento<XX>    = AVG(Resultado) de los líderes de proceso de ese instrumento
                          (directores con Tipo IN (2,3) agrupados por directores.Instrumento)

TotalAutoridadSuperior  = promedio de TotalDirector y la media de los instrumentos,
                          usando solo los términos que existan (si falta uno, vale el otro; si faltan ambos, 0)

TotalAutoevaluaciones   = media de las autoevaluaciones P y V (o la única que exista)

TotalDefinitiva:
  si hay estudiantes Y autoridad superior:
      PesoEstudiantes·TE/100 + PesoAutoevaluaciones·TA/100 + PesoDirectores·TAS/100
  si solo hay estudiantes:
      se reescala:  k = 100/(PesoAutoevaluaciones + PesoEstudiantes)
      k·PesoAutoevaluaciones·TA/100 + k·PesoEstudiantes·TE/100
  si solo hay autoridad superior:
      k = 100/(PesoAutoevaluaciones + PesoDirectores)  … análogo
  si no hay ninguna de las dos:
      TotalDefinitiva = TotalAutoevaluaciones
```

Se escribe una fila en `consolidado_evaluaciones` con los totales y, en `Detalles`, **el array
completo del docente serializado a JSON** (incluye programa, facultad, sede, totales P/V y por
instrumento). Muchos reportes leen ese JSON en vez de recalcular.

> Las evaluaciones de **pares** (`IdDocentePar != 0`) **no entran** en el consolidado actual.

---

## 5.4 Análisis con IA

Dos procesos, ambos en `SincronizacionController`, ambos disparados manualmente con un POST
`import` desde la vista `sincronizacion/consola`, ambos usando **Ollama** (`app/services/OllamaService.php`).

### `openIAClasificacion` — clasificar comentarios

- Selecciona `estudiantes_evaluaciones` del periodo con `EstadoEvaluacion = 2` y
  `ObservacionesClasificacion IS NULL`, agrupadas por `Observaciones` (evita repetir la misma frase).
- Si ese texto ya fue clasificado antes, **reutiliza** la clasificación sin llamar al modelo.
- Si el comentario queda vacío tras limpiar puntuación, se marca `NA` directamente.
- Prompt: *"Necesito que califique el comentario con una de las siguientes categorías: POSITIVO, NEUTRAL, OPORTUNIDAD DE MEJORA o NA…"*. Modelo por defecto: `llama3.3:70b`.
- Normaliza la salida (`NEGATIVO → OPORTUNIDAD DE MEJORA`, `POSITIVE → POSITIVO`) y solo acepta las cuatro categorías válidas.
- Actualiza **todas** las evaluaciones con ese mismo texto de observación.
- Manejo de errores: HTTP 503 → `sleep(10)` y continúa; error 28 (timeout cURL) → continúa; cualquier otro → `break`.

### `openIAResumen` — perfil de fortalezas y debilidades

- **Precondición**: no debe haber docentes "faltantes" (evaluaciones con `DocenteNombre IS NULL`, es decir, carga académica huérfana). Si los hay, sólo los lista y no genera nada.
- Por cada docente del periodo sin fila en `resumen_evaluacion`, concatena todos sus comentarios (excluyendo los clasificados como `NA`) en una lista numerada.
- Prompt: pide un texto con los títulos `Fortalezas:`, `Debilidades:` y `Resumen:`. Modelo: `qwen3.6:27b`.
- Limpia el markdown de los títulos y guarda todo en `resumen_evaluacion.Conclusion`.
- Si el docente no tiene comentarios, guarda un texto fijo de "sin información suficiente".

Los resultados se consumen en `reportes/observaciones_ia`, `graficas/docente_analisis_ia` y el
endpoint `ajax/getResumen`. También se pueden **importar desde archivo** con
`archivos/importObservacionesClasificacion` y `archivos/importResumenEvaluacion` cuando el
proceso se ejecuta fuera de línea.

---

## 5.5 Sincronización con Academusoft

`SincronizacionController` + `SincronizacionModel`. La consola vive en
`?c=sincronizacion` y ofrece dos "instancias" porque la universidad separa los datos:

- **PREGRADO Y POSTGRADO** (`pregrado`): todo lo que **no** es sede "ESCUELA DE FORMACIÓN EMPRESARIAL".
- **ESCUELA** (`escuela`): solo esa sede.

Pasos, en este orden:

| # | Acción | Qué hace |
|---|---|---|
| 1 | `estudiantes` / `escuelaEstudiantes` | `EstudiantesModel` → pone todos en `Estado = 0`, luego crea/actualiza los que devuelve el API y los reactiva. Crea programas que no existan (con `IdFacultad = -1`, **hay que asignarles facultad a mano después**). Mapea sede por nombre y metodología PRESENCIAL→`P` / VIRTUAL→`V` |
| 2 | `estudiantesMaterias` / `escuelaEstudiantesMaterias` | `estudiantes_asignaturas` |
| 3 | `docentesMaterias` / `escuelaDocentesMaterias` | `docentes_asignaturas`; crea asignaturas faltantes (`crearAsignatura()`) |
| 4 | `docentes` / `escuelaDocentes` | `docentes` |
| 5 | `actualizarJefes` | Asigna `docentes.Jefe` a partir de la estructura de programas/directores |
| 6 | `agregarLabores` | Crea `labor_docente` desde `proyectos_docentes` |

Todas las acciones muestran una **consola** (`app/views/sincronizacion/consola.php`) con
contadores (`Total`, `Creados`, `Editados`, `Omitidos`, `Fallos`) y los errores fila a fila.
Nada se ejecuta sin `POST['import']`, así que abrir la pantalla es seguro.

Detalle del API: ver [07 — Integraciones](07-integraciones.md).

Hay además una acción `cambiarNombres` que **anonimiza** datos con Faker (`?model=<Nombre>`),
pensada para preparar entornos de demo. **No ejecutarla en producción.**

---

## 5.6 Notificaciones por correo

1. **Configurar plantillas**: `notificaciones/configurar` guarda en `configuraciones` (grupo `Notificaciones`) las claves `NotificacionEstudiantesAsunto/Mensaje`, `NotificacionAutoevaluacionAsunto/Mensaje`, `NotificacionDirectoresAsunto/Mensaje`.
2. **Encolar**: `notificaciones/estudiantes|docentes|directores` borra la cola previa de ese tipo (`EVA_EST`, …), consulta quién tiene evaluaciones sin finalizar y crea filas en `alertas_email` con **hasta 500 destinatarios por fila** y `EstadoEnvio = 1`.
3. **Enviar**: `alertasEmail/sendCron` (acción **pública**, para cron externo) toma 2–4 alertas pendientes y las manda con PHPMailer, con `sleep(5)` entre envíos. El interruptor global es la clave `DetenerEnvioNotificaciones` y el tamaño de lote lo modula `EnvioMaximoNotificaciones`.

Configuración SMTP: `Config::$email_send` (sobreescribible en `ConfigEnv`).

---

## 5.7 Labor docente y ODS

**Labor docente** (`labor_docente`): actividades no lectivas del docente en el periodo, con
`Tipo` (catálogo `tipos_labor_docente`, que tiene `Area` e `IdDirector` responsable),
`Objetivo`, horas planificadas (`HorasSemanales × NoSemanas = TotalHoras`), `Evidencia` y
un doble seguimiento de avance: `Progreso`/`Impacto` (por el director) y
`ProgresoNacional`/`ImpactoNacional` (por el líder de proceso). Los cambios de avance se
historian en `seguimiento_progresos`. Se pueden importar con `laborDocente/importLabores`.

**ODS** (`ods` + `ods_docentes`): documento por programa y periodo que consolida la carga y la
contratación de sus docentes. Cabecera con sede/facultad/programa/centro de costos y consecutivo;
detalle por docente con dedicación, vigencia, horas (docencia, virtual, preparación), fechas de
contrato, escalafón y salario. Tiene flujo de **firmas** (`FirmaDocente`, `FirmaDirector`,
`FirmaCarreraDocente`) y de **recepción** (`Recibida`, `FechaRecibida`, `IdUsuarioRecibida`).
Se imprime en PDF (`ods/imprimir`, `ods/imprimirLabor`) y se firma desde el portal
(`odsDocentes/firmarActa`, y para el docente `public/docentesActasView`).

**Requisiciones** (`requisiciones`): solicitud de vinculación de un docente nuevo. Estados
1 Solicitada → 2 Recibida → 3 Seleccionada → 4 Finalizada (o 11 Rechazada), con bandejas
por estado y acción `devolver` con `MotivoDevolucion`. Al eliminar una línea de ODS con "DN"
asociada, se borra la requisición vinculada.

**OPS** (`ops`): contratación puntual de conferencistas/talleristas, con flujo de aprobación
(`AprobacionEstado`, `AprobacionComentario`).

---

## 5.8 Escalafón y convocatorias de ascenso

1. `escalafon` define las categorías (Instructor → Titular, más "Especial") con el puntaje
   requerido y el salario para cada dedicación (TC/MT/CA/HC).
2. `convocatorias` abre un proceso con ventanas de postulación y de validación.
3. `docentes_convocatorias` inscribe a cada docente con su escalafón actual, dedicación y
   puntos acumulados.
4. El docente se postula desde el portal (`public/docentesConvocatorias`) adjuntando evidencias:
   cada evidencia es una fila en `postulaciones` que referencia un producto de
   `produccion_intelectual` (que aporta la puntuación de referencia).
5. El evaluador califica cada postulación (`convocatorias/postulaciones`).
6. `convocatorias/docente` resuelve el ascenso: compara
   `PuntosAcumulados + Σ Calificacion` contra el `PuntajeRequerido<Dedicacion>` de la
   siguiente categoría, y registra `EstadoAscenso` y, si no procede, el motivo en `Requisitos`.
7. `archivos/generarCarta` produce la carta oficial en PDF (formatos FOR-PAJ-70 "Notificación
   acumulación de puntos" y FOR-PAJ-71 "Notificación escalafón docente").
8. El docente consulta el resultado en `public/docentesEscalafonEstado` (visible solo si
   `convocatorias.PublicarResultados` está activo).
