# 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.

| 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.

---

## Estado de bugs del QA (qa_tecba_10-06-2026)

| Bug | Descripción | Estado |
|---|---|---|
| BUG-01 | CORS bloqueaba el sitio | ✅ Resuelto |
| BUG-02 | Asignaturas sin link en Sidebar | ✅ Resuelto |
| BUG-03 | "Matriculados" no filtra por carrera (`useSearchParams`) | ✅ Resuelto |
| BUG-04 | 4 llamadas idénticas en Asistencias | ✅ Resuelto |
| **BUG-05** | **Docentes: auto-asignación a ofertas** | ❌ **Pendiente** |
| BUG-06 | Géneros hardcodeados en Estudiantes | ✅ Resuelto |
| BUG-07 | Eliminación silenciosa sin feedback | ✅ Resuelto |
| **BUG-08** | **Paginación Asistencias con closure stale** | ❌ **Pendiente** |
| **BUG-09** | **Doble fetch estados en Estudiantes** | ❌ **Pendiente** |
| BUG-10 | Textos sin acentos | ✅ Resuelto |
| **BUG-11** | **React Router warnings futuros** | ❌ **Pendiente** |
| BUG-12 | Nivel null en creación de oferta | ✅ Resuelto |

**Resumen:** 8 resueltos · 4 pendientes (BUG-05, BUG-08, BUG-09, BUG-11)

---

## Bugs corregidos en detalle

### BUG-03 — `useSearchParams` en Matriculaciones
**Archivo:** `frontend/src/pages/Matriculaciones.tsx`
- Agregado `import { useSearchParams } from "react-router-dom"`
- Agregado `const [searchParams] = useSearchParams()`
- `carreraFiltro` se inicializa leyendo `carrera_id` de la URL al montar

### BUG-06 — Géneros dinámicos en Estudiantes
**Archivo:** `frontend/src/pages/Estudiantes.tsx`
- Eliminado array estático `GENEROS` (líneas 18-23)
- Agregado `fetchGeneros()` en `client.ts` apuntando a `GET /estudiantes/catalogos/generos`
- `useEffect` del mount carga géneros dinámicamente

### BUG-07 — Feedback de eliminación
**Archivos:** `Estudiantes.tsx`, `Asignaturas.tsx`
- En `handleDelete`, el `catch` ahora llama `setError(msg)` con el mensaje del backend en lugar de solo cerrar el modal

### BUG-10 — Acentos corregidos
**Archivos:** `Estudiantes.tsx`, `Asignaturas.tsx`, `PaginationControls`
- Padrón académico, Género, Teléfono, Cédula de identidad, Observación, Catálogo, Código, Página

### BUG-12 — Nivel null en creación de oferta
**Archivo:** `frontend/src/pages/Ofertas.tsx`
- Campo Nivel agregado al formulario de creación
- Encadenado a la modalidad del período seleccionado
- Filtro de niveles en tabla se recarga al cambiar carrera

### Arquitectura — `modalidad_academica_id` integrado
**Archivos:** `common.py`, `carreras.py`, `client.ts`, `Carreras.tsx`, `oferta_asignaturas.py`, `Asignaturas.tsx`
- `CarreraCreate`/`CarreraUpdate` incluyen `modalidad_academica_id`
- INSERT en `carreras.py` guarda la modalidad
- `fetchCarrerasLista` devuelve `Carrera[]` completa (no `CatalogoItem[]`)
- `fetchNiveles(modalidad_academica_id)` filtra los 31 niveles por modalidad
- `Asignaturas.tsx` reescrito: `codigo_referencia` → `codigo`, columna "Carreras" agregada
- Formulario de carrera incluye select de Modalidad
- `handleAddOferta` busca período activo por modalidad (no más `pensumOfertas[0]`)

---

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

### 1. BUG-05 — Docente se auto-asigna a ofertas (CRÍTICO)
**Lógica confirmada:**
- Admin crea períodos y ofertas
- El docente entra a su vista y **se auto-asigna** a las ofertas disponibles
- La asignación va en `ofertas_asignaturas.docente_id`

**Qué revisar antes de codificar:**
- `AppRouter.tsx` — ver rutas del docente registradas
- `HorarioDocente.tsx` — ya existe, ver qué hace actualmente
- `backend/app/api/v1/routes/oferta_asignaturas.py` — ver si hay endpoint de auto-asignación
- Si no hay endpoint: crear `PATCH /ofertas-asignaturas/{id}/asignar-docente` que tome el `docente_id` del token JWT

### 2. BUG-08 — Paginación Asistencias con closure stale
**Archivo:** `frontend/src/pages/Asistencias.tsx`
- Reemplazar `onPrev={() => load(page - 1)}` por `setPage(c => Math.max(c - 1, 1))`
- Agregar `useEffect([page])` que llame `load(page)`

### 3. BUG-09 — Doble fetch en Estudiantes
**Archivo:** `frontend/src/pages/Estudiantes.tsx`
- Consolidar los `useEffect` de mount para que `load(1)` se dispare una sola vez

### 4. BUG-11 — React Router warnings
**Archivo:** `frontend/src/main.tsx`
```tsx
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
```

### 5. `Ofertas.tsx` — Nivel sin filtrar por modalidad en nueva oferta
- El select de Nivel debe filtrarse según la modalidad de la carrera seleccionada
- Llamar `fetchNiveles(modalidad_academica_id)` reactivamente cuando cambie la carrera
- Bloquear submit si el nivel no corresponde a la modalidad

### 6. `Estudiantes.tsx` — Formulario de alta sin UX fluida
- Dividir en secciones: Datos personales / Contacto / Académicos
- Validación inline (cédula, email, teléfono)
- Navegación con `Enter` entre campos

### 7. `Matriculaciones.tsx` — Flujo lento
- Encadenar selects: Carrera → Período → Nivel → Oferta
- Feedback inmediato al seleccionar estudiante
- Confirmación antes de guardar con resumen

### 8. Global — Navegación con `Enter` entre campos
- Implementar hook `useEnterNavigation(refs[])` reutilizable
- Aplicar en todos los formularios

---

## 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`.

---

## 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` |
| `Estudiantes.tsx` | `frontend/src/pages/Estudiantes.tsx` |
| `Matriculaciones.tsx` | `frontend/src/pages/Matriculaciones.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` |

---

## 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.
- **Flujo de horarios:** Admin crea slots disponibles por oferta → docente se auto-asigna y gestiona su horario dentro de esos bloques.