# Changelog — rama `features/MirkoALS2`
**Fecha:** 20/06/2026  
**Autor:** Mirko Lazarte  
**Base:** `dc9020f` (dump + diagrama ER + migration_006 aplicada)

---

## Resumen ejecutivo

Esta rama implementa 7 funcionalidades / correcciones sobre el módulo académico de SGETECBA.  
Toca: backend FastAPI (5 archivos de rutas, 2 schemas, 1 crud), frontend React/TypeScript (6 páginas, 3 páginas nuevas, router, sidebar, client API) y 2 alteraciones de esquema en la BD PostgreSQL ya aplicadas en desarrollo.

---

## 1. AsignacionHorarios — corrección de bugs críticos

**Archivos:** `frontend/src/pages/AsignacionHorarios.tsx` (nuevo, antes no existía como página separada)

### Problema
Los modales de "crear horario" y "editar docente" no funcionaban porque:
- Los abrían funciones `async` que hacían fetch de catálogos antes de mostrar el modal → si el fetch fallaba, el modal nunca aparecía.
- El pre-seleccionado del docente usaba `oferta.asignacion_docente_id` (ID de la relación) en lugar de `oferta.docente_id` (ID del docente real).

### Solución
- **`useEffect` inicial**: carga todos los catálogos (períodos, carreras, turnos, períodos-clase, docentes) en un único `Promise.all` al montar el componente.
- **Modales sincrónicos**: `abrirModalHorario()` y `abrirModalDocente()` solo modifican estado local; ningún fetch adentro.
- **Fix docente**: `setDocenteId(oferta.docente_id ?? "")`.
- Backend `clases_planificadas_crud.py`: agrega `d.id AS docente_id` al SELECT para exponer el ID correcto.

---

## 2. Panel de Alumnos — nueva página

**Archivos nuevos:**  
- `frontend/src/pages/PanelAlumnos.tsx`

**Archivos modificados:**  
- `backend/app/api/v1/routes/matriculaciones.py` — nuevo endpoint `GET /panel-alumnos`  
- `frontend/src/api/client.ts` — `fetchPanelAlumnos`  
- `frontend/src/types/api.ts` — `PanelAlumnoItem`, `PanelAlumnosResponse`  
- `frontend/src/components/Sidebar.tsx` — ítem "Panel Alumnos"  
- `frontend/src/layouts/SecretariaLayout.tsx` — ítem "Panel Alumnos"  
- `frontend/src/routes/AppRouter.tsx` — rutas `/admin/panel-alumnos` y `/secretaria/panel-alumnos`

### Funcionalidad
Visualización del estado de matrícula por período académico:
- **Filtros**: período (obligatorio) + carrera (opcional).
- **Stat cards**: Nuevos / Antiguos / Sin materias / Total matriculados.
- **3 tabs**:
  - *Nuevos*: alumnos sin historial previo en ningún período anterior.
  - *Antiguos*: alumnos ya registrados que se re-matricularon.
  - *Sin materias*: alumnos con historial en el sistema pero sin inscripciones activas en el período.
- **Búsqueda** por nombre o CI dentro de cada tab.
- **Columna Pensum** (ver ítem 6): badge verde (2023) o azul (2025+) por alumno.

### Endpoint backend
```
GET /matriculaciones/panel-alumnos
  ?periodo_academico_id=<int>   (obligatorio)
  ?carrera_id=<int>             (opcional)
```
Retorna `{ con_materias: [...], sin_materias: [...] }`.  
`es_nuevo` se determina con `NOT EXISTS` verificando inscripciones en otros períodos.

---

## 3. Períodos Académicos → versión de Pensum

**Archivos modificados:**  
- `backend/app/api/v1/routes/periodos_academicos.py`  
- `backend/app/schemas/common.py`  
- `backend/app/api/v1/routes/pensum.py`  
- `backend/app/api/v1/routes/matriculaciones.py`  
- `frontend/src/pages/Periodos.tsx`  
- `frontend/src/types/api.ts`  
- `frontend/src/api/client.ts`

### Motivación
Un mismo año académico puede regirse por normas de pensum distintas (ejemplo: períodos 2024 → norma 2023; períodos 2025–2028 → norma 2025). La BD y el UI deben reflejar esto para que la matrícula ofrezca las materias correctas según la versión del plan de estudios que sigue cada estudiante.

### Cambios de BD (ya aplicados — ver `BD/migration_pensum_calificaciones_20-06-2026.sql`)
- `ALTER TABLE periodos_academicos ADD COLUMN anio_pensum INTEGER`
- `ALTER TABLE estudiante_carreras ADD COLUMN anio_pensum INTEGER`

### Cambios backend
- `periodos_academicos.py`: `anio_pensum` en SELECT, INSERT y UPDATE dinámico.
- `pensum.py`: nuevo endpoint `GET /versiones` → devuelve `[2023, 2025]` (años distintos del pensum activo).
- `matriculaciones.py` → `oferta_para_matricular`: resuelve `anio_pensum` desde `estudiante_carreras` del alumno (con fallback al período activo) y filtra `pen.anio = :anio_pensum` en el JOIN, evitando duplicados cuando coexisten dos versiones del pensum.

### Cambios frontend
- `Periodos.tsx`: selector "Versión de Pensum" (populated dinámicamente desde `/pensum/versiones`) en el formulario de creación/edición. Badge `P{año}` en cada card de período.
- `types/api.ts`: `anio_pensum: number | null` en `PeriodoAcademico`.
- `client.ts`: `fetchPensumVersiones()`.

---

## 4. Pensum multi-versión — datos de prueba

**Solo BD** (ya aplicado — ver SQL adjunto)

Duplicó las 153 entradas del pensum existente (anio=2025) creando 153 nuevas con anio=2023. Así existen dos planes de estudio en paralelo para poder probar qué materias ve cada alumno según su norma.

Actualizaciones:
- Períodos 2024-x → `anio_pensum = 2023`
- Períodos 2025-x / 2026-x → `anio_pensum = 2025`
- Estudiantes 1 y 2 → `anio_pensum = 2023` en `estudiante_carreras`
- Estudiantes 3, 4 y 5 → `anio_pensum = 2025` en `estudiante_carreras`

---

## 5. Notas Finales — exclusivo para docente + motivo obligatorio

**Archivos modificados:**  
- `backend/app/api/v1/routes/calificaciones.py`  
- `frontend/src/pages/Calificaciones.tsx`

### Reglas implementadas
1. **Solo el docente asignado** puede registrar o modificar una nota de tipo `FINAL`. Admin y Asistente Académico ya no pueden hacerlo desde la UI ni desde la API.
2. **Modificar una nota FINAL es posible**, pero el campo `observacion` pasa a ser **obligatorio** — el docente debe indicar el motivo del cambio.
3. El resto de tipos de nota (PARCIAL, RECUPERATORIO, HABILITACION) no se ven afectados.

### Backend
- Helper `_verificar_docente_oferta(db, user_id, inscripcion_id?, oferta_id?)`: verifica en `asignaciones_docente → docentes.usuario_id` que el usuario logueado sea el docente asignado. Lanza HTTP 403 si no.
- `POST /calificaciones`: si `tipo_nota == "FINAL"` → verifica rol `DOCENTE` + ownership.
- `PUT /calificaciones/{id}`: si nota existente es `FINAL` → verifica rol + ownership + `observacion` non-empty (HTTP 422 si falta).

### Frontend
- `canEdit = isDocente` (antes también admin/asistente podían).
- Validación client-side antes de llamar al API: si hay `calificacion_id` y `obs.trim()` vacío → error "El motivo de modificación es obligatorio."
- Input `observacion` con fondo amarillo + borde rojo cuando es edición.

---

## 6. Panel Alumnos — columna Pensum

**Archivos modificados:**  
- `backend/app/api/v1/routes/matriculaciones.py`  
- `frontend/src/types/api.ts`  
- `frontend/src/pages/PanelAlumnos.tsx`

### Qué se agregó
La tabla de alumnos en el Panel ahora muestra en qué versión del pensum está inscripto cada alumno:
- Badge **verde** → pensum 2023 (norma anterior).
- Badge **azul** → pensum 2025+ (norma actual).
- `—` → alumno sin `anio_pensum` asignado en `estudiante_carreras`.

El endpoint `/panel-alumnos` hace `LEFT JOIN sgetecba.estudiante_carreras ec ON ... MIN(ec.anio_pensum)` para incluir el campo. Si hay carrera filtrada, el JOIN es específico a esa carrera.

---

## 7. Calificaciones Admin — nueva página completa

**Archivos nuevos:**  
- `frontend/src/pages/CalificacionesAdmin.tsx`

**Archivos modificados:**  
- `backend/app/api/v1/routes/calificaciones.py` — 3 nuevos endpoints  
- `backend/app/api/v1/routes/carreras.py` — filtro `facultad_id`  
- `backend/app/api/v1/routes/oferta_asignaturas.py` — filtro `asignatura_id`  
- `frontend/src/api/client.ts` — 5 nuevas funciones  
- `frontend/src/types/api.ts` — 5 nuevas interfaces  
- `frontend/src/routes/AppRouter.tsx` — `/admin/calificaciones` → `CalificacionesAdminPage`

### Motivación
El administrador no podía ver las calificaciones porque la página anterior solo estaba pensada para docentes. Se necesitaba: ver todas las notas, conocer el historial completo (quién registró, cuándo, con qué observación), y poder buscar el historial de un alumno en particular.

### Nuevos endpoints backend

| Endpoint | Descripción |
|----------|-------------|
| `GET /calificaciones/admin/buscar-estudiantes?q=X` | Autocomplete de alumnos que tienen calificaciones |
| `GET /calificaciones/admin/historial-estudiante?estudiante_id=X` | Historial completo de un alumno: todas las materias, todos los períodos, todas las notas |
| `GET /calificaciones/admin/por-oferta/{id}` | Todos los alumnos de una oferta con TODAS sus notas (no solo un tipo), `fecha_registro` y `registrado_por_nombre` |

Además:
- `GET /carreras?facultad_id=X` — nuevo parámetro de filtro.
- `GET /oferta-asignaturas?asignatura_id=X` — nuevo parámetro de filtro.

### Página frontend — dos modos

#### Modo "Por oferta / grupo"
Filtro cascade con **combobox** (input + dropdown filtrado, sin dependencias externas):
1. **Facultad** → 2. **Carrera** → 3. **Materia** + **Período** (paralelos) → **Botones de grupo/turno** (ej: "Matemáticas I — Mañana / Grupo A")

Una vez elegida la oferta, la tabla muestra:
- Una columna por tipo de nota presente (PARCIAL, FINAL, RECUPERATORIO…)
- Valor con color verde/rojo (aprobado/reprobado)
- Fecha de registro
- Nombre del usuario que la registró
- Observación (si existe)

#### Modo "Por estudiante"
- Input de búsqueda con autocomplete (`/admin/buscar-estudiantes`): mínimo 2 caracteres.
- Al seleccionar un alumno → historial completo con `rowspan` por materia/período.
- Columnas: Período · Materia · Carrera · Grupo · Tipo · Nota · Resultado · Fecha · Registrado por · Observación.
- Materias sin calificaciones aparecen igual (fila "Sin calificaciones").

Ambos modos tienen botón **Imprimir** (`window.print()` con CSS `@media print` que oculta el resto de la UI).

### Interfaces TypeScript nuevas

```typescript
BuscarEstudianteItem
HistorialCalificacion
HistorialInscripcion
HistorialEstudianteResponse
EstudianteConCalificaciones
AdminPorOfertaResponse
```

---

## Archivos modificados — resumen

### Backend
| Archivo | Cambios |
|---------|---------|
| `routes/calificaciones.py` | Helper `_verificar_docente_oferta`, restricción FINAL, 3 endpoints admin |
| `routes/matriculaciones.py` | `/panel-alumnos`, `oferta_para_matricular` con filtro pensum |
| `routes/periodos_academicos.py` | Campo `anio_pensum` en CRUD |
| `routes/pensum.py` | `GET /versiones` |
| `routes/carreras.py` | Filtro `facultad_id` en listado |
| `routes/oferta_asignaturas.py` | Filtro `asignatura_id` en listado |
| `routes/horario_clases.py` | Mejoras en endpoint `por-periodo` |
| `crud/clases_planificadas_crud.py` | Expone `docente_id` en la oferta |
| `schemas/common.py` | `anio_pensum` en PeriodoAcademicoRead/Create/Update |
| `schemas/facultades.py` | Ajuste menor |
| `schemas/clases_planificadas.py` | Schema actualizado con `docente_id` |

### Frontend
| Archivo | Cambios |
|---------|---------|
| `pages/AsignacionHorarios.tsx` | **Nueva página** — catalogs upfront, modales síncronos, fix docente_id |
| `pages/PanelAlumnos.tsx` | **Nueva página** — panel de estado de matrícula con pensum badge |
| `pages/CalificacionesAdmin.tsx` | **Nueva página** — calificaciones completas para admin |
| `pages/Calificaciones.tsx` | `canEdit = isDocente`, motivo obligatorio, UX feedback |
| `pages/Periodos.tsx` | Selector `anio_pensum`, badge en cards |
| `pages/Matriculaciones.tsx` | Ajustes UI |
| `pages/Estudiantes.tsx` | Ajustes UI |
| `api/client.ts` | +8 nuevas funciones, `fetchCarrerasLista` acepta `facultad_id` |
| `types/api.ts` | +6 interfaces nuevas, `anio_pensum` en PeriodoAcademico y PanelAlumnoItem |
| `components/Sidebar.tsx` | Ítem "Panel Alumnos" |
| `layouts/SecretariaLayout.tsx` | Ítem "Panel Alumnos" |
| `routes/AppRouter.tsx` | 3 rutas nuevas, admin calificaciones → CalificacionesAdminPage |

---

## Cambios de base de datos

Ver `BD/migration_pensum_calificaciones_20-06-2026.sql` para los DDL/DML exactos.

**Tablas alteradas:**
- `sgetecba.periodos_academicos` → columna `anio_pensum INTEGER`
- `sgetecba.estudiante_carreras` → columna `anio_pensum INTEGER`

**Datos insertados/actualizados:**
- 153 filas nuevas en `sgetecba.pensum` (versión 2023)
- Actualización de `anio_pensum` en todos los períodos académicos existentes
- Actualización de `anio_pensum` en `estudiante_carreras` para 5 estudiantes de prueba

---

## Notas de integración

- Los endpoints `/admin/...` de calificaciones usan `CALIFICACION_READ_ROLES` (Admin + Asistente + Docente). Solo Admin llega por el router a la página admin, los demás siguen viendo la vista docente.
- La restricción de Notas Finales es enforcement doble: API (403/422) + UI (botón deshabilitado, error inline).
- El filtro de pensum en `oferta_para_matricular` tiene fallback: si `estudiante_carreras` no tiene `anio_pensum`, usa el `anio_pensum` del período activo actual.
- `BD/seed_horarios.sql` (presente en el worktree) **no se incluye en este commit** — corresponde a datos de prueba propios del ambiente de desarrollo.
