# SPRINT 1.5a-FIXES-3 · Cierre final

> **Para Ricci:** pegá este prompt en sesión NUEVA de Claude Code. Es el último sub-sprint antes del cierre oficial de Sprint 1.5a.

---

Hola Claude. Soy Ricci. Sprint 1.5a-fixes-2 + sprint-pdf-replica + sprint-pdf-no-cache cerrados. PDF de Innovium se ve idéntico al legacy de Infinia ✅. Ahora vamos al **último sub-sprint de fixes** antes del cierre oficial.

5 bugs encontrados en validación visual final. Mezcla de severidades, todos chicos pero importantes para que la app sea usable.

## Lectura obligatoria

1. `CLAUDE.md`
2. `app/Views/layouts/admin.php` (o equivalente del sidebar)
3. `routes.php` o equivalente con definición de rutas
4. `app/Services/Pdf/PDF.php`
5. `app/Services/PdfGeneratorNI.php`

## Los 5 bugs

### 🔴 GRAVES (2)

#### Bug #36 · Sidebar "ADMINISTRACIÓN" solo visible en URLs `/admin/*`

**Lo que pasa:** cuando un usuario `tenant_admin` (ej: `admin@demo.cl`) está en `/dashboard`, el sidebar **NO** muestra la sección ADMINISTRACIÓN. Solo aparece cuando navega a `/admin/categorias` u otra URL `/admin/*`.

**Impacto:** el admin queda atrapado en el dashboard sin poder navegar al panel. La única forma de salir es escribir URL a mano.

**Causa probable:** el layout del sidebar tiene un check basado en `$_SERVER['REQUEST_URI']` o similar — `if (str_starts_with($currentUrl, '/admin/')) { mostrar sección }`.

**Decisión:** **el sidebar debe mostrar ADMINISTRACIÓN siempre que el usuario tenga el permiso `admin.acceso`**, independiente de la URL actual.

**Fix:**

```php
// EN EL LAYOUT DEL SIDEBAR:

// ANTES (algo así):
@if (str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/'))
    <div class="sidebar-section">ADMINISTRACIÓN</div>
    <a href="/admin/categorias">Categorías</a>
    ...
@endif

// DESPUÉS:
@if (Auth::can('admin.acceso'))
    <div class="sidebar-section">ADMINISTRACIÓN</div>
    <a href="/admin/categorias">Categorías</a>
    ...
@endif
```

**Buscar en código:** cualquier referencia a `REQUEST_URI`, `currentUrl`, `request()->path()`, o similar dentro del layout del sidebar.

#### Bug #38 · Mover "Contratos" de ADMINISTRACIÓN a MÓDULOS

**Lo que pasa:** el sidebar tiene "Contratos" duplicado:
- En MÓDULOS aparece como "Contratos PRÓX." (deshabilitado)
- En ADMINISTRACIÓN aparece como "Contratos" (funcional)

**Decisión:** "Contratos" pertenece a MÓDULOS conceptualmente (operación del día a día), no a ADMINISTRACIÓN (que es configuración del catálogo y sistema).

**Layout final del sidebar:**

```
Innovium · Demo
─────────────────
MÓDULOS  (operativo)
  Contratos              ← funcional, accesible para vendedor + admin
  Cobranzas    PRÓX.
  Operaciones  PRÓX.
  Bodega       PRÓX.
  Reportes     PRÓX.
─────────────────
ADMINISTRACIÓN  (solo si tiene admin.acceso)
  Categorías
  Niveles
  Productos
  Planes
  Sucursales
  Velatorios
  Convenios
  Configuración
```

**Cambios concretos:**
1. Eliminar "Contratos PRÓX." de MÓDULOS
2. Eliminar "Contratos" de ADMINISTRACIÓN
3. Agregar "Contratos" funcional en MÓDULOS (link a `/admin/contratos`)
4. **Permisos:** la URL `/admin/contratos` debe ser accesible para vendedor también (verificar que el rol `vendedor` tenga el permiso correspondiente, sino agregarlo)

**No tocar:** las URLs siguen siendo `/admin/contratos`, `/admin/contratos/nuevo`, etc. Solo cambia dónde aparece el ítem en el sidebar y a quién le aparece.

### 🟡 MEDIO

#### Bug #37 · `/admin/` raíz devuelve 404

**Lo que pasa:** si alguien navega a `http://demo.innovium.test:8000/admin/` (sin path después), recibe `404 · No encontramos /admin`.

**Decisión:** redirigir `/admin/` y `/admin` (con o sin slash final) a la primera página admin disponible: `/admin/contratos` (ahora que es el módulo principal) o `/dashboard` si el usuario no tiene permiso admin.

**Fix en `routes.php` (o donde estén las rutas):**

```php
// Agregar:
$router->get('/admin', function () {
    if (Auth::can('admin.acceso')) {
        return Response::redirect('/admin/contratos');
    }
    return Response::redirect('/dashboard');
});
$router->get('/admin/', function () {
    return Response::redirect('/admin');
});
```

### 🟢 LEVES (2)

#### `utf8_decode()` deprecated en PHP 8.2

**Lo que pasa:** PHP 8.2 emite ~120 warnings por cada `utf8_decode()` en el código del PDF. La función se eliminará en PHP 9.

**Decisión:** crear helper estático `PDF::tx()` que use `iconv()` y reemplazar masivamente.

**Fix:**

```php
// app/Services/Pdf/PDF.php
class PDF extends FPDF
{
    /**
     * Convierte UTF-8 a ISO-8859-1 para FPDF, sin deprecation warnings.
     * Reemplazo de utf8_decode() (deprecated en PHP 8.2, eliminada en PHP 9).
     */
    public static function tx(string $text): string
    {
        return iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $text) ?: $text;
    }
    
    // ...resto de métodos
}
```

**Reemplazo masivo en 2 archivos:**
- `app/Services/Pdf/PDF.php` → cambiar `utf8_decode(` por `self::tx(`
- `app/Services/PdfGeneratorNI.php` → cambiar `utf8_decode(` por `PDF::tx(`

**Verificar import:** que `PdfGeneratorNI.php` tenga `use App\Services\Pdf\PDF;` arriba.

**Validación:**
```bash
grep -rn "utf8_decode" app/Services/Pdf/ app/Services/PdfGeneratorNI.php
# Esperás: 0 resultados
```

#### Posición de la firma muy arriba

**Lo que pasa:** la firma del contratante en el PDF aparece **arriba** del texto "RICCI RAFAEL DIAZ DIAZ" en lugar de estar **sobre la línea de firma** (la línea horizontal del bloque CONTRATANTE).

**Causa probable:** en `PdfGeneratorNI.php`, la línea que inserta `$this->Image($firmaPath, 135, $this->GetY() - 2, 60, 0, 'PNG')` tiene un `-2` en Y que mete la firma DEMASIADO arriba.

**Fix:** ajustar la posición Y para que la firma quede **justo encima de la línea de firma**, no flotando arriba del nombre.

**Buscar la línea:**
```bash
grep -n "Image" app/Services/PdfGeneratorNI.php | grep -i "firma"
```

**Ajustar el offset Y:** probar valores positivos (8, 10, 12) en lugar de `-2`. Iterar visualmente.

```php
// ANTES:
$this->Image($firmaPath, 135, $this->GetY() - 2, 60, 0, 'PNG');

// DESPUÉS (valor a ajustar visualmente):
$this->Image($firmaPath, 135, $this->GetY() + 8, 60, 0, 'PNG');
```

⚠️ **Probar 2-3 valores y elegir el que mejor coincide con el legacy.** Comparar con `docs/paquete-pdf-replica/PDF_INFINIA_REFERENCIA.pdf`.

## Plan de commits

Atómicos en orden:

1. `fix(sidebar): ADMINISTRACIÓN visible por permiso, no por URL (Bug #36)`
2. `fix(sidebar): mover Contratos a MÓDULOS, sacar de ADMINISTRACIÓN (Bug #38)`
3. `fix(routes): /admin/ redirige a /admin/contratos o /dashboard (Bug #37)`
4. `fix(pdf): utf8_decode → PDF::tx con iconv (PHP 8.2 ready)`
5. `fix(pdf): posición vertical de firma del contratante`
6. `chore: sprint 1.5a-fixes-3 cerrado`

**Total: 6 commits, ~45 min de trabajo.**

## Tests

Asegurarse que los tests existentes (~215) siguen verdes. Agregar tests específicos donde aplique:

```php
// tests/feature/SidebarVisibilityTest.php
public function testAdminVeAdministracionEnDashboard(): void
{
    $this->loginAs('admin@demo.cl');
    $response = $this->get('/dashboard');
    $this->assertStringContainsString('ADMINISTRACIÓN', $response->body());
    $this->assertStringContainsString('Categorías', $response->body());
}

public function testVendedorNoVeAdministracion(): void
{
    $this->loginAs('vendedor@demo.cl');
    $response = $this->get('/dashboard');
    $this->assertStringNotContainsString('ADMINISTRACIÓN', $response->body());
}

public function testVendedorVeContratosEnModulos(): void
{
    $this->loginAs('vendedor@demo.cl');
    $response = $this->get('/dashboard');
    // El sidebar debe mostrar el link a /admin/contratos
    $this->assertStringContainsString('href="/admin/contratos"', $response->body());
}

// tests/feature/AdminRedirectTest.php
public function testAdminSlashRedirigeAContratos(): void
{
    $this->loginAs('admin@demo.cl');
    $response = $this->get('/admin');
    $this->assertSame(302, $response->status());
    $this->assertStringContainsString('/admin/contratos', $response->header('Location'));
}

// tests/unit/PdfTxTest.php
public function testPdfTxConvierteUtf8AIso(): void
{
    $result = PDF::tx('Conchalí');
    // En ISO-8859-1, í es \xED
    $this->assertSame('Concha' . "\xED", $result);
}

public function testPdfTxNoEmiteDeprecation(): void
{
    set_error_handler(function ($severity, $message) {
        if ($severity === E_DEPRECATED) {
            throw new \Exception("Deprecated: $message");
        }
        return false;
    });
    
    try {
        PDF::tx('Texto con áéíóú ñ ¿¡');
        $this->assertTrue(true); // si llegamos acá, no hubo deprecation
    } finally {
        restore_error_handler();
    }
}
```

## Antes de tocar código · 6 confirmaciones

1. ¿Confirmás que el sidebar muestra ADMINISTRACIÓN basado en **permiso del usuario**, no en URL actual?
2. ¿Confirmás mover Contratos de ADMINISTRACIÓN a MÓDULOS (1 sola entrada, funcional, accesible para vendedor + admin)?
3. ¿Confirmás que `/admin/` redirige (302) a `/admin/contratos` o `/dashboard` según permisos?
4. ¿Confirmás que `PDF::tx()` reemplaza TODOS los `utf8_decode()` en los 2 archivos del PDF?
5. ¿Confirmás iterar la posición Y de la firma comparando visualmente con el PDF de Infinia hasta que quede sobre la línea?
6. ¿Confirmás trabajar en orden: bug #36 → #38 → #37 → utf8 → firma → cierre?

Cuando respondas las 6 explícitamente, esperás OK de Ricci antes de empezar.

## Tiempo estimado

- Trabajo Claude Code: **~45 min**
- Validación Ricci entre fixes: **~15 min**
- **Total: ~1 hora**

## Cierre · Definition of Done

NO consideres este sprint cerrado hasta que:

1. Los 215 tests anteriores sigan verdes
2. Los nuevos tests (~5) pasen
3. Ricci valide visualmente:
   - Sidebar correcto en `/dashboard` (admin ve todo, vendedor solo módulos)
   - `/admin/` redirige
   - PDF se genera sin warnings
   - Firma del PDF en posición correcta
4. CLAUDE.md actualizado con el cierre

¡Vamos! 🚀
