# SGETECBA — Estado del proyecto al 10/06/2026

**Stack:** FastAPI + PostgreSQL 18 (schema: `sgetecba`) + React + TypeScript + Vite + TailwindCSS  
**Branch:** `features/MirkoALS`  
**Repo:** https://github.com/ZIS-TECBA/sgetecba  
**Desktop:** `C:\work mio\sgetecba`  
**Commit:** `git add -A && git commit -m "..." && git push upstream features/MirkoALS`

---

## Lo que se resolvió hoy (10/06/2026)

### Análisis de arquitectura — Lógica de negocio clarificada

Antes de codificar se hizo un análisis completo del schema real (`schema_dump.sql` + `data_only.sql`) para entender la lógica correcta del sistema.

**Conclusiones:**

| Entidad | Rol real |
|---|---|
| `asignaturas` | Catálogo global. Sin FK a carrera. La relación carrera↔asignatura es SOLO a través de `ofertas_asignaturas`. |
| `carreras` | Tiene `modalidad_academica_id` → define qué tipo de niveles aplican (semestres, años, módulos). |
| `niveles_academicos` | Tabla genérica con 31 filas mezcladas: semestres, años, módulos, trimestres, etapas. Debe filtrarse por modalidad. |
| `ofertas_asignaturas` | El nexo central: `asignatura + carrera + modalidad + período + nivel + turno + grupo + cupo`. Una materia puede tener múltiples ofertas en el mismo período si difieren en turno o grupo. |
| `turnos` | Solo 2 registros reales: Mañana y Tarde. |
| `periodos_academicos` | Ligados a modalidad. El flag `actual = true` indica el período activo por modalidad. |

**Flujo correcto de alta de materia:**
1. **Asignaturas** → crear la materia (código + nombre). Catálogo maestro.
2. **Carreras → Pensum → "Agregar materia"** → seleccionar materia, nivel (filtrado por modalidad de la carrera), turno. Crea la oferta.
3. **Ofertas** → vista operativa: ver inscritos, cambiar estado, editar turno/nivel/grupo. NO es donde se crean ofertas desde cero.

---

### Bugs corregidos

#### BUG CRÍTICO — `Asignaturas.tsx` completamente roto

**Archivo:** `frontend/src/pages/Asignaturas.tsx`

El archivo usaba `codigo_referencia` en todo el componente, pero:
- La BD tiene columna `codigo`
- El schema Pydantic tiene `AsignaturaRead.codigo`
- La interface TypeScript tiene `Asignatura.codigo`

El campo nunca se mostraba en la tabla, nunca se enviaba al crear/editar, y TypeScript debería haber fallado en compilación.

**Fix:** reescritura completa del archivo usando `codigo` en todos los lugares. Además se agregó la columna "Carreras" en la tabla (mostraba chips con las carreras donde está asignada cada materia — el campo `carreras: string[]` ya venía del backend pero nunca se renderizaba).

---

#### BUG — `CarreraCreate` y `CarreraUpdate` sin `modalidad_academica_id`

**Archivo:** `backend/app/schemas/common.py`

`CarreraCreate` no tenía el campo `modalidad_academica_id`. Consecuencia: las carreras creadas desde la UI quedaban con `modalidad_academica_id = NULL` silenciosamente, rompiendo todo lo que depende de la modalidad (filtro de niveles, búsqueda de período activo, etc.).

**Fix:** agregado `modalidad_academica_id: int | None = None` a `CarreraCreate` y `CarreraUpdate`.

---

#### BUG — `carreras.py` INSERT no guardaba modalidad

**Archivo:** `backend/app/api/v1/routes/carreras.py`

El INSERT de `crear_carrera` no incluía `modalidad_academica_id` en las columnas ni en los valores.

**Fix:**
- `modalidad_academica_id` agregado al INSERT
- `modalidad_academica_id` agregado a `_ALLOWED_UPDATE_FIELDS` para que también funcione en el PUT

---

#### BUG — `fetchCarrerasLista` devolvía `CatalogoItem[]` sin `modalidad_academica_id`

**Archivo:** `frontend/src/api/client.ts`

`fetchCarrerasLista` usaba `CatalogoItem[]` (solo `id` y `nombre`). `Carreras.tsx` necesita `modalidad_academica_id` de cada carrera para filtrar niveles y buscar el período activo. Al faltar ese campo, todo lo que dependía de la modalidad en el frontend usaba `undefined`.

**Fix:** `fetchCarrerasLista` ahora devuelve `Carrera[]` con todos los campos incluyendo `modalidad_academica_id`. Agregada también `fetchModalidadesLista()` nueva función.

---

#### BUG — `createCarrera`/`updateCarrera` en client.ts sin `modalidad_academica_id`

**Archivo:** `frontend/src/api/client.ts`

Los tipos de los payloads no incluían `modalidad_academica_id`, por lo que TypeScript lo rechazaba aunque se intentara enviar.

**Fix:** agregado `modalidad_academica_id?: number | null` a ambas funciones.

---

#### BUG — Niveles no filtrados por modalidad en el drawer de Carreras

**Archivo:** `frontend/src/pages/Carreras.tsx`

`abrirAddForm` llamaba `fetchNiveles()` sin parámetros, trayendo los 31 niveles mezclados.

**Fix:** `fetchNiveles(pensumCarrera.modalidad_academica_id)` — solo muestra los niveles usados en ofertas de esa modalidad.

---

#### BUG — Hardcode `modalidad_academica_id ?? 1` en `handleAddOferta`

**Archivo:** `frontend/src/pages/Carreras.tsx`

Si la carrera no tenía modalidad asignada, se forzaba `modalidad_academica_id = 1` (Semestral) silenciosamente.

**Fix:** si `pensumCarrera.modalidad_academica_id` es null, muestra error: "Esta carrera no tiene modalidad asignada. Editá la carrera primero."

---

#### BUG — `handleAddOferta` usaba `pensumOfertas[0]` para obtener el período

**Archivo:** `frontend/src/pages/Carreras.tsx`

Si el pensum estaba vacío, `pensumOfertas[0]?.periodo_academico_id` era `undefined`.

**Fix:** busca el período activo en la lista `periodos` filtrando por la modalidad de la carrera. Si tampoco encuentra uno, muestra error descriptivo.

---

#### BUG — `Carreras.tsx` no tenía campo Modalidad en el formulario

**Archivo:** `frontend/src/pages/Carreras.tsx`

El modal de crear/editar carrera no pedía la modalidad. El admin no podía asignarla desde la UI.

**Fix:** agregado select de Modalidad con nota: "Define qué niveles (semestres, módulos, años) estarán disponibles en el pensum." Opciones desde `fetchModalidadesLista()`.

---

#### BUG — `oferta_asignaturas.py` endpoint de niveles sin filtro por modalidad

**Archivo:** `backend/app/api/v1/routes/oferta_asignaturas.py`

`GET /catalogos/niveles` devolvía los 31 niveles sin filtrar.

**Fix:** parámetro opcional `modalidad_academica_id`: devuelve solo los niveles con al menos una oferta en esa modalidad (DISTINCT JOIN con `ofertas_asignaturas` y `periodos_academicos`).

---

#### BUG — `Ofertas.tsx` nivel null en creación + filtro no encadenado a carrera ✅

**Archivo:** `frontend/src/pages/Ofertas.tsx`

- El nivel en creación llegaba siempre como `null` (hardcodeado en `handleSave`).
- El filtro de niveles en la tabla no se recargaba al cambiar la carrera seleccionada.

**Fix:** campo Nivel agregado al formulario de creación, encadenado a la modalidad del período seleccionado. El select de nivel en filtros se recarga cuando cambia la carrera.

---

### Archivos entregados hoy

| Archivo | Destino |
|---|---|
| `Asignaturas.tsx` | `frontend/src/pages/Asignaturas.tsx` |
| `Carreras.tsx` | `frontend/src/pages/Carreras.tsx` |
| `Ofertas.tsx` | `frontend/src/pages/Ofertas.tsx` |
| `client.ts` | `frontend/src/api/client.ts` |
| `common.py` | `backend/app/schemas/common.py` |
| `carreras.py` | `backend/app/api/v1/routes/carreras.py` |
| `oferta_asignaturas.py` | `backend/app/api/v1/routes/oferta_asignaturas.py` |

---

## Pendientes — Alta prioridad (próxima sesión)

### 1. `Ofertas.tsx` — Nivel sigue sin filtrarse por modalidad de la carrera en nueva oferta

**Problema:** Al crear una nueva oferta, el select de Nivel muestra todos los tipos mezclados (semestres, años, módulos). Una carrera semestral puede recibir "Año 1" o "Módulo 3" sin validación.

**Qué hacer:**
- El select de Nivel debe filtrarse según la modalidad de la carrera seleccionada en el formulario.
- Llamar `fetchNiveles(modalidad_academica_id)` reactivamente cuando cambie la carrera en el form.
- Bloquear el submit si el nivel seleccionado no corresponde a la modalidad.

**Archivos:** `frontend/src/pages/Ofertas.tsx`, `frontend/src/api/client.ts`

---

### 2. `Estudiantes.tsx` — Formulario de alta largo, sin UX fluida ni validación

**Problema:** El formulario de crear estudiante es largo y confuso, sin validación visible ni navegación cómoda entre campos.

**Qué hacer:**
- Dividir el formulario en secciones visuales: Datos personales / Datos de contacto / Datos académicos.
- Agregar validación inline (cédula, email, teléfono) con feedback visible antes del submit.
- Navegación con `Enter` entre campos (ver pendiente global #4).
- Revisar si hay campos innecesarios que se puedan ocultar o inferir.

**Archivos:** `frontend/src/pages/Estudiantes.tsx`

---

### 3. `Matriculaciones.tsx` — Flujo lento y poco intuitivo

**Problema:** El flujo de matricular un estudiante tarda mucho o resulta confuso (pasos poco claros, selects no encadenados).

**Qué hacer:**
- Encadenar los selects: Carrera → Período → Nivel → Oferta (cada uno filtra al siguiente).
- Feedback inmediato al seleccionar estudiante: mostrar en qué carreras/materias ya está inscripto.
- Confirmación antes de guardar con resumen de lo que se va a matricular.
- Navegación con `Enter` entre campos.

**Archivos:** `frontend/src/pages/Matriculaciones.tsx`, `frontend/src/api/client.ts`

---

### 4. Global — Navegación con `Enter` entre campos en todos los formularios

**Problema:** En ningún formulario del sistema el `Enter` mueve el foco al campo siguiente. El admin tiene que usar el mouse o `Tab` manualmente.

**Qué hacer:**
- Agregar `onKeyDown` handler en cada `<input>` y `<select>` para capturar `Enter` y hacer `focus()` en el siguiente campo del formulario.
- Alternativamente, implementar un hook reutilizable `useEnterNavigation(refs[])` y aplicarlo en todos los formularios.
- El último campo del formulario con `Enter` debe disparar el submit (si el form es válido).
- Aplicar en: `Ofertas.tsx`, `Estudiantes.tsx`, `Matriculaciones.tsx`, `Carreras.tsx`, `Asignaturas.tsx` y cualquier otro formulario del sistema.

---

## Sprint 2 (backlog)

- **Docente → Horarios:** UI para que el admin cree slots (día + hora_inicio + hora_fin) por oferta. El endpoint `horario_clases.py` ya existe. Falta la vista del docente.
- **Normalizar respuestas API:** varios endpoints devuelven `200 { ok: false, error: "..." }`. Migrar a 4xx.
- **Vista Pensum por Carrera:** tabla que muestre qué materias YA tienen oferta activa vs. cuáles faltan abrir.
- **PDF malla curricular:** quitar columna Turno del endpoint `exportar_malla_pdf` en `reportes.py` (pendiente desde sesión anterior, aún no aplicado).

---

## Notas de arquitectura para recordar

- **No existe tabla `pensum` ni `malla`** — la relación asignatura↔carrera solo existe a través de `ofertas_asignaturas`. El "pensum" es inferido.
- **`niveles_academicos` es genérico** — siempre filtrar por `modalidad_academica_id` antes de mostrar al usuario.
- **Unicidad de oferta:** `(asignatura_id, carrera_id, modalidad_academica_id, periodo_academico_id, nivel_academico_id, turno_id, grupo_id)` con COALESCE a 0 para los nullables. Una materia puede tener múltiples ofertas si difieren en turno o grupo.
- **Rebuild BD:** `DROP SCHEMA CASCADE` → `schema_only.sql` → `data_only.sql`. El UPDATE de hashes bcrypt debe ejecutarse en psql interactivo (no PowerShell) para evitar corrupción de `$`.
- **Seed users:** password `Demo123*` para todos.
- **Error business:** convención actual es `HTTP 200 { ok: false, error: "..." }` — pendiente migrar a 4xx en Sprint 2.
