# SIAE — Sistema Integral de Asistencia Escolar
## Arquitectura Completa del Sistema

> **Base:** Proyecto SIGAE existente en Laravel 12, Spatie Permissions, MySQL, autenticación por username/email.  
> **Stack:** Laravel 12 (API) + React 18 + TypeScript + Vite + Tailwind CSS + MySQL

---

## 1. Propuesta de Arquitectura

```
┌─────────────────────────────────────────┐
│          CLIENTE (React + Vite)         │
│  Tailwind CSS · React Query · Zustand   │
└──────────────┬──────────────────────────┘
               │ HTTPS / JSON API
┌──────────────▼──────────────────────────┐
│     BACKEND Laravel 12 (API REST)       │
│  Controllers · Services · Actions       │
│  Policies · Events · Jobs · Queues      │
└──────┬───────────────────────┬──────────┘
       │                       │
┌──────▼──────┐     ┌──────────▼──────────┐
│   MySQL DB  │     │  Microservicio Facial│
│  (principal)│     │  (Python/local, opt.)│
└─────────────┘     └─────────────────────┘
```

**Decisiones clave:**
- **API-first:** Laravel expone solo JSON API. El frontend React consume la API completamente.
- **Modular por dominio:** cada módulo tiene su carpeta en `app/Modules/`.
- **Colas (Queue):** todas las notificaciones van por jobs para no bloquear el request.
- **Reconocimiento facial:** endpoints preparados con contrato definido, microservicio local desacoplado.
- **Modo Kiosco:** usuario tipo `kiosco` con permisos mínimos, vista React fullscreen sin sidebar.

---

## 2. Estructura de Carpetas

### Backend Laravel

```
app/
├── Http/
│   ├── Controllers/Api/
│   │   ├── Auth/
│   │   ├── Configuracion/
│   │   ├── Personas/
│   │   ├── Matricula/
│   │   ├── Horarios/
│   │   ├── Acceso/
│   │   ├── Asistencia/
│   │   ├── Visitantes/
│   │   ├── Biometria/
│   │   ├── Notificaciones/
│   │   ├── Reportes/
│   │   └── Auditoria/
│   ├── Requests/          # Form Requests por módulo
│   ├── Resources/         # API Resources (transformadores)
│   └── Middleware/
│       ├── CheckRole.php
│       └── KioscoOnly.php
├── Models/
├── Services/              # Lógica de dominio
│   ├── MatriculaService.php
│   ├── AsistenciaService.php
│   ├── AccesoService.php
│   └── NotificacionService.php
├── Actions/               # Acciones atómicas reutilizables
├── Events/
├── Listeners/
├── Jobs/
│   ├── EnviarWhatsapp.php
│   ├── EnviarEmail.php
│   └── EnviarNotificacionPanel.php
├── Policies/
└── Observers/

database/
├── migrations/
├── seeders/
└── factories/

routes/
├── api.php                # Todas las rutas API
└── web.php                # Solo landing y auth web
```

### Frontend React

```
resources/js/   (o carpeta frontend/ separada)
├── src/
│   ├── api/               # Axios instances y endpoints
│   ├── components/
│   │   ├── ui/            # Botones, inputs, badges, modales
│   │   ├── layout/        # Sidebar, Header, Layout
│   │   └── shared/        # Tablas, filtros, paginación
│   ├── features/
│   │   ├── auth/
│   │   ├── configuracion/
│   │   ├── personas/
│   │   ├── matricula/
│   │   ├── horarios/
│   │   ├── acceso/
│   │   ├── asistencia/
│   │   ├── visitantes/
│   │   ├── biometria/
│   │   ├── notificaciones/
│   │   ├── reportes/
│   │   ├── auditoria/
│   │   └── kiosco/
│   ├── hooks/             # Custom hooks
│   ├── stores/            # Zustand stores
│   ├── types/             # TypeScript interfaces
│   └── utils/
```

---

## 3. Modelo Relacional Resumido

```
instituciones ──< periodos_academicos
periodos_academicos ──< matriculas
periodos_academicos ──< horario_detalles

niveles ──< grados ──< secciones ──< aulas

estudiantes ──< matriculas
estudiantes >──< apoderados  [via estudiante_apoderado]
estudiantes ──< asistencia_estudiantes_aula
estudiantes ──< asistencias_estudiantes_institucion
estudiantes ──< codigos_qr
estudiantes ──< biometria_facial

docentes ──< horario_detalles
docentes ──< sesiones_aprendizaje
docentes ──< asistencias_docentes_institucion
docentes >──< areas  [via docente_area]

administrativos ──< marcaciones_administrativos

usuarios ──< [ estudiantes | docentes | administrativos | apoderados ]

horarios ──< horario_detalles
horario_detalles ──< sesiones_aprendizaje
sesiones_aprendizaje ──< asistencia_estudiantes_aula

visitantes ──< visitas

notificaciones ──< log_notificaciones
auditoria_cambios (log global)
incidencias
dispositivos_kiosco
```

---

## 4. Migraciones — Campos por Tabla

### `instituciones`
| Campo | Tipo | Notas |
|---|---|---|
| id | bigIncrements | PK |
| nombre | string | |
| ugel | string nullable | |
| dre | string nullable | |
| codigo_modular | string nullable | |
| ruc | string nullable | |
| direccion | string nullable | |
| logo | string nullable | |
| telefono | string nullable | |
| email | string nullable | |
| director | string nullable | |
| timestamps | | |

### `periodos_academicos`
| Campo | Tipo | Notas |
|---|---|---|
| id | bigIncrements | |
| institucion_id | FK | |
| nombre | string | Ej: "2025" |
| fecha_inicio | date | |
| fecha_fin | date | |
| activo | boolean default true | solo 1 activo |
| timestamps | | |

### `niveles`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombre, orden, activo | | Ej: Primaria, Secundaria |

### `grados`
| Campo | Tipo | Notas |
|---|---|---|
| id, nivel_id(FK), nombre, orden, activo | | |

### `secciones`
| Campo | Tipo | Notas |
|---|---|---|
| id, grado_id(FK), nombre, turno_id(FK), activo | | |

### `aulas`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombre, capacidad, piso, activo | | |

### `turnos`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombre, hora_inicio, hora_fin, activo | | Mañana / Tarde |

### `areas`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombre, codigo, nivel_id(FK nullable), activo | | |

### `usuarios` (ya existe, ampliar)
| Campo | Tipo | Notas |
|---|---|---|
| + persona_type | string nullable | Polymorphic |
| + persona_id | unsignedBigInt nullable | |
| + activo | boolean default true | |

### `estudiantes` (ya existe, refactorizar)
Separar datos académicos a `matriculas`. Agregar:
| Campo | Tipo | Notas |
|---|---|---|
| + fecha_nacimiento | date nullable | |
| + lugar_nacimiento | string nullable | |
| + foto | string nullable | |
| + soft_delete | | |

### `apoderados`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombres, apellido_paterno, apellido_materno, dni(unique), sexo | | |
| celular, whatsapp, correo, foto, estado | | |
| user_id(FK nullable), timestamps, softDeletes | | |

### `estudiante_apoderado`
| Campo | Tipo | Notas |
|---|---|---|
| estudiante_id(FK), apoderado_id(FK), parentesco | | |
| es_contacto_principal(boolean), autorizado_recojo(boolean) | | |

### `docentes`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombres, apellido_paterno, apellido_materno | | |
| dni(unique), sexo, fecha_nacimiento | | |
| foto, celular, correo, especialidad | | |
| tipo(enum: nombrado/contratado), estado | | |
| user_id(FK nullable), timestamps, softDeletes | | |

### `administrativos`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombres, apellido_paterno, apellido_materno | | |
| dni(unique), sexo, cargo, area_trabajo | | |
| foto, celular, correo, estado | | |
| user_id(FK nullable), timestamps, softDeletes | | |

### `visitantes`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombres, apellidos, dni, sexo | | |
| celular, correo, foto, empresa, cargo | | |
| tipo(enum: frecuente/ocasional), estado | | |
| timestamps, softDeletes | | |

### `matriculas` (ya existe, ampliar)
| Campo | Agregar | Notas |
|---|---|---|
| + periodo_id(FK) | reemplaza ciclo_id | |
| + nivel_id(FK), grado_id(FK) | | |
| + seccion_id(FK), aula_id(FK) | | |
| + numero_orden | integer nullable | |
| + tipo(enum: regular/traslado/nuevo) | | |
| + softDeletes | | |

### `movimientos_matricula`
| Campo | Tipo | Notas |
|---|---|---|
| id, matricula_id(FK), tipo | enum: traslado/retiro/cambio_seccion/cambio_aula | |
| seccion_origen_id, seccion_destino_id | FK nullable | |
| aula_origen_id, aula_destino_id | FK nullable | |
| fecha, motivo, user_id(FK), timestamps | | |

### `horarios`
| Campo | Tipo | Notas |
|---|---|---|
| id, periodo_id(FK), seccion_id(FK) | | |
| aula_id(FK nullable), turno_id(FK) | | |
| activo, timestamps | | |

### `horario_detalles`
| Campo | Tipo | Notas |
|---|---|---|
| id, horario_id(FK), dia_semana(enum:lun-vie) | | |
| docente_id(FK), area_id(FK) | | |
| hora_inicio(time), hora_fin(time) | | |
| timestamps | | |
| unique(horario_id, dia_semana, hora_inicio) | | |

### `docente_area`
| Campo | Tipo | Notas |
|---|---|---|
| docente_id(FK), area_id(FK), periodo_id(FK) | | pivot |

### `sesiones_aprendizaje`
| Campo | Tipo | Notas |
|---|---|---|
| id, horario_detalle_id(FK), docente_id(FK) | | |
| titulo, fecha, hora_inicio, hora_fin nullable | | |
| estado(enum: abierta/cerrada/omitida) | | |
| observacion, user_id(FK registro) | | |
| timestamps, softDeletes | | |

### `asistencia_estudiantes_aula`
| Campo | Tipo | Notas |
|---|---|---|
| id, sesion_id(FK), estudiante_id(FK), matricula_id(FK) | | |
| estado(enum: asistio/tardanza/falta/justificado/retirado) | | |
| hora_registro, metodo(enum: manual/qr/facial/dni) | | |
| observacion, user_id(FK), timestamps | | |
| unique(sesion_id, estudiante_id) | | |

### `asistencias_estudiantes_institucion`
| Campo | Tipo | Notas |
|---|---|---|
| id, estudiante_id(FK), matricula_id(FK), fecha | | |
| hora_entrada, hora_salida nullable | | |
| estado_entrada(enum: puntual/tardanza) | | |
| metodo_entrada, metodo_salida | enum: qr/dni/facial/manual | |
| dispositivo_id(FK nullable), observacion | | |
| user_registro_id, timestamps | | |

### `asistencias_docentes_institucion`
| Campo | Tipo | Notas |
|---|---|---|
| id, docente_id(FK), fecha | | |
| hora_entrada, hora_salida nullable | | |
| estado(enum: presente/tardanza/falta/justificado) | | |
| metodo_entrada, metodo_salida | | |
| observacion, user_id, timestamps | | |

### `marcaciones_administrativos`
| Campo | Tipo | Notas |
|---|---|---|
| id, administrativo_id(FK), fecha | | |
| hora_entrada, hora_inicio_receso nullable | | |
| hora_fin_receso nullable, hora_salida nullable | | |
| estado(enum: activo/en_receso/salio) | | |
| jornada_minutos integer nullable | | |
| metodo_entrada… metodo_salida | | |
| timestamps | | |
| **Regla:** no cerrar jornada con receso abierto | | |

### `visitas`
| Campo | Tipo | Notas |
|---|---|---|
| id, visitante_id(FK), fecha | | |
| hora_entrada, hora_salida nullable | | |
| motivo, persona_visitada, area_destino | | |
| autorizado_por, foto_visita nullable | | |
| estado(enum: activa/finalizada/cancelada) | | |
| user_id, timestamps | | |
| **Regla:** unique activa por visitante | | |

### `codigos_qr`
| Campo | Tipo | Notas |
|---|---|---|
| id, persona_type, persona_id | morphs | |
| codigo(string unique), activo | | |
| fecha_expiracion nullable, timestamps | | |

### `biometria_facial`
| Campo | Tipo | Notas |
|---|---|---|
| id, persona_type, persona_id | morphs | |
| encoding text | vector JSON del rostro | |
| umbral_coincidencia decimal(5,2) default 0.6 | | |
| activo, timestamps | | |

### `fotos_biometricas`
| Campo | Tipo | Notas |
|---|---|---|
| id, persona_type, persona_id | morphs | |
| ruta_foto, es_principal(boolean) | | |
| timestamps | | |

### `dispositivos_kiosco`
| Campo | Tipo | Notas |
|---|---|---|
| id, nombre, codigo(unique), tipo | enum: entrada/salida/mixto | |
| ubicacion, activo, user_id(FK) | Usuario kiosco asociado | |
| ultimo_ping, timestamps | | |

### `notificaciones`
| Campo | Tipo | Notas |
|---|---|---|
| id, tipo(enum: whatsapp/email/panel) | | |
| evento, destinatario_type, destinatario_id | morphs | |
| destinatario_contacto, contenido text | | |
| estado(enum: pendiente/enviado/error/reintento) | | |
| intentos int default 0, error_detalle text nullable | | |
| enviado_at nullable, timestamps | | |

### `incidencias`
| Campo | Tipo | Notas |
|---|---|---|
| id, tipo, descripcion | | |
| persona_type, persona_id | morphs | |
| estado(enum: abierta/resuelta/desestimada) | | |
| user_reporta_id, user_resuelve_id nullable | | |
| resolucion text nullable, timestamps | | |

### `auditoria_cambios`
| Campo | Tipo | Notas |
|---|---|---|
| id, user_id(FK), accion | string | |
| modelo, modelo_id, datos_antes JSON nullable | | |
| datos_despues JSON nullable, motivo text nullable | | |
| ip_address, user_agent, timestamps | | |

---

## 5. Seeders Iniciales

```
DatabaseSeeder
├── RolesPermisosSeeder       → 11 roles + permisos por módulo
├── InstitucionSeeder         → institución por defecto
├── CatalogoSeeder            → niveles, grados, turnos, areas
├── PeriodoAcademicoSeeder    → periodo activo actual
├── UserAdminSeeder           → superadmin + usuarios demo por rol
└── DispositivoKioscoSeeder   → 1 dispositivo de demo
```

### Roles y permisos resumidos:

| Rol | Acceso principal |
|---|---|
| superadmin | Todo sin restricción |
| director | Reportes, configuración, todos los módulos (lectura/escritura) |
| subdirector | Similar a director, sin configuración institucional |
| secretaria | Matrícula, personas, horarios, reportes básicos |
| docente | Sesiones, asistencia aula (solo las suyas) |
| tutor | Asistencia aula de su sección, reportes de su sección |
| auxiliar | Acceso institucional, visitantes |
| portero | Solo módulo de acceso: entrada/salida y visitantes |
| estudiante | Ver su propio horario y asistencia |
| apoderado | Ver información de sus hijos |
| kiosco | Solo marcación: entrada/salida, sin config ni reportes |

---

## 6. Flujos de Negocio Principales

### F1 — Matrícula de Estudiante
```
1. Buscar estudiante por DNI (¿existe?)
2. Si nuevo → registrar persona estudiante
3. Buscar/crear apoderado + relacionar
4. Verificar no tiene matrícula ACTIVA en el periodo actual
5. Asignar: periodo, nivel, grado, sección, aula
6. Generar código_matricula único
7. Crear QR asociado al estudiante
8. Registrar en auditoria_cambios
9. Notificar al apoderado (job)
```

### F2 — Marcación Institucional (Kiosco)
```
1. Recibir input: QR/DNI/facial
2. Identificar persona y tipo (estudiante/docente/admin/visitante)
3. Validar no tenga entrada abierta sin salida
4. Si entrada → crear registro con hora y método
5. Si salida → actualizar registro con hora_salida
6. Para administrativo: gestionar receso (inicio/fin) como estados
7. Lanzar evento → NotificacionIngreso job
8. Responder en < 1 segundo (modo kiosco)
```

### F3 — Asistencia en Aula
```
1. Docente abre sesión de aprendizaje (título + hora)
2. Sistema verifica que sea su horario_detalle del día y hora
3. Obtiene lista de estudiantes matriculados activos de esa sección
4. Docente marca a cada uno: asistió/tardanza/falta/justificado
5. Cierra sesión → se sella hora_fin
6. No se puede reabrir sesión cerrada (solo corrección con auditoría)
```

### F4 — Visitante
```
1. Buscar por DNI → ¿es frecuente?
2. Si nuevo → registrar visitante
3. Verificar que no tenga visita activa
4. Registrar: motivo, persona visitada, área, autorizado por
5. Entrada → visita en estado 'activa'
6. Salida → actualizar hora_salida, estado = 'finalizada'
7. Alerta si visitante sin salida pasado un umbral de tiempo
```

---

## 7. API Endpoints

### Auth
```
POST   /api/auth/login
POST   /api/auth/logout
POST   /api/auth/forgot-password
POST   /api/auth/reset-password
GET    /api/auth/me
```

### Configuración
```
GET|PUT  /api/configuracion/institucion
GET|POST|PUT|DELETE  /api/configuracion/periodos
GET|POST|PUT|DELETE  /api/configuracion/niveles
GET|POST|PUT|DELETE  /api/configuracion/grados
GET|POST|PUT|DELETE  /api/configuracion/secciones
GET|POST|PUT|DELETE  /api/configuracion/aulas
GET|POST|PUT|DELETE  /api/configuracion/turnos
GET|POST|PUT|DELETE  /api/configuracion/areas
```

### Personas
```
GET|POST        /api/personas/estudiantes
GET|PUT|DELETE  /api/personas/estudiantes/{id}
GET|POST        /api/personas/docentes
GET|PUT|DELETE  /api/personas/docentes/{id}
GET|POST        /api/personas/administrativos
GET|PUT|DELETE  /api/personas/administrativos/{id}
GET|POST        /api/personas/apoderados
GET|PUT|DELETE  /api/personas/apoderados/{id}
```

### Matrícula
```
GET|POST        /api/matriculas
GET|PUT|DELETE  /api/matriculas/{id}
POST            /api/matriculas/{id}/movimiento
GET             /api/matriculas/{id}/historial
GET             /api/matriculas/{id}/constancia   (PDF)
GET             /api/estudiantes/{id}/matricula-activa
```

### Horarios
```
GET|POST        /api/horarios
GET|PUT|DELETE  /api/horarios/{id}
GET|POST        /api/horarios/{id}/detalles
PUT|DELETE      /api/horarios/{id}/detalles/{detalle_id}
GET             /api/horarios/docente/{docente_id}
GET             /api/horarios/seccion/{seccion_id}
```

### Acceso Institucional
```
POST  /api/acceso/marcar                  ← endpoint principal kiosco
GET   /api/acceso/estado/{tipo}/{id}      ← consultar estado actual
GET   /api/acceso/presentes               ← quién está dentro
GET   /api/acceso/registros               ← historial con filtros
POST  /api/acceso/correccion              ← corrección manual auditada
```

### Asistencia Aula
```
POST  /api/sesiones                       ← abrir sesión
PUT   /api/sesiones/{id}/cerrar
GET   /api/sesiones/{id}/lista            ← lista de estudiantes
POST  /api/sesiones/{id}/asistencia       ← registrar masivo
PUT   /api/sesiones/{id}/asistencia/{est_id}
GET   /api/docentes/{id}/sesiones
```

### Visitantes
```
GET|POST        /api/visitantes
GET|PUT|DELETE  /api/visitantes/{id}
POST            /api/visitantes/{id}/entrada
POST            /api/visitantes/{id}/salida
GET             /api/visitantes/activos
GET             /api/visitantes/{id}/historial
```

### Biometría
```
POST  /api/biometria/qr/generar/{tipo}/{id}
POST  /api/biometria/facial/registrar
POST  /api/biometria/facial/verificar       ← llama microservicio local
GET   /api/biometria/personas/{tipo}/{id}
```

### Notificaciones
```
GET   /api/notificaciones?destinatario=me
PUT   /api/notificaciones/{id}/leida
GET   /api/notificaciones/log              ← admin: historial envíos
```

### Reportes
```
GET   /api/reportes/dashboard
GET   /api/reportes/matriculas            → PDF/Excel
GET   /api/reportes/asistencia-aula       → PDF/Excel
GET   /api/reportes/asistencia-docentes   → PDF/Excel
GET   /api/reportes/administrativos       → PDF/Excel
GET   /api/reportes/visitantes            → PDF/Excel
GET   /api/reportes/cumplimiento-docente  → PDF/Excel
```

### Auditoría
```
GET   /api/auditoria
GET   /api/incidencias
POST  /api/incidencias
PUT   /api/incidencias/{id}
```

---

## 8. Componentes y Páginas React

### Páginas principales
```
/login                     → LoginPage
/dashboard                 → DashboardPage (tarjetas en tiempo real)
/configuracion/*           → páginas de configuración
/personas/estudiantes      → EstudiantesPage + EstudianteForm
/personas/docentes         → DocentesPage + DocenteForm
/personas/administrativos  → AdminsPage
/personas/apoderados       → ApoderadosPage
/matriculas                → MatriculasPage
/matriculas/nueva          → NuevaMatriculaWizard (3 pasos)
/matriculas/:id            → DetalleMatricula + movimientos
/horarios                  → HorariosPage + VistaSemanал
/acceso                    → AccesoPage (tiempo real)
/asistencia/sesiones       → SesionesPage
/asistencia/sesiones/:id   → ListaAsistenciaPage
/visitantes                → VisitantesPage
/reportes                  → ReportesPage + filtros + export
/auditoria                 → AuditoriaPage
/kiosco                    → KioscoPage (fullscreen, sin layout)
```

### Componentes compartidos clave
```
<DataTable />              → tabla genérica con filtros, paginación
<FormField />              → input + label + error unificado
<SearchDNI />              → búsqueda de persona por DNI con autocompletado
<EstadoBadge />            → badge de color por estado
<ConfirmModal />           → modal de confirmación
<ExportButton />           → exportar PDF/Excel
<QrScanner />              → cámara + decodificador QR
<CamaraCaptura />          → captura de foto
<SemanaView />             → vista semanal de horario
<KioscoCard />             → tarjeta de acción en modo kiosco
```

---

## 9. Estrategia de Roles y Permisos

Se usa `spatie/laravel-permission`. Los permisos siguen el patrón:
`{modulo}.{accion}` — Ejemplos:

```
matriculas.ver        matriculas.crear     matriculas.editar     matriculas.eliminar
sesiones.abrir        sesiones.cerrar      sesiones.asistencia
acceso.marcar         acceso.corregir      acceso.ver
personas.estudiantes.ver ...crear ...editar ...eliminar
reportes.ver          reportes.exportar
configuracion.ver     configuracion.editar
auditoria.ver
kiosco.marcar
```

**Middleware personalizado:**
```php
// En rutas API
->middleware(['auth:sanctum', 'role:director|secretaria'])
->middleware(['auth:sanctum', 'permission:matriculas.crear'])
// Para kiosco: bloquear acceso a rutas admin
->middleware(['auth:sanctum', 'role:kiosco', App\Http\Middleware\KioscoOnly::class])
```

---

## 10. Plan de Implementación por Fases

### Fase 1 — Fundación (2 semanas)
- [ ] Refactorizar migraciones existentes (estudiantes, matriculas)
- [ ] Nuevas migraciones: instituciones, periodos, niveles, grados, secciones, aulas, turnos, areas
- [ ] Nuevas migraciones: docentes, administrativos, apoderados, visitantes
- [ ] Ampliar users (persona_type, persona_id, activo)
- [ ] RolesPermisosSeeder completo
- [ ] API Auth (login username/email, logout, me)
- [ ] React: layout base + login + dashboard esqueleto

### Fase 2 — Configuración y Personas (1.5 semanas)
- [ ] CRUD Configuración institucional (API + React)
- [ ] CRUD Estudiantes, Docentes, Administrativos, Apoderados (API + React)
- [ ] Subida de fotos
- [ ] Búsqueda por DNI

### Fase 3 — Matrícula (1.5 semanas)
- [ ] Migraciones: matriculas (refactor), movimientos_matricula
- [ ] MatriculaService con reglas de negocio
- [ ] API matrícula completa + policies
- [ ] React: wizard de matrícula, lista con filtros, constancia PDF
- [ ] Historial de movimientos

### Fase 4 — Horarios y Sesiones (1 semana)
- [ ] Migraciones: horarios, horario_detalles, docente_area
- [ ] API horarios con validación de cruces
- [ ] Migraciones: sesiones_aprendizaje
- [ ] API sesiones (abrir/cerrar/omitir)
- [ ] React: vista semanal + gestión de sesiones

### Fase 5 — Asistencia en Aula (1 semana)
- [ ] Migración: asistencia_estudiantes_aula
- [ ] AsistenciaService + reglas de negocio
- [ ] API asistencia aula
- [ ] React: lista de asistencia por sesión

### Fase 6 — Acceso Institucional + Kiosco (2 semanas)
- [ ] Migraciones: asistencias (estudiantes/docentes), marcaciones_administrativos, visitas
- [ ] Migraciones: codigos_qr, biometria_facial, fotos_biometricas, dispositivos_kiosco
- [ ] AccesoService con todas las reglas de negocio
- [ ] API de marcación unificada
- [ ] React: página de acceso en tiempo real
- [ ] React: KioscoPage fullscreen

### Fase 7 — Notificaciones (1 semana)
- [ ] Migración: notificaciones
- [ ] Jobs: EnviarWhatsapp, EnviarEmail, EnviarNotificacionPanel
- [ ] Eventos y listeners
- [ ] Configuración de canal por institución

### Fase 8 — Reportes y Auditoría (1 semana)
- [ ] Migraciones: auditoria_cambios, incidencias
- [ ] AuditoriaObserver global
- [ ] Dashboard con datos en tiempo real
- [ ] Reportes PDF (Laravel DomPDF) y Excel (Laravel Excel)
- [ ] React: página de reportes con filtros y export

### Fase 9 — Pulido y Pruebas (1 semana)
- [ ] Tests de flujos críticos (Feature Tests)
- [ ] Optimización de consultas (índices, eager loading)
- [ ] Revisión de permisos y policies
- [ ] Documentación básica

---

## 11. Código Base de Ejemplo

### A. Login por username o email

```php
// app/Http/Controllers/Api/Auth/LoginController.php
class LoginController extends Controller
{
    public function __invoke(LoginRequest $request): JsonResponse
    {
        $field = filter_var($request->login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
        $credentials = [$field => $request->login, 'password' => $request->password];

        if (!Auth::attempt($credentials)) {
            throw ValidationException::withMessages([
                'login' => ['Credenciales incorrectas.'],
            ]);
        }

        $user = Auth::user();
        if (!$user->activo) {
            Auth::logout();
            return response()->json(['message' => 'Usuario inactivo.'], 403);
        }

        $token = $user->createToken('api-token')->plainTextToken;

        AuditoriaService::registrar($user, 'login', 'users', $user->id);

        return response()->json([
            'token' => $token,
            'user'  => new UserResource($user),
        ]);
    }
}
```

### B. Middleware de Rol para Kiosco

```php
// app/Http/Middleware/KioscoOnly.php
class KioscoOnly
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if ($user && $user->hasRole('kiosco')) {
            $allowedRoutes = ['api.acceso.marcar', 'api.acceso.estado'];
            if (!in_array($request->route()->getName(), $allowedRoutes)) {
                return response()->json(['message' => 'Acceso no permitido en modo kiosco.'], 403);
            }
        }
        return $next($request);
    }
}
```

### C. Modelo Matrícula

```php
// app/Models/Matricula.php
class Matricula extends Model
{
    use SoftDeletes, HasFactory;

    protected $fillable = [
        'periodo_id', 'estudiante_id', 'nivel_id', 'grado_id',
        'seccion_id', 'aula_id', 'codigo_matricula', 'fecha_matricula',
        'numero_orden', 'tipo', 'estado', 'observaciones', 'user_id',
    ];

    protected $casts = [
        'fecha_matricula' => 'date',
        'estado' => MatriculaEstado::class, // PHP Enum
    ];

    public function periodo(): BelongsTo { return $this->belongsTo(PeriodoAcademico::class); }
    public function estudiante(): BelongsTo { return $this->belongsTo(Estudiante::class); }
    public function seccion(): BelongsTo { return $this->belongsTo(Seccion::class); }
    public function aula(): BelongsTo { return $this->belongsTo(Aula::class); }
    public function movimientos(): HasMany { return $this->hasMany(MovimientoMatricula::class); }
    public function asistenciasAula(): HasMany { return $this->hasMany(AsistenciaEstudianteAula::class); }

    public function scopeActiva(Builder $q): Builder
    {
        return $q->where('estado', MatriculaEstado::ACTIVO);
    }

    public function scopeDelPeriodo(Builder $q, int $periodoId): Builder
    {
        return $q->where('periodo_id', $periodoId);
    }
}
```

### D. MatriculaService (reglas de negocio)

```php
// app/Services/MatriculaService.php
class MatriculaService
{
    public function matricular(array $data, User $operador): Matricula
    {
        // Regla: no puede tener matrícula activa en el mismo periodo
        $existe = Matricula::where('estudiante_id', $data['estudiante_id'])
            ->where('periodo_id', $data['periodo_id'])
            ->activa()->exists();

        if ($existe) {
            throw new MatriculaDuplicadaException(
                'El estudiante ya tiene una matrícula activa en este periodo.'
            );
        }

        return DB::transaction(function () use ($data, $operador) {
            $matricula = Matricula::create([
                ...$data,
                'codigo_matricula' => $this->generarCodigo($data),
                'user_id' => $operador->id,
                'estado' => MatriculaEstado::ACTIVO,
            ]);

            CodigoQr::firstOrCreate(
                ['persona_type' => Estudiante::class, 'persona_id' => $data['estudiante_id']],
                ['codigo' => Str::uuid(), 'activo' => true]
            );

            AuditoriaService::registrar($operador, 'matricula.crear', 'matriculas', $matricula->id,
                null, $matricula->toArray()
            );

            event(new EstudianteMatriculado($matricula));
            return $matricula;
        });
    }

    private function generarCodigo(array $data): string
    {
        $periodo = PeriodoAcademico::find($data['periodo_id']);
        $count = Matricula::where('periodo_id', $data['periodo_id'])->count() + 1;
        return sprintf('MAT-%s-%04d', $periodo->nombre, $count);
    }
}
```

### E. AccesoService — Marcación institucional

```php
// app/Services/AccesoService.php
class AccesoService
{
    public function marcar(string $tipo, string $personaType, int $personaId, string $metodo, ?int $dispositivoId = null): array
    {
        return DB::transaction(function () use ($tipo, $personaType, $personaId, $metodo, $dispositivoId) {
            if ($personaType === 'estudiante') {
                return $this->marcarEstudiante($tipo, $personaId, $metodo);
            }
            if ($personaType === 'administrativo') {
                return $this->marcarAdministrativo($tipo, $personaId, $metodo);
            }
            // docente, visitante...
        });
    }

    private function marcarAdministrativo(string $tipo, int $id, string $metodo): array
    {
        $hoy = today();
        $registro = MarcacionAdministrativo::firstOrNew(['administrativo_id' => $id, 'fecha' => $hoy]);

        match ($tipo) {
            'entrada'      => $this->validarYRegistrarEntrada($registro, $metodo),
            'inicio_receso'=> $this->iniciarReceso($registro),
            'fin_receso'   => $this->finalizarReceso($registro),
            'salida'       => $this->registrarSalida($registro),
            default        => throw new \InvalidArgumentException("Tipo de marcación inválido: $tipo"),
        };

        return ['estado' => $registro->estado, 'hora' => now()->format('H:i:s')];
    }

    private function registrarSalida(MarcacionAdministrativo $r): void
    {
        // Regla: no puede salir con receso abierto
        if ($r->hora_inicio_receso && !$r->hora_fin_receso) {
            throw new RecesAbiertaException('Debe cerrar el receso antes de registrar la salida.');
        }
        $r->hora_salida = now();
        $r->estado = 'salio';
        $r->jornada_minutos = $r->hora_entrada
            ? $r->hora_entrada->diffInMinutes(now())
            : null;
        $r->save();
    }
}
```

### F. SesionAprendizaje — Abrir y asistencia

```php
// app/Services/SesionAprendizajeService.php
class SesionAprendizajeService
{
    public function abrir(array $data, User $docente): SesionAprendizaje
    {
        // Verificar que el detalle de horario pertenece al docente
        $detalle = HorarioDetalle::findOrFail($data['horario_detalle_id']);
        if ($detalle->docente_id !== $docente->persona_id) {
            throw new UnauthorizedException('No puede abrir una sesión fuera de su horario.');
        }

        // Verificar no hay sesión abierta para este detalle hoy
        $abierta = SesionAprendizaje::where('horario_detalle_id', $detalle->id)
            ->whereDate('fecha', today())
            ->where('estado', 'abierta')->exists();

        if ($abierta) {
            throw new SesionDuplicadaException('Ya existe una sesión abierta para este horario hoy.');
        }

        $sesion = SesionAprendizaje::create([
            'horario_detalle_id' => $detalle->id,
            'docente_id' => $detalle->docente_id,
            'titulo' => $data['titulo'],
            'fecha' => today(),
            'hora_inicio' => now()->format('H:i:s'),
            'estado' => 'abierta',
            'user_id' => $docente->id,
        ]);

        // Pre-poblar lista con estudiantes matriculados activos
        $this->prepoblarAsistencia($sesion, $detalle->horario->seccion_id);

        return $sesion;
    }

    private function prepoblarAsistencia(SesionAprendizaje $sesion, int $seccionId): void
    {
        $matriculas = Matricula::activa()
            ->where('seccion_id', $seccionId)
            ->get();

        $filas = $matriculas->map(fn($m) => [
            'sesion_id' => $sesion->id,
            'estudiante_id' => $m->estudiante_id,
            'matricula_id' => $m->id,
            'estado' => 'falta', // estado inicial por defecto
            'created_at' => now(), 'updated_at' => now(),
        ])->toArray();

        AsistenciaEstudianteAula::insert($filas);
    }
}
```

### G. AuditoriaService

```php
// app/Services/AuditoriaService.php
class AuditoriaService
{
    public static function registrar(
        User $user, string $accion, string $modelo,
        ?int $modeloId = null, ?array $antes = null, ?array $despues = null,
        ?string $motivo = null
    ): void {
        AuditoriaCambio::create([
            'user_id' => $user->id,
            'accion' => $accion,
            'modelo' => $modelo,
            'modelo_id' => $modeloId,
            'datos_antes' => $antes,
            'datos_despues' => $despues,
            'motivo' => $motivo,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);
    }
}
```

### H. Pantalla Kiosco (React)

```tsx
// features/kiosco/KioscoPage.tsx
export default function KioscoPage() {
  const [step, setStep] = useState<'select' | 'scan' | 'result'>('select');
  const [flujo, setFlujo] = useState<string | null>(null);
  const [resultado, setResultado] = useState<MarcacionResult | null>(null);
  const marcar = useMarcarMutation();

  const flujos = [
    { key: 'entrada_estudiante', label: 'Entrada Estudiante', icon: '🎒', color: 'bg-blue-600' },
    { key: 'salida_estudiante',  label: 'Salida Estudiante',  icon: '🏠', color: 'bg-blue-400' },
    { key: 'entrada_docente',    label: 'Entrada Docente',    icon: '👩‍🏫', color: 'bg-green-600' },
    { key: 'entrada_admin',      label: 'Entrada Admin',      icon: '💼', color: 'bg-purple-600' },
    { key: 'inicio_receso',      label: 'Inicio Receso',      icon: '☕', color: 'bg-yellow-500' },
    { key: 'fin_receso',         label: 'Fin Receso',         icon: '🔄', color: 'bg-yellow-600' },
    { key: 'salida_admin',       label: 'Salida Admin',       icon: '👋', color: 'bg-purple-400' },
    { key: 'entrada_visitante',  label: 'Entrada Visitante',  icon: '🪪', color: 'bg-orange-500' },
    { key: 'salida_visitante',   label: 'Salida Visitante',   icon: '🚪', color: 'bg-orange-400' },
  ];

  const handleScan = async (codigo: string) => {
    const res = await marcar.mutateAsync({ flujo: flujo!, codigo });
    setResultado(res);
    setStep('result');
    setTimeout(() => { setStep('select'); setFlujo(null); }, 4000);
  };

  if (step === 'select') return (
    <div className="min-h-screen bg-gray-900 flex flex-col items-center justify-center p-8">
      <h1 className="text-4xl font-bold text-white mb-2">SIAE — Control de Acceso</h1>
      <p className="text-gray-400 mb-10 text-lg">Seleccione el tipo de marcación</p>
      <div className="grid grid-cols-3 gap-6 max-w-4xl w-full">
        {flujos.map(f => (
          <button key={f.key} onClick={() => { setFlujo(f.key); setStep('scan'); }}
            className={`${f.color} text-white rounded-2xl p-8 text-center text-2xl font-bold
              shadow-lg hover:scale-105 transition-transform duration-150 flex flex-col items-center gap-3`}>
            <span className="text-5xl">{f.icon}</span>
            {f.label}
          </button>
        ))}
      </div>
    </div>
  );

  if (step === 'scan') return (
    <KioscoScanStep flujo={flujo!} onScan={handleScan} onCancel={() => setStep('select')} />
  );

  return <KioscoResultado resultado={resultado!} />;
}
```

---

## 12. Buenas Prácticas y Decisiones Técnicas

| Decisión | Justificación |
|---|---|
| API-first (Laravel JSON + React) | Permite escalar a app móvil sin cambiar el backend |
| `horario_detalles` separado de `sesiones` | La sesión es un **evento** del horario, no el horario mismo |
| Morfos para QR y biometría | Un solo modelo soporta estudiante/docente/admin sin duplicar código |
| Jobs para notificaciones | El request del kiosco no puede esperar un envío de WhatsApp |
| Soft deletes en entidades principales | Nunca perder historial; facilita auditoría |
| `AuditoriaService` estático | Facilita llamarlo desde cualquier punto sin inyección |
| MatriculaEstado como PHP Enum | Type-safe, documentado, central |
| `unique(sesion_id, estudiante_id)` | Imposible duplicar asistencia a nivel de BD |
| `unique(ciclo/periodo_id + estudiante_id)` | Imposible matricular dos veces al mismo estudiante |
| `unique activa por visitante` (via query guard) | Cumple la regla de negocio de visita única activa |
| Biometría facial desacoplada | Endpoint propio que delega a microservicio local (Python/Flask), nunca acopla al request principal |
| Zustand para estado global frontend | Liviano, sin boilerplate excesivo de Redux |
| React Query para server state | Cache automático, invalidación limpia, loading/error states |
