# Plan — Fase 4: panel de Registros de asistencia (listado, filtros, corrección con auditoría)

## Contexto

Fase 0 a 3 están completas, revisadas y commiteadas (`b63c910`,
`d9a03f1`, `b3381d8`, `0ed04ef`, `43830da`) — ver `../../CLAUDE.md`. El
kiosko ya crea `attendance_records` reales (modo manual, con o sin
asignación, `out_of_schedule`/`late_declaration` calculados), pero
**nadie puede verlos todavía desde el panel admin** — no hay listado,
no hay forma de corregir un dato mal declarado.

Esta fase construye esa pantalla — la última pieza que le faltaba al
ciclo completo "el docente ficha → el admin puede revisar y corregir
si hace falta". También es donde se cumple un requisito que quedó
pendiente desde el diseño del modo manual (`../../CLAUDE.md` §10):
**"el panel admin debe mostrar visible cuántos registros están en modo
manual, para que el uso del modo débil nunca pase desapercibido"** —
no negociable, se cumple acá con los summary cards del listado.

## Alcance

**Incluye:**
- `Livewire\Admin\AttendanceRecords` — listado en
  `/admin/attendance-records`. Filtros: docente (buscador por nombre/
  documento), curso, periodo académico, rango de fecha de clase, y
  **alertas** (fuera de horario / declarado tarde / modo manual —
  cada una filtrable independientemente, no excluyentes entre sí).
  Summary cards: Total, Fuera de horario, Declarado tarde, **Modo
  manual** (este último cumple el requisito no negociable de
  `../../CLAUDE.md` §10 — tiene que estar siempre visible, no solo
  disponible como filtro).
- `Livewire\Admin\AttendanceRecordDetail` (o nombre equivalente) —
  página propia (no modal, hay demasiado contenido: datos completos,
  3 fotos de evidencia, historial de correcciones) en
  `/admin/attendance-records/{record}`. Muestra todo el registro,
  badges de `out_of_schedule`/`late_declaration`/`identification_method`,
  las 3 fotos, y el historial completo de `attendance_corrections` ya
  aplicadas (quién, cuándo, motivo, campo, valor anterior → nuevo).
- Formulario de corrección en esa misma página: campos editables
  (`course`, `cycle`, `classroom`, `academic_period`, `topic`,
  `student_count`, `check_in_time`, `check_out_time`, `class_date`),
  precargados con los valores actuales, + `reason` obligatorio. Al
  guardar: por **cada campo que efectivamente cambió de valor**, crea
  una fila en `attendance_corrections` (mismo `reason` para todas las
  del mismo envío), y actualiza el registro real.
  - `teacher_id`, `identification_method` y `assignment_id` **no son
    corregibles** — representan cómo se capturó el registro, no un
    dato que se pueda "corregir", son de solo lectura acá.
  - Si se corrige `check_in_time`/`check_out_time` y el registro tiene
    `assignment_id`, **recalcular `out_of_schedule`** con la misma
    lógica de tolerancia de 15 min que ya usa el kiosko (ver tarea de
    refactor abajo).
  - Si se corrige `class_date`, **recalcular `late_declaration`**.
- **Refactor**: extraer la lógica de `out_of_schedule` de
  `Kiosk::isOutOfSchedule()` (privada, atada a las propiedades del
  componente) a un método reusable — ej. estático en `Assignment`
  (`Assignment::isOutOfSchedule(string $checkIn, string $checkOut):
  bool`) o en `AttendanceRecord`. `Kiosk.php` pasa a llamarlo en vez de
  tener su propia copia. Evita mantener la misma regla de tolerancia
  en dos lugares.
- Modelo `AttendanceCorrection` + relaciones (`belongsTo`
  `AttendanceRecord`, `belongsTo` `User` como `correctedBy`).
  `AttendanceRecord` gana `hasMany` `corrections()`.
- Ruta `/admin/attendance-records` y `/admin/attendance-records/{record}`,
  protegidas igual que el resto (`auth` + `role:ADMIN`).

**No incluye:**
- Reportes / exportables Excel-PDF, Dashboard — Fase 5.
- Bloquear la corrección de un registro cuyo `assignment_id` ya no
  existe (fue eliminado) — no debería poder pasar (la regla
  transversal ya bloquea eliminar `assignments` con dependencias), no
  se agrega un guard extra para un caso que no puede ocurrir.
- Huella / lector real — Fase 6.
- Porteo del diseño visual del mockup — sigue pendiente.

## Tareas

- [x] 1. Migración `attendance_corrections` — columnas de
      `../MODELO-DATOS.md` §12: `attendance_record_id`, `corrected_by`
      (FK `users`), `field`, `old_value`, `new_value`, `reason`
      (obligatorio), `created_at` (sin `updated_at`, inmutable — mismo
      patrón que `attendance_evidence` de Fase 2b).
- [x] 2. Modelo `AttendanceCorrection` + relaciones. `AttendanceRecord`
      gana `corrections()` (`hasMany`, ordenado por `created_at desc`).
- [x] 3. Refactor: mover la lógica de tolerancia de 15 min de
      `Kiosk::isOutOfSchedule()` a un método reusable (`Assignment` o
      `AttendanceRecord`, a criterio de quien implemente — lo
      importante es que quede en un solo lugar). Actualizar
      `Kiosk.php` para usarlo. Correr toda la suite existente después
      de este paso específico, antes de seguir — si algo de Fase 2b/3
      se rompe acá, es un problema del refactor, no de las tareas
      siguientes.
- [x] 4. `Livewire\Admin\AttendanceRecords` — listado con los filtros
      y summary cards descritos en Alcance. Eager-load
      (`teacher`, `course`, `cycle`, `classroom`, `academicPeriod`)
      para evitar N+1 en la tabla.
- [x] 5. `Livewire\Admin\AttendanceRecordDetail` — vista completa +
      historial de correcciones + formulario de corrección descrito en
      Alcance.
- [x] 6. Rutas para ambas pantallas, protegidas.
- [x] 7. Seeder o factory de `AttendanceRecord`/`AttendanceEvidence`
      de ejemplo si hace falta para poder probar el listado con datos
      (revisar si ya hay suficiente con lo que deja el flujo real del
      kiosko en los tests, o si conviene un seeder aparte para
      poblar el panel admin en desarrollo — decisión de quien
      implemente, dejarlo anotado si se agregó).

## Criterios de aceptación

- [x] `docker compose exec app php artisan migrate:fresh --seed` sin
      errores.
- [x] `docker compose exec app php artisan test` en verde, incluye:
      - Filtros del listado (docente, curso, periodo, rango de fecha,
        cada alerta) devuelven los resultados correctos.
      - Summary cards muestran los conteos correctos, en particular el
        de **modo manual** (requisito no negociable).
      - Corregir un solo campo (ej. `classroom_id`) con motivo crea
        exactamente 1 fila en `attendance_corrections` con
        `old_value`/`new_value` correctos, y actualiza el registro.
      - Corregir varios campos a la vez crea una fila **por campo**
        cambiado, todas con el mismo `reason`.
      - Sin `reason` (vacío) la corrección se bloquea, no se guarda
        nada.
      - Corregir `check_in_time`/`check_out_time` de un registro con
        `assignment_id` recalcula `out_of_schedule` correctamente
        (probar un caso que pasa de `false` a `true` y viceversa).
      - Corregir `class_date` recalcula `late_declaration`
        correctamente.
      - Regresión: la suite completa de Fase 2b/3 sigue en verde tras
        el refactor de la tarea 3 — no se modifican esos archivos de
        test para "hacerlos pasar", si algo se rompe es señal de que
        el refactor cambió el comportamiento, no solo la ubicación del
        código.
- [x] Verificado con HTTP real contra `:8020`: `/admin/attendance-records`
      pide sesión (302 sin cookie), la interacción completa (login →
      listado → detalle → corrección → historial) queda cubierta por
      `Fase4AttendanceRecordsTest` vía el cliente HTTP real de Laravel
      (`$this->actingAs()->get()`), mismo criterio que las fases
      anteriores — no se armó a mano un POST Livewire crudo por
      `curl` (protocolo de snapshot/checksum), la cobertura ya
      ejercita el stack completo de rutas/middleware/blade.

### Hallazgo adicional encontrado por Claude durante la implementación

`php artisan storage:link` **nunca se había corrido** en este proyecto
— sin él, ninguna foto guardada en el disco `public` (evidencias del
kiosko, foto de referencia de docentes) era accesible por URL, aunque
el archivo sí existiera en `storage/app/public/`. No se notó antes
porque esta es la primera pantalla que realmente muestra esas fotos
guardadas (el kiosko solo mostraba vistas previas temporales antes de
guardar). Corrido ahora; **queda pendiente para cuando se prepare
producción real**: automatizar este paso en el proceso de despliegue
(no es algo que se corra manualmente cada vez).

## Notas para quien implemente

- Identificadores en inglés, comentarios en español solo si aportan un
  porqué no obvio, UI 100% en español — igual que fases anteriores.
- `attendance_corrections` es de solo-alta — nunca se edita ni se
  borra una corrección ya creada (si se corrige algo dos veces, son
  dos filas). No agregar `eliminar()` a este modelo.
- El requisito de "modo manual siempre visible" (`../../CLAUDE.md`
  §10) no es un detalle cosmético — es la contraparte de haber
  descartado el PIN de contingencia: el respaldo de ese riesgo
  aceptado es que el admin pueda ver de un vistazo cuánto se está
  usando el modo sin huella. No lo dejes como "solo un filtro más
  entre varios", el summary card tiene que estar ahí siempre.
- Reusar componentes UI ya existentes (`x-ui.summary-cards`,
  `x-ui.sort-header`, `x-ui.badge`, `x-ui.icon-button`,
  `x-ui.confirm-modal` para confirmar antes de aplicar una corrección
  dado que modifica datos ya guardados) — mismo patrón que el resto
  del admin, no inventar uno nuevo para esta pantalla.
