# SGETECBA — Sesión de trabajo 11/06/2026

**Stack:** FastAPI + PostgreSQL 18 (schema: `sgetecba`) + React + TypeScript + Vite + TailwindCSS  
**Branch:** `features/MirkoALS`  
**Repo:** https://github.com/ZIS-TECBA/sgetecba

---

## Lo que se hizo en esta sesión

### 1. Ofertas — Curso libre: fix de mensaje de error

**Contexto:** Al intentar crear una oferta en modo "Curso libre" con un período sin modalidad asignada (ej. "Cursos Libres 2026"), el formulario bloqueaba con el mensaje incorrecto "Período y asignatura son obligatorios." ocultando que el campo faltante era la **Modalidad**.

**Fix 1 — Mensaje corregido** `frontend/src/pages/Ofertas.tsx` línea ~225  
- Antes: `"Período y asignatura son obligatorios."`  
- Después: mensajes específicos por campo faltante:
  - `"Selecciona un período."`
  - `"Selecciona una carrera."`
  - `"Selecciona la modalidad para este período."` ← el real culpable
  - `"Selecciona una asignatura."`

---

### 2. `modalidad_academica_id` nullable para cursos libres

**Contexto:** La tabla `ofertas_asignaturas` tenía `modalidad_academica_id NOT NULL`. Pero un "curso libre" (sin carrera) tampoco tiene modalidad de calendario — forzar elegirla no tiene sentido semántico.

**Decisión:** `modalidad_academica_id` es `NULL` cuando `carrera_id IS NULL` (curso libre).

#### Cambios realizados

| Capa | Archivo | Cambio |
|------|---------|--------|
| **DB** | PostgreSQL en vivo + `BD/pensum_tecba.sql` | `ALTER COLUMN modalidad_academica_id DROP NOT NULL` + índice único reconstruido con `COALESCE(..., 0)` |
| **Backend schema** | `backend/app/schemas/common.py` | `OfertaAsignaturaCreate` y `OfertaAsignaturaRead`: `modalidad_academica_id: int \| None = None`, `modalidad_nombre: str \| None = None` |
| **Backend route** | `backend/app/api/v1/routes/oferta_asignaturas.py` | `JOIN modalidades_academicas` → `LEFT JOIN`; chequeo de duplicado: `= :modalidad` → `IS NOT DISTINCT FROM :modalidad` |
| **Frontend types** | `frontend/src/types/api.ts` | `OfertaAsignatura.modalidad_academica_id: number \| null`, `modalidad_nombre: string \| null` |
| **Frontend API** | `frontend/src/api/client.ts` | `createOferta` parámetro: `modalidad_academica_id: number \| null` |
| **Frontend UI** | `frontend/src/pages/Ofertas.tsx` | Cuando `cursoLibre=true`: oculta dropdown de modalidad, omite validación, pasa `null` al backend |

**Flujo resultante para curso libre:**  
☑ Curso libre → Período → Asignatura → Guardar *(sin tocar Modalidad)*

**Nota de integridad:** el índice único `ux_ofertas_asignaturas_unica` fue recreado usando `COALESCE(modalidad_academica_id::int, 0)` para que dos ofertas sin modalidad del mismo período/asignatura sigan siendo consideradas duplicadas.

---

### 3. Usuarios — Página CRUD completa (nueva)

Se construyó la página de gestión de usuarios del sistema, que estaba completamente ausente del frontend.

**Archivos creados / modificados:**

| Archivo | Cambio |
|---------|--------|
| `frontend/src/pages/Usuarios.tsx` | Nueva página — CRUD completo |
| `frontend/src/routes/AppRouter.tsx` | Ruta `/admin/usuarios` registrada |
| `frontend/src/components/Sidebar.tsx` | Link "Usuarios" en sidebar, solo rol ADMIN |
| `frontend/src/api/client.ts` | Funciones: `fetchUsuarios`, `fetchRoles`, `createUsuario`, `updateUsuario`, `toggleUsuarioActivo`, `cambiarPasswordUsuario`, `deleteUsuario` |
| `backend/app/api/v1/routes/usuarios.py` | Endpoint `GET /usuarios/catalogos/roles` agregado |
| `frontend/src/types/api.ts` | Tipos `Usuario` y `Rol` |

**Funcionalidades incluidas:**

- Listado paginado con búsqueda por nombre/usuario
- Filtro por rol y estado (activo/inactivo)
- Crear usuario (nombre, username, contraseña inicial, rol)
- Editar datos (nombre, correo, rol)
- Cambiar contraseña por el admin
- Activar / desactivar acceso (soft toggle)
- Eliminar usuario (soft-delete)
- Solo visible para rol ADMIN

---

### 4. Asignaturas — Filtro "Sin carrera" no mostraba cursos libres

**Causa:** el filtro `sin_carrera` usaba `NOT EXISTS` sobre toda la tabla `ofertas_asignaturas`. Una asignatura ofrecida como curso libre tiene un registro en esa tabla con `carrera_id = NULL`, lo que la excluía del filtro aunque no tuviera carrera real.

**Fix** `backend/app/api/v1/routes/asignaturas.py`  
```sql
-- Antes
NOT EXISTS (SELECT 1 FROM ofertas_asignaturas oa2
  WHERE oa2.asignatura_id = a.id AND oa2.eliminado_en IS NULL)

-- Después
NOT EXISTS (SELECT 1 FROM ofertas_asignaturas oa2
  WHERE oa2.asignatura_id = a.id
    AND oa2.carrera_id IS NOT NULL        -- ← solo excluye si tiene carrera real
    AND oa2.eliminado_en IS NULL)
```

Ahora el filtro muestra asignaturas sin ninguna oferta + asignaturas que solo tienen ofertas de curso libre.

---

## Bugs pendientes (de la imagen de Pendientes)

| Bug | Descripción | Severidad | Estado |
|-----|-------------|-----------|--------|
| BUG-06 | `createOferta` siempre pasa `nivel_academico_id: null` — no hay selector en el form de creación | ALTA | ⏳ Pendiente |
| BUG-07 | `handleDelete` en Matriculaciones no tiene `catch` — errores silenciados | MEDIA | ⏳ Pendiente |
| BUG-09 | Doble `useEffect` en Asistencias → 4 requests HTTP al montar | ALTA | ⏳ Pendiente |
| BUG-10 | Paginación Asistencias usa closure con `page` potencialmente stale | MEDIA | ⏳ Pendiente |
| BUG-12 | Nombre duplicado en accessibility tree de tabla Estudiantes | BAJA | ⏳ Pendiente |

---

## Tareas pendientes del documento completo (Junio 2026)

### Sprint 1 — Bloqueos críticos

- [ ] **BUG CRÍTICO** `reportes.py`: `current_user["id"]` → `current_user["user_id"]` en todos los endpoints de reportes del docente
- [ ] **SEGURIDAD**: Eliminar `contraseña.txt` del repo e historial Git (BFG o filter-branch)
- [ ] **SEGURIDAD**: Eliminar `backend/_temp_*.py` (8 archivos) del repositorio
- [ ] **SEGURIDAD**: Agregar `.env` y `frontend/node_modules/` a `.gitignore`; crear `.env.example`
- [ ] **SEGURIDAD**: Rotar contraseña de BD, `JWT_SECRET` y cualquier API key expuesta
- [ ] **BUG** Matriculaciones: `fecha_inscripcion` sin valor por defecto → inicializar con hoy
- [ ] **BUG** Matriculaciones: estado sin pre-selección → pre-seleccionar ACTIVA
- [ ] **BUG** Matriculaciones: combobox de oferta sin búsqueda → implementar con debounce
- [ ] **BUG** Matriculaciones: Enter no avanza al siguiente campo (aplica a todos los forms)
- [ ] **NUEVO** Matriculaciones: pre-carga automática de materias de la carrera al matricular

### Sprint 2 — Funcionalidad core

- [ ] **NUEVO** Importación Excel de estudiantes — flujo completo con pipeline de staging
- [ ] **NUEVO** Asignación de docente a oferta con `fecha_inicio` / `fecha_fin`
- [ ] **NUEVO** Vista Pensum por Carrera en sidebar admin (derivada de `ofertas_asignaturas`)
- [ ] **BUG** Normalizar respuestas API: reemplazar `200 { ok: false }` por `400/404/409/422`
- [ ] **NUEVO** Editar materias de estudiante ya inscrito (agregar/dar de baja inscripciones)
- [ ] **BUG** BUG-06: agregar selector de nivel en form de creación de oferta

### Sprint 3 — Panel docente completo

- [ ] Mis Asignaturas (lista de ofertas asignadas al docente logueado)
- [ ] Pasar Lista: seleccionar sesión del día → marcar asistencia → cerrar sesión
- [ ] Horario: verificar que `HorarioDocente.tsx` consume `horario_clases` correctamente
- [ ] Clases Planificadas: registrar tema y estado por sesión
- [ ] Importación historial de asistencias desde Excel

### Sprint 4 — Cierre

- [ ] Usuarios CRUD (página frontend faltante)
- [ ] Faltas y Abandonos (páginas Secretaría)
- [ ] Reportes PDF consolidados para Dirección (después del fix `user_id`)
- [ ] Responsividad global: grids de filtros y tablas en móvil/tablet
- [ ] Dashboard: bugs pendientes
- [ ] Verificar UNIQUE constraint en `asignaciones_docente`
- [ ] Refactors: `AsistenciasSecretaria.tsx`, `importacion_service.py`, `reportes.py`, `client.ts`
- [ ] Tests mínimos: auth por roles, importación con duplicados, asistencia duplicada → 409

---

## Referencia rápida — archivos tocados esta sesión

```
backend/
  app/schemas/common.py                    OfertaAsignaturaCreate/Read nullable modalidad
  app/api/v1/routes/oferta_asignaturas.py  LEFT JOIN + IS NOT DISTINCT FROM
  app/api/v1/routes/usuarios.py            GET /catalogos/roles

frontend/src/
  types/api.ts                             OfertaAsignatura: modalidad nullable; tipos Usuario, Rol
  api/client.ts                            createOferta nullable + helpers Usuarios
  pages/Ofertas.tsx                        mensajes específicos + cursoLibre sin modalidad
  pages/Usuarios.tsx                       NUEVA — CRUD completo de usuarios
  components/Sidebar.tsx                   Link Usuarios (solo ADMIN)
  routes/AppRouter.tsx                     Ruta /admin/usuarios
  components/Toast.tsx                     NUEVO — componente toast
  hooks/useToast.ts                        NUEVO — hook toast

BD/
  pensum_tecba.sql                         DROP NOT NULL + UNIQUE INDEX con COALESCE

docs/MD Desarrollo/
  SGETECBA_Estado_11062026.md              NUEVO — este archivo
```

---

*SGETECBA — Documento interno de desarrollo — 11/06/2026 — Mirko*
