# QA Report — Sistema SGE TECBA

**Fecha:** 10/06/2026  
**Tester:** QA automatizado (devtools)  
**Credenciales usadas:** `admin` / `Admin123*`  
**Entorno:** Backend `localhost:8000` · Frontend `localhost:5173` · DB PostgreSQL `localhost:5432`  
**Rama:** `feature/santi`

---

## Resumen ejecutivo

| Severidad | Cantidad |
|---|---|
| 🔴 Crítico | 5 |
| 🟠 Medio | 4 |
| 🟡 Menor | 3 |
| ✅ Sin errores | 8 módulos |

---

## 🔴 CRÍTICOS — Funcionalidad rota

### BUG-01 · CORS bloqueaba todo el sitio

**Archivo:** `backend/.env` → `CORS_ORIGINS`  
**Observado en red:**
```
OPTIONS /api/v1/auth/login → 400 Bad Request
POST    /api/v1/auth/login → net::ERR_FAILED
```
**Causa:** El backend solo permitía origins `localhost:5173` y `localhost:4173`. Cualquier puerto diferente (staging, preview, CI) queda bloqueado.  
**Fix aplicado:** Se agregaron `localhost:5174` y `localhost:50901` a `CORS_ORIGINS` en `.env`. Para producción, definir la variable explícitamente por entorno.

---

### BUG-02 · Módulo "Asignaturas" inaccesible — sin link en sidebar

**Archivo:** `frontend/src/components/Sidebar.tsx` línea 18–26  
**Observado:** La ruta `/asignaturas` y la página `AsignaturasPage` existen y funcionan correctamente, pero el array `allNavItems` no incluye ese módulo. El ADMIN solo puede acceder escribiendo la URL manualmente.  
**Impacto:** Toda la gestión de asignaturas (alta, edición, eliminación) es inaccesible desde el UI para todos los roles.

```tsx
// FALTA este entry en allNavItems de Sidebar.tsx:
{ label: "Asignaturas", path: "/asignaturas", icon: BookOpen, roles: [ROLE_ADMIN, ROLE_DIRECCION] },
```

---

### BUG-03 · Botón "Matriculados" en Carreras no aplica el filtro de carrera

**Archivos:**
- `frontend/src/pages/Carreras.tsx` línea 187
- `frontend/src/pages/Matriculaciones.tsx`

**Observado:** El botón navega a `/admin/matriculaciones?carrera_id=${item.id}`, pero `MatriculacionesPage` nunca lee `useSearchParams()`. El parámetro se descarta y la tabla carga todas las matriculaciones sin filtrar.  
**Fix:** Agregar `useSearchParams` en `MatriculacionesPage` para inicializar `carreraFiltro` con el `carrera_id` de la URL al montar el componente.

---

### BUG-04 · Asistencias — 4 llamadas API idénticas en cada carga inicial

**Archivo:** `frontend/src/pages/Asistencias.tsx` líneas 222–267  
**Observado en red:** Al navegar a `/admin/asistencias` se disparan simultáneamente **4 GET** a `/api/v1/asistencias?page=1&page_size=20`.

```
9868.534  GET /api/v1/asistencias?page=1&page_size=20 → 200
9868.537  GET /api/v1/asistencias?page=1&page_size=20 → 200
9868.538  GET /api/v1/asistencias?page=1&page_size=20 → 200
9868.541  GET /api/v1/asistencias?page=1&page_size=20 → 200
```

**Causa:** Dos `useEffect` independientes llaman a `load(1)` al montar, multiplicados por React 18 StrictMode:

```tsx
// Efecto 1 — mount inicial
useEffect(() => { load(1); }, []);

// Efecto 2 — también dispara en mount porque estudianteQDebounced inicia vacío
useEffect(() => { load(1); }, [estudianteQDebounced]);
```

**Fix:** Consolidar ambos efectos o agregar un flag de mounted para evitar la carga duplicada.

---

### BUG-05 · Gestión de Docentes completamente ausente en UI

**Archivo:** `frontend/src/routes/AppRouter.tsx`  
**Observado:** `api/client.ts` tiene CRUD completo de docentes (`fetchDocentes`, `createDocente`, `updateDocente`, `deleteDocente`, `resetPasswordDocente`, `cambiarUsuarioDocente`), pero no existe ninguna página ni ruta registrada en el router. El administrador no puede gestionar docentes desde el sistema.

---

## 🟠 MEDIOS — Comportamiento incorrecto

### BUG-06 · Géneros hardcodeados en el cliente

**Archivo:** `frontend/src/pages/Estudiantes.tsx` líneas 18–23

```tsx
const GENEROS = [
  { id: 1, nombre: "Masculino" },
  { id: 2, nombre: "Femenino" },
  { id: 3, nombre: "Otro" },
  { id: 4, nombre: "Sin dato" },
];
```

Si los IDs en la base de datos difieren de 1–4 (por re-seed, truncate, o migración), el campo se enviará con el ID incorrecto silenciosamente. Debe obtenerse dinámicamente desde la API.

---

### BUG-07 · Eliminación silenciosa — usuario no recibe feedback en caso de error

**Archivos:**
- `frontend/src/pages/Estudiantes.tsx` líneas 157–168
- `frontend/src/pages/Asignaturas.tsx` líneas 104–116

```tsx
} catch {
  setConfirmDelete(null); // error ignorado — el modal se cierra sin aviso
}
```

Si la eliminación falla (constraint FK, error de red, permiso), el modal se cierra sin mostrar ningún mensaje. El usuario asume que se eliminó correctamente.  
**Fix:** Mostrar `setError(...)` dentro del catch con el mensaje del backend.

---

### BUG-08 · Paginación de Asistencias puede perder la página actual

**Archivo:** `frontend/src/pages/Asistencias.tsx` líneas 611–613

```tsx
onPrev={() => load(page - 1)}
onNext={() => load(page + 1)}
```

`page` es leído desde el closure en el momento del render. Si el usuario hace doble click rápido, el segundo click usa el valor de `page` anterior al primer `setPage`. Los demás módulos usan correctamente `setPage(c => c + 1)` + `useEffect([page])`.

---

### BUG-09 · Estudiantes realiza doble fetch de estados en cada mount

**Observado en red:** Al navegar a `/admin/estudiantes`:

```
GET /api/v1/estudiantes/estados → 200  (×2)
GET /api/v1/estudiantes?page=1&page_size=10 → 200  (×2)
```

Mismo patrón de doble efecto combinado con React 18 StrictMode. Impacto menor en performance pero genera carga innecesaria al servidor.

---

## 🟡 MENORES — UX / Consistencia

### BUG-10 · Textos sin acentos en múltiples módulos

| Archivo | Línea | Texto actual | Texto correcto |
|---|---|---|---|
| `Estudiantes.tsx` | 175 | "Padron academico" | "Padrón académico" |
| `Estudiantes.tsx` | 204 | "Genero", "Telefono" | "Género", "Teléfono" |
| `Estudiantes.tsx` | 305 | "Cedula de identidad" | "Cédula de identidad" |
| `Estudiantes.tsx` | 365 | "Observacion" | "Observación" |
| `Asignaturas.tsx` | 122 | "Catalogo normalizado" | "Catálogo normalizado" |
| `Asignaturas.tsx` | 149, 209 | "Codigo" | "Código" |
| `PaginationControls` | — | "Pagina" | "Página" |

---

### BUG-11 · React Router v6 — warnings de flags futuros en consola

**Observado en consola (4 warnings al cargar):**
```
⚠️ React Router Future Flag Warning: v7_startTransition
⚠️ React Router Future Flag Warning: v7_relativeSplatPath
```

**Fix:** Agregar `future` flags al `<BrowserRouter>` en `main.tsx`:

```tsx
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
```

---

### BUG-12 · Creación de ofertas no permite asignar nivel académico

**Archivo:** `frontend/src/pages/Ofertas.tsx` línea 202

```tsx
await createOferta({ ..., nivel_academico_id: null });
```

Al crear una nueva oferta, el nivel siempre se envía como `null`. El campo solo puede editarse después de creada la oferta (en modo edición). El formulario de creación debería incluir el selector de nivel.

---

## ✅ Módulos verificados sin errores

| Módulo | Ruta | Resultado |
|---|---|---|
| Login | `/login` | ✅ Autenticación funciona, error visible con credenciales incorrectas |
| Dashboard | `/dashboard` | ✅ KPIs y gráficos cargan correctamente con datos reales |
| Carreras | `/carreras` | ✅ CRUD completo funciona, pensum drawer opera correctamente |
| Períodos | `/admin/periodos` | ✅ CRUD funciona, toggle activo/inactivo sincroniza con BD |
| Ofertas | `/admin/ofertas` | ✅ Carga, filtros por carrera/período, combobox de asignatura funcionan |
| Matriculaciones | `/admin/matriculaciones` | ✅ Tabla con filtros, flujo de 2 pasos, matriculación masiva operativa |
| Asistencias | `/admin/asistencias` | ✅ Carga, filtros encadenados, modal de edición con motivo obligatorio |
| Asignaturas | `/asignaturas` | ✅ Funciona correctamente — solo inaccesible desde sidebar (ver BUG-02) |

---

## Hallazgos de infraestructura

| Item | Detalle |
|---|---|
| PostgreSQL | Servicio Windows `postgresql-x64-18` — requiere inicio manual con privilegios de administrador |
| CORS `.env` | Actualizado en esta sesión — agregar `localhost:5174` para el servidor de desarrollo |
| Realtime SSE | `GET /api/v1/realtime/stream` → conexión abierta, se aborta correctamente al navegar |
| Token JWT | Expiración: 120 minutos. Sin renovación automática — el usuario es deslogueado sin aviso |

---

*Generado automáticamente — Sesión del 10/06/2026*
