# Sesión de Trabajo — 23 de Junio 2026

**Rama:** `feature/santi`  
**Responsable:** Santiago Caballero  
**Asistido por:** Claude Sonnet 4.6  

---

## Resumen ejecutivo

Sesión de corrección de bugs y mejoras al flujo docente. Se realizaron 4 fixes y 2 features nuevas, todas con QA profesional individual. Backup pre-sesión tomado al inicio.

---

## Backup pre-sesión

**Archivo:** `BD/backups/backup_2026-06-23_08-42_pre_dia23.sql`  
**Líneas:** 6518 | **Esquema:** `sgetecba`

---

## 1. Fix — Bug #1: `carreras-por-estudiante` vacío para alumnos nuevos

**Commit:** `2e9d480`  
**Archivo:** `backend/app/api/v1/routes/matriculaciones.py`

**Causa raíz:** `GET /matriculaciones/catalogos/carreras-por-estudiante?estudiante_id=X` solo buscaba carreras a través de `inscripciones`. Un alumno recién creado (sin inscripciones) devolvía lista vacía → el modal de Matriculaciones no mostraba carrera.

**Fix:** UNION con `estudiantes.carrera_id` como fallback:

```sql
SELECT c.id, c.nombre FROM sgetecba.inscripciones i
  JOIN sgetecba.ofertas_asignaturas oa ON ...
  JOIN sgetecba.carreras c ON c.id = oa.carrera_id
  WHERE i.estudiante_id = :eid AND i.eliminado_en IS NULL
UNION
SELECT c.id, c.nombre FROM sgetecba.estudiantes e
  JOIN sgetecba.carreras c ON c.id = e.carrera_id
  WHERE e.id = :eid AND e.eliminado_en IS NULL
ORDER BY nombre
```

**QA:** Modal muestra carrera correctamente para alumnos sin inscripciones → ✅ PASS

---

## 2. Fix — Bug #2: `AsignacionHorarios` no cargaba por `Promise.all` + `page_size` 300

**Commit:** `2e9d480`  
**Archivos:** `frontend/src/api/client.ts`, `frontend/src/pages/AsignacionHorarios.tsx`

**Causa raíz 1:** `fetchDocentesLista()` pedía `page_size: 300` pero el backend tiene `le=100` → 422 → `Promise.all` falla toda la carga.

**Causa raíz 2:** `Promise.all` interrumpe todo si un fetch falla.

**Fix:**
- `client.ts`: `page_size: 300 → 100`
- `AsignacionHorarios.tsx`: `Promise.all → Promise.allSettled` con checks individuales

**QA:** Asignación de Horarios carga todos los catálogos → ✅ PASS

---

## 3. Fix — Rol MKT + grupo GERENCIAL_READ_ROLES (sesión anterior)

**Commit:** `2e9d480`  
**Archivos:** `backend/app/core/roles.py`, `backend/app/api/v1/routes/reportes.py`

- Nuevo grupo: `GERENCIAL_READ_ROLES = (ADMIN, DIRECCION, MKT)`
- 3 endpoints gerenciales usan `*GERENCIAL_READ_ROLES`
- Usuario `marketing` puede ver reportes gerenciales

**QA:** login marketing → `GET /reportes/gerencial/*` → 200 → ✅ PASS

---

## 4. Fix — Auto-resolve `asignacion_docente_id` al crear horario

**Commit:** `e182eda`  
**Archivo:** `backend/app/crud/clases_planificadas_crud.py`

**Situación:** Admin crea horario con `POST /horario-clases` sin `asignacion_docente_id`. El campo queda NULL. `GET /docente/horario` filtra `WHERE asignacion_docente_id = :aid` → docente no ve su horario.

**Fix:** En `crear_horario_admin`, si no se pasa `asignacion_docente_id`, auto-lookup de la asignación activa de la oferta:

```python
if not asignacion_docente_id:
    row_asig = db.execute(text("""
        SELECT id FROM sgetecba.asignaciones_docente
        WHERE oferta_asignatura_id = :oid
          AND activo = true AND eliminado_en IS NULL
        LIMIT 1
    """), {"oid": payload.oferta_asignatura_id}).first()
    if row_asig:
        asignacion_docente_id = row_asig[0]
```

**Fix DB complementario:** `horario_clases` id=1 ya existía con `asignacion_docente_id=null` → update directo:
```sql
UPDATE sgetecba.horario_clases SET asignacion_docente_id = 1 WHERE id = 1;
```

**QA:** `GET /docente/horario` muestra Programacion I con Lunes 07:00 y Martes 08:30 → ✅ PASS

---

## 5. Feature — Vista horario del alumno en Panel Alumnos

**Commit:** `315a250`  
**Archivos:** `backend/app/api/v1/routes/estudiantes.py`, `frontend/src/api/client.ts`, `frontend/src/pages/PanelAlumnos.tsx`

### Backend — nuevo endpoint

```
GET /api/v1/estudiantes/{estudiante_id}/horario?periodo_academico_id=X
```

Roles: `ACADEMIC_READ_ROLES`

Query: `inscripciones → ofertas_asignaturas → horario_clases + asignaciones_docente`  
Respuesta agrupada por materia (asignatura_nombre, turno, grupo, docente, horarios[]).

### Frontend

- `client.ts`: `fetchHorarioEstudiante(estudiante_id, periodo_academico_id)`
- `PanelAlumnos.tsx`: botón "Horario" por fila → modal con tabla de materias y bloques horarios

**QA:** Omar Barea Hip → modal muestra Programacion I | Manana · Grupo A · Docente Demo | Lunes 07:00 → ✅ PASS

---

## 6. Fix — "Este docente ya está asignado" (409 incorrecto por soft-delete)

**Commit:** `ff4fca2`  
**Archivo:** `backend/app/crud/docentes_crud.py`

**Causa raíz:** La tabla tiene dos índices únicos contradictorios:

| Índice | Tipo | Problema |
|--------|------|---------|
| `..._docente_id_oferta_asignatura_id_key` | UNIQUE full (sin WHERE) | Bloquea re-INSERT post soft-delete |
| `ux_asignaciones_docente_activo` | UNIQUE partial `WHERE eliminado_en IS NULL` | Correcto para soft-delete |

Cuando la asignación es soft-deleted y el usuario intenta volver a asignar, el INSERT viola el UNIQUE full y el backend devuelve 409 — pero la UI muestra "Sin docente" (stale data).

**Fix:** En `create_asignacion`, si existe un registro soft-deleted para el mismo par → `UPDATE SET eliminado_en=NULL, activo=TRUE` en lugar de INSERT:

```python
existing = db.execute(text("""
    SELECT id FROM sgetecba.asignaciones_docente
    WHERE docente_id = :docente_id
      AND oferta_asignatura_id = :oferta_asignatura_id
      AND eliminado_en IS NOT NULL
    LIMIT 1
"""), ...).first()

if existing:
    db.execute(text("""
        UPDATE sgetecba.asignaciones_docente
        SET eliminado_en = NULL, activo = TRUE, actualizado_en = now()
        WHERE id = :id
    """), {"id": existing[0]})
```

**QA — 6/6 checks PASS:**

| Check | Resultado |
|-------|-----------|
| Re-asignar docente soft-deleted → 201 | ✅ PASS |
| Horario por-periodo muestra docente | ✅ PASS |
| Ciclo DELETE + re-CREATE | ✅ PASS |
| Docente inexistente → 404 | ✅ PASS |
| Duplicado activo → 409 (protección intacta) | ✅ PASS |
| `/docente/horario` muestra materia + horarios | ✅ PASS |

---

## 7. Feature — Filtro `sesion_asistencia_id` en `GET /asistencias`

**Commit:** `f9ff9ef`  
**Archivo:** `backend/app/api/v1/routes/asistencias.py`

**Situación:** `GET /asistencias` no tenía filtro por sesión específica. Sin él, el frontend traía todas las asistencias de la oferta mezcladas.

**Fix:** Nuevo query param opcional `sesion_asistencia_id`:

```python
sesion_asistencia_id: int | None = Query(default=None)
# → agrega: da.sesion_asistencia_id = :sesion_asistencia_id
```

**QA:** `GET /asistencias?sesion_asistencia_id=3` retorna solo los registros de la sesión 3, sin afectar consultas sin filtro → ✅ PASS

---

## 8. Flujo de asistencia — QA end-to-end

**Flujo completo verificado via API:**

| Paso | Operación | HTTP | Estado |
|------|-----------|------|--------|
| 1 | `POST /sesiones` (oferta_id=2, asignacion_id=1, fecha=2026-06-24, ABIERTA) | 201 | ✅ |
| 2 | `GET /sesiones/{id}/alumnos` → 2 alumnos inscritos | 200 | ✅ |
| 3 | `POST /asistencias` (docente) — Omar=Presente, Dayana=Ausente | 200 | ✅ |
| 4 | `PATCH /sesiones/{id}/estado?estado=CERRADA` | 200 | ✅ |
| 5 | `GET /docente/horario` → docente ve 2 sesiones (Jun-23 + Jun-24, ambas CERRADAS) | 200 | ✅ |
| 6 | `GET /asistencias?sesion_asistencia_id=3` → filtro correcto por sesión | 200 | ✅ |
| 7 | Sesiones anteriores intactas (regresión) | — | ✅ |

**Nota técnica:** `PATCH /sesiones/{id}/estado` recibe `estado` como **query param**, no como body JSON.  
Uso correcto: `PATCH /sesiones/3/estado?estado=CERRADA`

---

## Descubrimientos / Pendientes

### Constraint duplicado en `asignaciones_docente`

La tabla tiene el UNIQUE full `(docente_id, oferta_asignatura_id)` además del partial con `WHERE eliminado_en IS NULL`. El full constraint es redundante y conflictivo con el soft-delete. Se recomienda eliminarlo en una migración futura:

```sql
ALTER TABLE sgetecba.asignaciones_docente
DROP CONSTRAINT asignaciones_docente_docente_id_oferta_asignatura_id_key;
-- El índice parcial ux_asignaciones_docente_activo ya protege duplicados activos
```

### Estudiantes soft-deleted con asistencias históricas

`GET /asistencias` excluye estudiantes con `eliminado_en IS NOT NULL`. Comportamiento correcto para la vista operativa. Para un historial completo incluyendo bajas se necesitaría un parámetro adicional.

---

## Archivos modificados

| Archivo | Tipo | Cambio |
|---------|------|--------|
| `backend/app/api/v1/routes/matriculaciones.py` | Fix | UNION carrera_id fallback (bug #1) |
| `frontend/src/api/client.ts` | Fix + Feature | page_size 100, fetchHorarioEstudiante |
| `frontend/src/pages/AsignacionHorarios.tsx` | Fix | Promise.allSettled (bug #2) |
| `frontend/src/pages/PanelAlumnos.tsx` | Feature | Modal horario alumno |
| `backend/app/api/v1/routes/estudiantes.py` | Feature | GET /{id}/horario |
| `backend/app/crud/clases_planificadas_crud.py` | Fix | Auto-resolve asignacion_docente_id |
| `backend/app/crud/docentes_crud.py` | Fix | Reactivar asignacion soft-deleted |
| `backend/app/api/v1/routes/asistencias.py` | Feature | Filtro sesion_asistencia_id |

---

## Commits del día

| Hash | Descripción |
|------|-------------|
| `2e9d480` | fix: bug #1 carreras-por-estudiante UNION + bug #2 page_size/allSettled + rol MKT |
| `e182eda` | fix: auto-resolve asignacion_docente_id al crear horario + docs sesiones 22-23/06 |
| `315a250` | feat: vista horario del alumno en Panel Alumnos |
| `ff4fca2` | fix: reactivar asignacion_docente soft-deleted al re-asignar |
| `f9ff9ef` | feat: agregar filtro sesion_asistencia_id en GET /asistencias |

---

## Estado del sistema al cierre

| Módulo | Estado |
|--------|--------|
| Matriculaciones | ✅ Funcional — UNION fix |
| Asignación de Horarios | ✅ Funcional — Promise.allSettled + re-asignación |
| Panel Alumnos | ✅ Funcional + modal horario alumno nuevo |
| Horario docente | ✅ Funcional — auto-resolve asignacion_docente_id |
| Sesiones de asistencia | ✅ Flujo completo verificado end-to-end |
| Asistencias | ✅ Filtro por sesión agregado |
| Reportes gerenciales | ✅ Accesibles para rol MKT |

**Usuarios de prueba (contraseña universal: `Admin123*`):**

| Usuario | Rol |
|---------|-----|
| `admin` | ADMIN |
| `asistente_academico` | ASISTENTE_ACADEMICO |
| `docente` | DOCENTE — vinculado a `docentes.id=1` |
| `decano` | DIRECCION |
| `marketing` | MKT |
