# Plan — Fase 2a: users, autenticación y gestión de administradores/docentes

## Contexto

Fase 0 (setup) y Fase 1 (catálogos base + `system_configuration`) ya
están completas, revisadas y commiteadas (`b63c910`) — ver
`../../CLAUDE.md`. Las rutas `/admin/*` de Fase 1 se construyeron **sin**
autenticación a propósito, porque `users` todavía no existía.

Esta fase construye la identidad real del sistema (`users` con
`role: ADMIN|TEACHER`, según `../MODELO-DATOS.md` §1) y la autenticación
— deja el panel administrativo protegido y con la gestión de personas
lista. **No incluye el kiosko** (eso es la Fase 2b, que depende de que
esta fase exista primero: el kiosko necesita `users` con `role=TEACHER`
para identificar por documento).

**Referencia estructural** (adaptar, no copiar literal — el modelo de
roles es distinto): `../../../tutoria-fichas/app/Livewire/Auth/Login.php`
(`Auth::attempt()` con columna de guard propia + chequeo `activo` en el
mismo array) y `../../../tutoria-fichas/app/Http/Middleware/EnsurePerfil.php`
(middleware de rol, ahí es multi-perfil — acá es un enum `role` simple,
mucho más chico).

## Alcance

**Incluye:**
- Rediseñar la migración `create_users_table` (la de Laravel, **editarla
  directamente** — estamos en desarrollo activo, no se crean migraciones
  `alter`, mismo criterio que ya usa `tutor-backend`/`tutoria-fichas`)
  con las columnas exactas de `../MODELO-DATOS.md` §1: `document_type`
  (enum), `document_number` (único compuesto con `document_type`),
  `names`, `paternal_surname`, `maternal_surname` (nullable), `sex`,
  `birth_date`, `email` (nullable), `phone` (nullable), `password`
  (nullable), `role` (enum `ADMIN`/`TEACHER`, real enum de BD),
  `reference_photo_path` (nullable), `active`, soft delete
  (`deleted_at`).
- Actualizar `App\Models\User` con esos campos, `$fillable`, casts,
  helper `isAdmin()`/`isTeacher()` (o el enum de rol que se prefiera).
- Autenticación: `Livewire\Auth\Login` — email + password, **solo
  `role=ADMIN`** puede loguearse (un `TEACHER` nunca tiene password
  seteado, así que `Auth::attempt` ya falla solo para ellos, pero
  agregar `role: 'ADMIN'` explícito en el array de `attempt()` para que
  quede documentado en el propio código, no solo implícito). Logout.
- Middleware `EnsureRole` (o nombre equivalente) que exige
  `role=ADMIN` autenticado — **envolver con él todas las rutas
  `/admin/*` ya existentes de Fase 1** (`catalogs`, `academic-periods`,
  `classrooms`, `system-configuration`), no solo las nuevas de esta
  fase. Esto es la tarea que cierra el hueco de seguridad que Fase 1
  dejó a propósito.
- `Livewire\Admin\Users` parametrizado por `role` (mismo patrón que
  `CatalogSimple` parametrizado por `type` en Fase 1) — dos vistas,
  `/admin/users/administrators` y `/admin/users/teachers`, mismo
  componente:
  - Alta/edición con los campos de identidad. `password` solo aparece
    en el formulario cuando `role=ADMIN` (obligatorio al crear,
    opcional al editar — dejar en blanco no cambia la contraseña
    existente). `reference_photo_path`: solo en el formulario de
    `TEACHER`, subida de archivo simple opcional (no está atado a
    enrolamiento de huella — eso es Fase 6, esto es solo guardar una
    foto de referencia si el admin ya la tiene a mano).
  - Eliminar respeta la regla transversal (`../../CLAUDE.md` §11):
    bloquear con `QueryException` 23000 si hay dependencias, dirigir a
    desactivar.
- Seeder: **un administrador real de desarrollo** (mismo criterio que
  `tutoria-fichas`: `admin@unamba.edu.pe` / `password`, marcado
  claramente como dato de desarrollo, no real) + 2-3 `TEACHER` de
  ejemplo (placeholders, sin `password`, para poder probar la Fase 2b
  después).

**No incluye:**
- Kiosko (reposo, identificación por documento, formulario de fichaje,
  captura de fotos) — Fase 2b.
- `attendance_records` / `attendance_evidence` — Fase 2b (viven junto
  al flujo que las usa, no acá).
- `fingerprints` / enrolamiento de huella real — Fase 6, bloqueada
  hasta que llegue el hardware.
- Flujo de "cambio de contraseña obligatorio en primer login" — se
  evaluó y se deja fuera por ahora (pocos administradores, seteo manual
  alcanza); si se quiere agregar después es un cambio chico y aislado,
  no bloquea esta fase.
- Porteo del diseño visual del mockup — sigue pendiente, tarea aparte.

## Tareas

- [x] 1. Editar `database/migrations/0001_01_01_000000_create_users_table.php`
      con las columnas de `MODELO-DATOS.md` §1 — reemplazar el
      esqueleto por defecto de Laravel (`name`, `email_verified_at`,
      etc.) por el esquema real. Único compuesto
      `(document_type, document_number)`. `role` como enum real de BD
      (`ADMIN`, `TEACHER`).
- [x] 2. Actualizar `App\Models\User`: `$fillable`, `casts()` (`active`,
      `birth_date`, etc.), soft deletes (`use SoftDeletes`), relación/
      helper de rol.
- [x] 3. `Livewire\Auth\Login` + vista — email/password,
      `Auth::attempt(['email' => ..., 'password' => ..., 'role' =>
      'ADMIN', 'active' => true])`, mensaje de error genérico (no decir
      si fue el email o el password lo que falló). Logout (`POST
      /logout`).
- [x] 4. Middleware `EnsureRole` (alias `role`, parametrizable
      `role:ADMIN` por si algún día hace falta otro valor) — redirige a
      `/login` si no hay sesión, `403` si el rol no calza.
- [x] 5. Envolver **todas** las rutas `/admin/*` (las 4 de Fase 1 + las
      nuevas de esta fase) con `auth` + `role:ADMIN` en
      `routes/web.php`.
- [x] 6. `Livewire\Admin\Users` parametrizado por `role`
      (`administrators`/`teachers` en la URL → `ADMIN`/`TEACHER` en la
      query), con los campos descritos en Alcance. Rutas
      `/admin/users/{type}`.
- [x] 7. Seeder de desarrollo: 1 `ADMIN` real (credenciales de
      desarrollo, marcadas como tal) + 2-3 `TEACHER` placeholder.
- [x] 8. Actualizar `system-configuration.blade.php` (Fase 1) — ya no
      hace falta el comentario "sin auth en esta fase", y
      `$config->updater?->name` debe cambiar a los campos reales
      (`names`/`paternal_surname`) ahora que `users` tiene el esquema
      final. Es la nota de continuidad que quedó anotada al cerrar
      Fase 1 (`../../CLAUDE.md`) — no se te puede pasar.

## 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
      tests nuevos para: login exitoso, login fallido (credenciales
      malas, usuario inactivo, intento de loguear un `TEACHER` sin
      password), acceso a `/admin/*` sin sesión redirige a login,
      acceso autenticado como `ADMIN` funciona.
      22/22 passed (178 assertions) — corrido de forma independiente
      por Claude, dos hallazgos encontrados y corregidos (ver abajo).
- [x] Verificado con HTTP real (no solo tests): `curl` contra `:8020`
      confirma que `/`, `/admin/catalogs/cycles` y
      `/admin/users/administrators` redirigen (302) sin sesión, y que
      `/login` renderiza los campos en español. El CRUD completo de
      administradores/docentes vía Livewire ya queda cubierto por
      `Fase2aAuthUsersTest` (que sí ejercita guardar/editar/eliminar
      contra la BD real, no mocks).
- [x] Intentar eliminar un `TEACHER`/`ADMIN` con dependencias se
      bloquea (probado contra `system_configuration.updated_by` como
      dependencia real) — cubierto en
      `test_cannot_delete_self_or_user_with_dependencies`.
- [x] `system-configuration.blade.php` ya no depende de
      `users.name` — usa `$config->updater?->displayName()`, confirmado
      compilando y mostrando el nombre correcto.

### Hallazgos de la revisión (Claude, verificación independiente)

Cursor dejó el código completo pero no llegó a correr `migrate`,
`test` ni la verificación HTTP (se cortó el contexto a mitad de la
tarea). Al correrlo yo mismo aparecieron 2 fallas reales,
**ninguna en la lógica de negocio de la Fase 2a**:

1. **Test mal adaptado** (`Fase1CatalogsTest::test_home_and_admin_pages_require_auth`):
   esperaba que `/admin/catalogs/unknown` redirigiera a `/login`, pero
   `whereIn('type', [...])` se evalúa en el *matching* de rutas, antes
   de que corra cualquier middleware — sin sesión y con un `type`
   inválido, Laravel da 404 igual, no redirige. Corregido: se restauró
   `assertNotFound()` (el comportamiento correcto, igual que en la
   Fase 1 original).
2. **Infraestructura de Fase 0** (no de esta fase): el `Dockerfile`
   compiló GD sin soporte JPEG (faltaba `libjpeg62-turbo-dev` +
   `--with-jpeg`). No se notó en Fase 1 porque nada generaba/validaba
   imágenes; acá `test_teacher_crud_has_no_password_and_optional_photo`
   usa `UploadedFile::fake()->image()` y lo expuso. Corregido en el
   `Dockerfile`, imagen reconstruida y verificada
   (`gd_info()['JPEG Support'] => 1`).

## 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 Fase 1.
- `document_type`/`document_number` como único compuesto — no
  `document_number` solo (eso es lo que hace `tutoria-fichas`, acá se
  decidió distinto, ver `../../CLAUDE.md` §2).
- Un `TEACHER` **nunca** tiene `password` — no generar una contraseña
  automática "por si acaso"; el campo se queda `null` y el formulario
  de docentes ni siquiera lo muestra.
- No agregar recuperación de contraseña por correo en esta fase (no
  hay servicio de mail configurado, no se pidió) — si un admin olvida
  su password, otro admin se lo resetea manualmente desde
  `/admin/users/administrators`.
- Regla transversal (`../../CLAUDE.md` §11): nunca eliminar
  físicamente algo con dependencias, ya cubierto por el patrón de
  `QueryException` 23000 que ya usan los componentes de Fase 1 —
  replicarlo igual acá, no inventar otro mecanismo.
