# Migración al layout nuevo de Tailwind

**Estado:** propuesta · 23/10/2026
**Norma visual:** [layout.md](layout.md) (tokens, componentes, navegación). Este documento es el **plan** para llegar a ella en este repositorio.
**Prerrequisito:** fase 7 de [MigracionLaravel10.MD](MigracionLaravel10.MD) (Livewire 3) cerrada. El layout usa `wire:navigate`, `app/Livewire` y la inyección de Alpine de Livewire 3.
**Orden (decidido 23/10/2026):** fase 7 → **este plan** → fases 8 y 9 (Laravel 11 y 12).

---

## 1. Objetivo

1. Las vistas Bootstrap (`layouts.planti`) se reescriben con Tailwind según `layout.md`. Tablas y formularios usan los tokens y los componentes `x-*`.
2. Los componentes Livewire que hoy se pintan en un iframe pasan a pintarse directamente dentro del layout.
3. Al terminar no queda Bootstrap, ni iframes de contenido, ni clases armadas con `session('color…')`.

### 1.1 Decisiones (23/10/2026)

| # | Decisión |
|---|---|
| D1 | La infraestructura del layout **se construye desde `layout.md`**. No hay código previo que portar. |
| D2 | **Se incluye el selector de empresa (Alt + S).** Solo aparece en los grupos empresariales con más de una compañía, y solo para los usuarios cuyo rol lo habilite. **Preselecciona el filtro de compañía de cada módulo; no restringe consultas** (decidido 24/10/2026, §4). |
| D3 | El plan arranca **después de la fase 7** y antes de las fases 8 y 9. |
| D4 | **Los permisos conviven en dos modelos**, cada uno con una tarea distinta: perfiles + `aplicaciones` deciden qué *pantallas* ve un usuario (como hoy), y spatie decide qué *acciones* puede hacer (§5). |

### 1.2 Reglas del plan

1. **El aislamiento por tenant no cambia.** `session('GrupoEmpresarial')` sigue saliendo del subdominio y **ninguna** pieza del layout lo modifica. El selector de empresa solo *reduce* el conjunto de compañías dentro del grupo (§4).
2. **El sitio nunca queda a medias.** Desde L1 todo el sitio usa el shell nuevo. Las pantallas que aún no se migran siguen funcionando en un iframe *dentro* del shell nuevo (§3, L1).
3. **Una pantalla sale del iframe solo cuando no le queda nada de Bootstrap**, ni CSS ni JS. El *preflight* de Tailwind y Bootstrap no conviven en el mismo documento.
4. **Gemelos juntos.** Una pantalla de importaciones y su gemela de exportaciones se migran en la misma tarea (CLAUDE.md §7).
5. **Sin cambios funcionales.** Mismos filtros, mismas columnas, mismas exportaciones y mismos permisos. Solo cambia la presentación.
6. **Sin `migrate`.** Los cambios de esquema y de datos (columna `modulos_neutros`, permisos nuevos, `menus.ruta`) se entregan como SQL en este documento y los aplica a mano el responsable (CLAUDE.md §3).

---

## 2. Punto de partida (23/10/2026)

### 2.1 Lo que `layout.md` da por hecho y aquí no existe

| Pieza | Estado |
|---|---|
| `layouts/app.blade.php` | Existe, pero es el layout viejo de laravel/ui (Bootstrap). Se reemplaza. |
| `partials/tema-empresa`, `TemaEmpresa`, `PaletaAcento`, `MenuNavegacion`, `config/modulos.php` | No existen. |
| Componentes `x-app-layout`, `x-button`, `x-secondary-button`, `x-danger-button`, `x-input`, `x-label`, `x-input-error`, `x-checkbox`, `x-dialog-modal`, `x-confirmation-modal`, `x-icono-modulo` | No existen. Los que hay (`button-primary`, `input-search`…) son los que `layout.md` §6.4 prohíbe. |
| Tailwind compilado | No hay. Solo el CDN JIT en `layouts.tailwind`. El proyecto compila con Mix y no tiene `tailwind.config.js`. |
| `grupo_empresarial.color_p` / `color_s` | ✅ Existen con datos. |
| `grupo_empresarial.modulos_neutros` | ❌ No existe. |
| Paquete de roles y permisos | Las tablas de spatie existen (`roles`: `admin`, `directivo`; 25 `permissions`; 6 filas en `model_has_roles`), pero **el paquete no está instalado** y ningún código las consulta. `users.rol` es texto libre. |

### 2.2 Tamaño

| Qué | Cantidad |
|---|---|
| Vistas sobre `layouts.planti` | 306 |
| Vistas contenedoras con iframe | 51 |
| Vistas sobre `layouts.tailwind` | 8 |
| Vistas Livewire (con clases Bootstrap) | 176 (49) |
| Vistas con JS atado a jQuery o Bootstrap (`.modal()`, DataTables, select2, `data-toggle`) | 140 |
| Vistas con clases armadas con `session('color…')` | 29 |
| Menú activo | 4 módulos (nivel 1), 12 grupos (nivel 2), 153 enlaces (nivel 3) |
| Sitios que calculan las compañías del grupo por su cuenta | 80 archivos (+12 que leen `session('companias')`) |

### 2.3 Ajustes a `layout.md` para este repositorio (aplicados el 23/10/2026)

| Sección | Ajuste |
|---|---|
| §7.1 | "Solo se muestran filas con `estado = 1`" **no basta**. `MenuNavegacion` debe conservar los filtros del sidebar actual: `id IN compania.aplicaciones`, `perfiles` (`JSONARRAY_VERIFY_DATA` por perfil y por usuario), **ni uno más**. Las aplicaciones salen de la primera compañía activa. `regimen` e `id_grupo_empresarial` no filtran hoy, y añadirlos cambiaría lo que ve cada cliente (corregido 24/10/2026). |
| §7.2 | Además de insertar la fila de nivel 3, hay que añadir su `id` a `compania.aplicaciones` (CLAUDE.md, "Cómo agregar un módulo"). |
| §8 | Los permisos `empresa-personalizar` y `empresa-seleccionar` se crean con SQL (tarea L0.7), no con migraciones. |
| §11 | Las "migraciones sin correr" se sustituyen por el SQL de L0.7. |
| §1 (selector) | El selector elige **compañías dentro del grupo**; nunca cambia `GrupoEmpresarial` (§4). |

---

## 3. Fases

| Fase | Contenido | Estimado\* |
|---|---|---|
| L0 | Base: Tailwind compilado, tokens, layout, componentes, menú, tema de empresa, roles | 8–12 días |
| L1 | Shell nuevo en todo el sitio, con las pantallas viejas en iframe dentro del shell | 3–5 días |
| L2 | Componentes Livewire que ya son Tailwind salen del iframe | 5–8 días |
| L3 | Vistas Bootstrap a Tailwind, módulo por módulo | 35–60 días |
| L4 | Selector de empresa: preselección del filtro de compañía en unos 12 componentes | 2–3 días |
| L5 | Limpieza: quitar Bootstrap, `planti`, `tailwind`, claves de sesión heredadas | 2–3 días |

\* Días de desarrollo de una persona, sin la validación de los usuarios. **Total aproximado: 55–91 días.** L3 domina y se puede repartir. L4 puede ir en paralelo con L2 y L3.

### Fase L0 — Base

| # | Tarea |
|---|---|
| L0.1 | Tailwind compilado con **Vite** (Laravel 10 lo trae; se retira Mix). `tailwind.config.js` con `content` sobre `resources/views/**` y `app/Livewire/**`, `darkMode: 'class'`. El CDN deja de usarse en producción. |
| L0.2 | `partials/tema-empresa.blade.php`: tokens de `layout.md` §3 para `:root` y `:root.dark`; fuentes Plus Jakarta Sans y Archivo. |
| L0.3 | `App\Support\PaletaAcento`: rampa de `--acento-*` y `--secundario-*` a partir de un hex, con contraste AA en claro y oscuro (`--acento-texto` ≥ 4.5:1; `--acento-contraste` blanco o casi negro). Se prueba con los colores reales de `grupo_empresarial`, incluido el amarillo `#FCB404`. |
| L0.4 | `App\Services\TemaEmpresa`: lee `color_p`, `color_s` y `modulos_neutros` del grupo de la sesión, con caché `tema-empresa:{id}` de 10 minutos y borrado al guardar `GrupoEmpresarial`. |
| L0.5 | `App\Services\MenuNavegacion`: árbol módulo → grupo → enlace con los filtros de §2.3. La consulta del sidebar actual (que hoy concatena valores en el SQL) pasa a parámetros enlazados. `config/modulos.php` con las 4 llaves y `panel`. |
| L0.6 | `layouts/app.blade.php` nuevo (encabezado, barra de módulos, sidebar, menú de usuario, modo oscuro) y los componentes `x-*` de §2.1. `config/livewire.php` → `layouts.app`. Alpine lo pone Livewire 3, sin CDN aparte. |
| L0.7 | Roles: instalar `spatie/laravel-permission` compatible con las tablas existentes (verificar columnas antes de instalar) y `HasRoles` en `App\Models\User`. SQL para la columna `modulos_neutros` y los permisos `empresa-personalizar` y `empresa-seleccionar` (§7). Alcance según §5: solo acciones, no pantallas. |
| L0.8 | `ColorEmpresa` (modal de colores), solo con `empresa-personalizar`. La escritura en `grupo_empresarial` se prueba únicamente con el grupo 2. |

**Salida:** una ruta de muestra (`/layout/muestra`) con todos los componentes en claro y oscuro, revisada con los grupos 1, 2 y 3 y con un primario claro.

#### Avance de L0 (24/10/2026, rama `feature/layout-tailwind`)

| # | Resultado |
|---|---|
| L0.1 | Vite 5 + Tailwind 3.4 + `@tailwindcss/forms`. Entradas: `resources/css/ctc.css` y `resources/js/ctc.js`. Se retiró Mix (`webpack.mix.js`); `public/css/app.css` y `public/js/app.js` siguen como archivos ya compilados para las vistas antiguas. Sale en **`public/ctc`** (no en `public/build`, que ya usa el tema de `planti` y Vite vaciaría), y **se versiona**, porque el despliegue es por subida de archivos (ver MigracionLaravel10.MD §6): se ejecuta `npm run build` antes de cada commit que toque vistas del layout. |
| L0.2 | `partials/tema-empresa.blade.php`: tokens claro/oscuro, colores de módulo por `[data-mod]` y "Módulos en gris". |
| L0.3 | `App\Support\PaletaAcento`: rampa con contraste AA. Probada con #FCB404 (AVGUST), #15803D, #BE123C, #1A3A95 y #000000: `--acento-texto` cumple 4.5:1 en los dos modos. |
| L0.4 | `App\Services\TemaEmpresa` (caché `tema-empresa:{id}`, 10 min). Reutiliza `PaletaTenant::normalizarHex`, porque `color_p` trae espacios y valores vacíos. Los dos modelos `GrupoEmpresarial` borran la caché al guardar. También da `razon_social`: la sesión no siempre la trae. |
| L0.5 | `App\Services\MenuNavegacion` con los filtros exactos del sidebar actual, con parámetros enlazados. Con el usuario 14 del grupo 2 muestra las mismas opciones que el sidebar actual; la única diferencia es un grupo sin enlaces (Configuración de Exportaciones), que se oculta. |
| L0.6 | `layouts/app.blade.php` nuevo y componentes `x-app-layout`, `x-button`, `x-secondary-button`, `x-danger-button`, `x-input`, `x-label`, `x-input-error`, `x-checkbox`, `x-modal`, `x-dialog-modal`, `x-confirmation-modal` y `x-icono-modulo`. El layout viejo de laravel/ui pasó a `layouts/app-ui` (7 vistas de auth). El menú lateral es Blade + Alpine dentro del layout, no un componente Livewire. **`wire:navigate` no se activa hasta L5**: navegar sin recargar hacia una pantalla Bootstrap la rompería. |
| L0.7 | `spatie/laravel-permission` 6.25 sobre las tablas existentes (mismo esquema, sin migraciones). `HasRoles` en `App\User`, que es el modelo de autenticación: **los roles se asignan con `model_type = 'App\User'`**. Las 6 filas heredadas usan `App\Models\User` y no aplican. Pendiente: el SQL de §7 y asignar roles (P1). |
| L0.8 | `Layout\ColorEmpresa`: solo con `empresa-personalizar`, comprobado al abrir y al guardar. El grupo sale de la sesión al guardar. Sin probar la escritura (no hay roles asignados todavía). |
| Muestra | `/layout/muestra` (`Layout\Muestra`, página completa Livewire sobre `layouts.app`, datos fijos de ejemplo). Revisada con capturas en claro, oscuro, móvil (390 px) y con el diálogo abierto, con el grupo 2 (#BE123C) y con AVGUST (#FCB404). Sin errores de JavaScript. |

### Fase L1 — Shell nuevo con convivencia

| # | Tarea |
|---|---|
| L1.1 | Una vista puente: dentro de `layouts.app`, un iframe sin bordes que ocupa todo `<main>` y carga la pantalla vieja sin el shell de `planti` (una variante `planti-contenido` que solo trae CSS y JS de Bootstrap). |
| L1.2 | Las rutas de menú apuntan al shell nuevo. El iframe se ajusta al alto de su contenido y los enlaces internos navegan el documento padre (`target="_top"` o `postMessage`). |
| L1.3 | El iframe recibe el modo oscuro y los colores por parámetro, para que la convivencia no se vea rota (aunque esas pantallas no tengan modo oscuro todavía). |
| L1.4 | El sidebar viejo (`Plantilla\Sidebar`) se retira cuando todas las rutas pasan por el shell. |
| L1.5 | Control de acceso por ruta **en modo registro** (§5.3, paso 1): un middleware con la misma lógica de `MenuNavegacion` anota en el log los accesos que negaría, sin negarlos. |

**Salida:** todo el sitio con la navegación nueva y ninguna pantalla perdida. La prueba de humo debe dar 0 regresiones.

#### Avance de L1 (24/10/2026)

| # | Resultado |
|---|---|
| L1.1 | **`/marco?u=<path>`** (`resources/views/layout/marco.blade.php`): el layout nuevo con un iframe que ocupa todo `<main>` y tiene su propio scroll. No se creó una variante `planti-contenido`: el mismo `planti` tiene un **modo solo contenido**. Dentro de `/marco` oculta el sidebar, la barra superior y el pie, y conserva scripts y modales. Con HTTPS lo decide el servidor (`Sec-Fetch-Dest: iframe`) y ni siquiera arma el menú viejo; sin esa cabecera lo hace un script del `<head>` antes del primer pintado. `u` solo admite un path local: una URL externa, `javascript:` o `/marco` dentro de `/marco` caen en `/home`. |
| L1.2 | Los enlaces del menú nuevo van a `/marco`, igual que el Dashboard y "Cambiar mi contraseña". Al navegar dentro del iframe se actualizan la URL (`history.replaceState`), el título y el enlace marcado, y se abre su grupo y su módulo. **No hace falta ajustar el alto del iframe:** ocupa todo el contenido y hace su propio scroll. Un enlace con `target="_top"` sale del layout. |
| — | **Middleware `EnvolverEnMarco`:** una pantalla del menú (o `home` y `user/password`) abierta directo se redirige a `/marco` antes de ejecutar el controlador, así que no corre dos veces. Solo con `Sec-Fetch-Dest: document`, que los navegadores mandan **solo por HTTPS**. Por HTTP, una pantalla abierta directo (un marcador, por ejemplo) se ve como hoy. `?sin_marco=1` la abre sin el layout nuevo. Las subpantallas, exportaciones, PDF y la app móvil no se tocan. |
| — | **Todas las pantallas Bootstrap dentro del layout (24/10/2026).** Las subpantallas, las ventanas emergentes y las respuestas de formularios también se envuelven. `planti` lleva la marca `<meta name="ctc-planti">`. Con HTTPS, `EnvolverEnMarco` revisa la respuesta después del controlador: si es una página de planti pedida en la ventana principal, guarda su HTML (caché de 5 minutos, ligado a la sesión) y devuelve `/marco` con un iframe que lo pide una sola vez (`?__ctc_marco=<token>`). Así la pantalla no se ejecuta dos veces y funciona también con POST. Por HTTP lo hace un script de planti, solo para GET; esa pantalla sí se ejecuta dos veces. Descargas, JSON, la app móvil y las pantallas ya migradas no llevan la marca y no se tocan. El login de escritorio entra directo a `/marco?u=/home`. |
| — | **Botón para mostrar y ocultar el menú lateral** en el encabezado. En escritorio lo oculta y se recuerda (`localStorage.lateral_oculto`); en móvil abre el cajón. Elegir un módulo en la barra lo vuelve a mostrar. |
| — | El login y el layout nuevo salen a la ventana superior si se cargan dentro de un iframe: la sesión vencida no deja el login dentro del marco, ni un layout dentro de otro. |
| L1.3 | No hecho. Dentro del modo oscuro, las pantallas Bootstrap se siguen viendo claras; se resuelve al migrarlas en L3. |
| L1.4 | El sidebar viejo sigue en `planti` para las pantallas abiertas fuera de `/marco`. Se retira en L5. |
| L1.5 | **`RegistrarAccesoRuta`** anota en `storage/logs/acceso-*.log` (60 días) cada página que un usuario abre fuera de su menú, sin negarla: `es_menu: true` si es un enlace del menú de otra empresa o perfil, `false` si no está en ningún menú. Se apaga con `CONTROL_ACCESO_RUTAS=off`. |

**Probado:**
- `rpo`, `home` y `rkpi` en Chrome dentro de `/marco`: se ven sin el sidebar viejo, con el enlace marcado y sin errores nuevos de JavaScript. El error de `rkpi` ("Invalid or unexpected token") ya existía: viene del script de impresión de `KPI/index.blade.php`, que no cambió.
- El middleware y la validación de `u`, con 10 casos simulando las cabeceras de HTTPS.
- El registro de accesos, con una ruta del menú propio, una del menú de otra empresa y una fuera de todo menú.

**Pendiente de comprobar en el servidor con HTTPS:** la redirección directa a `/marco`, porque Herd sirve el sitio local por HTTP.

### Fase L2 — Livewire fuera del iframe

| # | Tarea |
|---|---|
| L2.1 | Los componentes que hoy usan `layouts.tailwind` se pintan directamente en `layouts.app`. Se quita la ruta del iframe (o redirige a la nueva) y se actualiza `menus.ruta`. |
| L2.2 | Colores fijos y `session('color…')` pasan a tokens (`layout.md` §10). |
| L2.3 | Scripts en línea y `$(document).ready` pasan a `livewire:navigated`, `@script` o Alpine, para que funcionen con `wire:navigate`. |
| L2.4 | Gráficas: los colores se leen de los tokens en el navegador (`layout.md` §6.8), incluidas las de `App\Charts\Base`, y se repintan al cambiar el modo. |

#### L2 se divide en dos (decidido 24/10/2026)

Las pantallas de iframe usan 803 clases armadas con el color de la sesión (`text-{{ $color }}-700`, en 84 archivos), cargan sus librerías por CDN y no están preparadas para el modo oscuro. Rediseñarlas a tokens al mismo tiempo que se sacan del iframe sería hacer L3 a ciegas. Por eso:

- **L2a (mecánica, todas):** la pantalla sale del iframe con **el mismo aspecto que hoy**. Ver el avance abajo.
- **L2b (L2.2 a L2.4):** rediseño a tokens, modo oscuro y gráficas con tokens, pantalla por pantalla junto con L3. `L2.3` se hace al activar `wire:navigate` en L5: hasta entonces cada pantalla carga completa y los `$(document).ready` siguen funcionando.

#### Avance de L2a (24/10/2026)

**Infraestructura:**
- **Familia de color `empresa`:** `text-empresa-700`, `bg-empresa-100`… La escala 50–950 sale del primario (`PaletaAcento::escala`) en variables CSS. Los componentes migrados ponen `$this->color = 'empresa'` en lugar de `session('color')`. La familia completa va en el `safelist` de Tailwind, porque las vistas la arman con `{{ $color }}`.
- **`@tailwindcss/forms` con `strategy: 'class'`:** solo estiliza los campos con `form-input`/`form-select`/`form-checkbox` (los `x-*` nuevos). Los campos de las pantallas heredadas se ven como antes.
- **`<x-app-layout heredado>`:** modo claro fijo (sin botón de tema), las librerías de `layouts.tailwind` (`layout.librerias-heredadas`: jQuery, select2, Highcharts, SweetAlert 1 y 2, toastr) y el partial de eventos del navegador.
- **`config('ctc.pantallas_layout')`:** lista de paths migrados. El menú los enlaza directo y `EnvolverEnMarco` no los toca.

**Pantallas:** 35 pasan a `resources/views/pantallas/…`:
- **Piloto:** Pedidos en curso, hecha a mano.
- **Resto:** 34 con un script que extrae el `<body>` (o la sección `content`) de la vista del iframe y deja en el `<head>` sus estilos y scripts propios. Quita las librerías ya incluidas, pasa los colores de los componentes (y de sus hijos) a `empresa` y apunta las dos rutas, contenedor y contenido, a la vista nueva.
- **Retiradas:** las 68 vistas viejas, contenedor y contenido.
- **Ajustes a mano:**
  - RIM v2 y PO v2 traían reglas de `html, body` pensadas para el iframe; pasaron a un contenedor propio.
  - Se quitaron 4 `@stack('scripts')` que habrían duplicado los scripts.
  - `TorreControlController` (perfil 20) apuntaba al contenedor `pedido-curso`.
- **Corrección de paso:** tres vistas se pedían como `expo.*` y viven en `ExPo/`. En Linux, que distingue mayúsculas, no se habrían encontrado; las nuevas usan la misma ruta en los dos lados.

**Segunda tanda (24/10/2026):**
- **Qué faltaba:** las subpantallas Tailwind a las que llevan los botones de acción de las pantallas de L2a: seguimiento y campos dinámicos del DO (importación y exportación), pendientes, históricos, formularios de orden de compra y de productos, seguimiento de contenedor y reportes.
- **Por qué:** antes se abrían dentro del iframe del contenedor; al sacar su pantalla del iframe en L2a quedaban sueltas, fuera del layout. Eran documentos HTML completos con Tailwind por CDN.
- **Conversión:** 24 vistas convertidas igual que en la primera tanda, más 6 contenedores sencillos (reportes globales de importación y Brenntag, recobro, recobro de bodegaje, consulta de fecha BL y declaraciones juramentadas).
- **Controladores:** `IndicadoresController` y `ReporteControlContenedoresController` apuntan a las vistas nuevas.
- **Seguridad:** 13 de esas rutas no tenían `->middleware('auth')` y ahora lo tienen. Antes se podían abrir sin sesión, aunque sin datos del grupo. Entre ellas: `seguimiento-imp`, `dinamicos-imp`, sus gemelas de exportación y varios reportes.
- **Prueba de humo:** 200, salvo tres que ya fallaban igual en la línea base porque necesitan parámetros (`impo/purchase-procesar`, `config/productos-procesar`, `charts`).
- **En Chrome:** el seguimiento del DO abierto desde Pedidos en curso queda dentro del layout.

**Fuera de L2a, quedan para L3:**
- **Contenedores con parámetros o closures:** Dashboard v2 y pendientes de pedido.
- **Páginas Bootstrap que incrustan iframes:** Torre de Control, KPI, `iframe.index` y `menuws`.

**Pruebas:**
- **Prueba de humo de las 35:** todas como en la línea base, salvo `pedido/proceso`. Antes daba 200 porque solo pintaba el contenedor; su contenido (`pedido/procesows`) ya pasaba el límite de 120 s en la línea base, y ahora esa consulta corre en la misma página. Para el usuario no cambia.
- **En Chrome:** Pedidos en curso (también el botón Consultar, que responde 200), Dashboard, PO v2 y RIM v2, sin errores de JavaScript.
- **Ojo:** el CDN de Highcharts responde 403 a navegadores sin interfaz ("HeadlessChrome"). Las pruebas automáticas usan el agente de Chrome normal.

### Fase L3 — Vistas Bootstrap

Se migra por módulo (Importaciones, Exportaciones, CIA, Administración y Dashboard). Dentro de cada módulo se empieza por los enlaces más usados. Por cada pantalla y su gemela:

1. Plantilla de página `layout.md` §5, tablas §6.3, formularios §6.4 y estados §6.6.
2. Modales Bootstrap a `x-dialog-modal` o Alpine (§6.5). DataTables y select2 se reemplazan o se envuelven con estilos de tokens. Se decide una sola vez, en la primera pantalla que los use, y se documenta aquí.
3. La pantalla sale del iframe puente y se actualiza su `menus.ruta`.
4. Se pasa la lista de revisión de `layout.md` §10 y su fila de [MigracionListaPruebas.MD](MigracionListaPruebas.MD).

#### Cómo se avanza en L3 (24/10/2026)

- **Orden por uso real.** El menú registra cada clic en `seguimiento_apps`. En los últimos 180 días, las pantallas Bootstrap más usadas son:

  | Pantalla | Clics |
  |---|---|
  | `/rpo` | 3.358 |
  | `/rexpo` | 1.515 |
  | Archivo digital | 1.196 |
  | `/rtorrecontrol` | 914 |
  | Productos Websimex | 840 |
  | `/rreport` | 678 |
  | `/rhistorico` | 664 |

  Se convierten en ese orden, cada una con su gemela.
- **Por familias:** una pantalla, sus subpantallas y los componentes Livewire que incrusta. Un componente incrustado con modales Bootstrap no puede verse bien en los dos layouts a la vez.
- **Lotes:** primero las pantallas de consulta. Crear y editar van en un lote aparte, porque escriben en producción y necesitan la prueba de escritura del responsable con el grupo 2.
- **Convivencia:** `pantalla($url)` (en `JMetodos`) da el enlace directo si la pantalla ya está migrada y uno por `/marco` si sigue en Bootstrap. `config('ctc.pantallas_layout')` admite prefijos (`'searchpo*'`).
- **Kit** (`resources/views/components`): `x-encabezado-pagina`, `x-tarjeta`, `x-campo`, `x-select`, `x-alerta`, `x-enlace-boton`, `x-boton-icono` y la paginación `vendor.pagination.ctc`.
- **Avisos globales:** los avisos SweetAlert que estaban dentro de `planti` ahora están en `partials.eventos-planti`, y `planti` y el layout nuevo los incluyen. El layout nuevo carga SweetAlert 1 y 2. jQuery no se carga: las vistas convertidas usan Alpine y `fetch`.
- **Tailwind:** cada carpeta convertida se añade a `content` en `tailwind.config.js`. Escanear todas las vistas compilaría las clases de Bootstrap.

#### Lote 1: Orden de compra de importación, consulta (24/10/2026)

**Convertido:**
- **Vistas:**
  - `Po.index`, usada por `rpo`, `searchpo*`, `rposearchout*` y por `POController@show`. Esta última solo le pasa `$po`; ya fallaba y ahora tolera los datos ausentes.
  - `Po.historico` (`rhistorico`).
  - `Po.anular` (`rpoviewinac`).
  - La tabla de columnas dinámicas, compartida en `Po._tabla`.
- **Componentes:** `po.facturacion` (diálogo de facturas y gastos por fila), `po.actualizar-orden-compra` y `po.linea-orden-compra-act`.

**Cambios de comportamiento:**
- **Ninguno en los datos.** Mismos campos, nombres, acciones y rutas.
- **Interfaz:**
  - Los modales Bootstrap pasan a Alpine.
  - El acordeón de facturas y gastos pasa a pestañas.
  - "Actualizar fechas" recorre las rutas con `fetch`, una tras otra, en lugar de `$.ajax` síncrono, y muestra el avance.
- **Se quitó código muerto:** el modal de documentos y su manejador (`.openModal`, que ningún elemento usaba) y los estilos para ocultar DataTables.

**Probado:**
- Prueba de humo de `rpo`, `searchpo` y `rhistorico`: 200.
- En Chrome, en claro y oscuro: el listado, el diálogo de facturas (abrir y cerrar) y la sincronización (consulta de pendientes, 200). Sin errores de JavaScript.

**Convención de colores (24/10/2026):**
- Cada fila se marca entera con el color de su estado (`COLORCON` o `COLOR`): barra lateral y fondo suave.
- Las filas sin estado en los datos no se marcan, igual que antes.
- **Verificado por el responsable con Tenaris.**

**Pendiente de prueba de escritura (responsable, grupo 2):**
- Agregar, editar y eliminar una factura y un gasto.
- "Actualizar orden de compra" en la sincronización.
- "Actualizar fechas".
- Anular una orden.

**Ya existía y se mantiene:**
- `eliminarFactura`/`eliminarGastoPo` se escuchan en todos los componentes de la página: cada fila marca inactivo el mismo registro.
- `FacturacionPo::find($id)` no filtra por grupo empresarial (H13).

**Sigue en Bootstrap (lote 2):** `rpo/create` y `rpo/{id}/edit` (`Po.create`, 948 líneas; `Po.edit`, 2.284). Desde el listado se abren por `/marco`.

#### Lote 2: Orden de cargue de exportación, consulta (24/10/2026)

Gemela del lote 1.

**Convertido:**
- **Vistas:** `ExPo.index` (`rexpo`, `searchexpo*`, `rexposearchout*`), `ExPo.historico` (`rexhistorico`) y `ExPo.anular` (`rexpoviewinac`). La tabla, compartida en `ExPo._tabla`.
- **Componente:** `ex-po.ex-po-facturacion`. Su diálogo y el de importación salen de un mismo parcial (`livewire/partials/facturacion-orden`); el grupo 28 agrega número y fecha de BL y "Related Parties".

**Diferencias que se conservan respecto de importación:**
- El listado no colorea filas (en Bootstrap tampoco: la leyenda y el color estaban comentados). El histórico sí, con el mismo estilo de fila marcada de `/rpo`.
- Los enlaces de rastreo que vienen en los datos (`http…`) se muestran como botón.
- Ver orden (PDF/XLS) y notificar por correo solo con `operacion_ex = 'E'`. Antes sus encabezados salían siempre y las celdas no: la tabla quedaba desalineada.

**Corregido de paso:** en el listado Bootstrap, las celdas incluían `COLOR` y `COLORCON` pero los encabezados no, y las columnas se corrían. Ahora se omiten en los dos.

**Rendimiento (R2):**
- Facturas, conceptos y gastos se consultan al abrir el diálogo.
- La sincronización de facturas desde Websimex pasó al momento de abrir el diálogo (decidido el 24/10/2026). `/rexpo` quedó en 15 consultas.
- `/rexpo` (Tenaris) pasó de **116 a 65 consultas** y de 19,7 s a 11,5 s en local. Lo que queda es esa sincronización: una consulta a la orden y otra a Websimex por fila.

**Probado:**
- Las cuatro pantallas responden 200, en un solo proceso.
- En Chrome: el listado, el histórico y el diálogo de facturas, que carga al abrir. Sin errores de JavaScript.

**Pendiente de prueba de escritura (responsable, grupo 2):** las mismas acciones del lote 1, en exportación, más el envío de la orden por correo.

#### Lote 3: Archivo digital (24/10/2026)

**Convertido:**
- `Documentos.archivodigital` y el componente `documentos.archivodigital`: filtros, tabla, descarga en ZIP y comentarios de revisión.
- La ventana de comentarios es de Alpine; antes era un modal Bootstrap con jQuery.

**Seguridad:** los comentarios se insertaban como HTML sin escapar (`innerHTML`), así que un comentario con HTML o script se ejecutaba en el navegador de quien lo abría. Ahora se muestran como texto.

**Rendimiento:**
- **Qué pasaba:** el filtro traía **todos** los pedidos del importador con todos sus documentos y aplicaba el rango de fechas en PHP; en la consulta estaba comentado. Con Tenaris agotaba la memoria y la pantalla no abría.
- **Fecha de apertura:** el rango se filtra ahora en la consulta de pedidos.
- **Fecha de subida:** se filtran los documentos que se cargan. Están en otra base (`cti`), así que no se pueden filtrar los pedidos por ellos en la misma consulta.
- **Estado del componente:** la lista ya no se guarda en él; se calcula en cada render, como antes. El ZIP vuelve a calcularla con los filtros actuales.
- **Resultados comparados** con la versión anterior, con los mismos parámetros:
  - grupo 2, un mes y 2023–2026: idénticos;
  - Tenaris, septiembre–octubre: los mismos 3.413 y 4.613 documentos. En el modo por apertura cambia el orden, porque ninguna de las dos consultas tiene `ORDER BY`.
- Tenaris por apertura: de 11,1 s a 2,0 s.
- **Pendiente:** la tabla no se pagina (como antes). Con Tenaris son ~1.200 filas por mes y 4 MB de HTML.

**Ya existía y se mantiene:** `guardarComentario` busca el documento por id sin comprobar que sea del grupo (H13).

**Probado:**
- 200 con el grupo 2 y con Tenaris.
- En Chrome, en modo oscuro, con la ventana de comentarios abierta (sin guardar). Sin errores de JavaScript.

**Pendiente de prueba (responsable):** guardar un comentario y descargar el ZIP.

#### Lote 4: Torre de Control, importación y exportación (24/10/2026)

**Convertido:**
- `TorreControl.index` (`rtorrecontrol`) y su gemela `ExTorreControl.index` (`rtorrecontrol_ex`, controlador `Ex_TorreControlController`).
- **Partes de cada pantalla:**
  - indicadores por estado, con el color de categoría como barra lateral;
  - por cada estado, su tabla y su gráfica (Chart.js 2, el mismo que cargaba `planti`), con los textos y líneas de la gráfica tomados de los tokens;
  - en exportación, la gráfica general cuando `grafica_global = SI`;
  - el grupo 27 sigue viendo la tabla dinámica de Central en un iframe.

**Comportamiento que se conserva:**
- El perfil 14 no ve la torre y el perfil 20 va a pedidos en curso.
- El doble clic lleva a `rposearchout/{estado}/{valor}` en importación y abre `/rexdatosordenes/{estado}` en exportación.

**Cambia:**
- El "Generar reporte" de exportación imprimía desde una ventana emergente con estilos Bootstrap; ahora imprime la página.
- El layout tiene estilos de impresión: oculta menú, encabezado y barra de módulos.
- Se quitó código muerto: `PrintElem`/`Popup` en importación (ningún botón lo llamaba) y un JSON de ejemplo que el controlador asignaba y nunca usaba.

**Rendimiento (R5, parcial):**
- **Qué pasaba:** el controlador de importación registraba en Central una tabla dinámica por estado, **para todos los clientes**, con un POST tras otro: 9 s en local con Tenaris. Solo el grupo 27 las ve.
- **Ahora:** se piden solo para el grupo 27, y en paralelo (`Http::pool`). Tenaris pasó de 9,3 s a 0,08 s fuera de SQL.
- **Lo que queda:** los 32 s de SQL, que son las consultas configuradas por cliente en `definicion_control_tower` (`sql_nro` y `sql_detalle`, una por estado). Con Tenaris pasan el límite de 30 s de PHP de Herd en local; en producción depende de su `max_execution_time` y de la latencia a la base.

**Probado:**
- Las dos pantallas responden 200 con el grupo 2.
- En Chrome, en claro y en oscuro, con las gráficas dibujadas. Sin errores de JavaScript.

#### Lote 5: Productos creados en Websimex (24/10/2026)

**Convertido:**
- `websimex.productos` (`websimexproductos`) y su componente `Websimex\Productos`.
- No tiene gemela: la misma pantalla trae las dos pestañas, importación (`ProductosWS`, Websimex) y exportación (`ProductosExpoCIA`).
- Las pestañas son de Alpine (antes, Bootstrap con jQuery). Los filtros van en un formulario: Enter también busca.
- Los iconos de aprobado y bloqueado se pintan en la vista; se quitó la propiedad `$icono`, que guardaba su HTML en el estado del componente.

**Comportamiento que se conserva:** filtros, columnas (clase y familia solo para el importador 2691) y las dos descargas a Excel.

**Rendimiento (R6, hecho por decisión del responsable):**
- **Qué pasaba:** la búsqueda sin filtros traía **todos** los productos del cliente a una propiedad pública: con Tenaris, 13.290 filas, unos 25 s hasta pintarlas. Esas filas viajaban en cada petición de Livewire.
- **Ahora:** los resultados no se guardan en el estado. La tabla se pagina en la base (50 por página, `WithPagination`, una página por pestaña) y la descarga hace su propia consulta, completa.
- Al pulsar Buscar se guardan los filtros (`filtro_impo`, `filtro_expo`): la tabla y la descarga usan esos, como antes, aunque después se escriba otra cosa.
- Tenaris: buscar pasa de ~25 s a 2,2 s, y la página pesa 400 KB.
- Orden estable para paginar: por `ID_PRODUCTO` (importación) y por `id` (exportación).
- Nueva vista de paginación para Livewire: `vendor.pagination.ctc-livewire`.

**Seguridad:** el importador era una propiedad pública que las búsquedas usaban antes de que `render()` la volviera a tomar de la sesión. En Livewire 3 el navegador puede cambiarla, así que se podían consultar los productos de otro importador. Ahora sale siempre de `session('websimexid')`.

**Probado:**
- La pantalla responde 200 con Tenaris.
- En Chrome, en claro y en oscuro: búsqueda en las dos pestañas, paso a la página 2 y descarga de importación (13.290 filas en el Excel). Todo de solo lectura. Sin errores de JavaScript.

#### Lote 6: kit ui-ctc y Pedidos en Curso (24/10/2026)

**Qué es:** el diseño "Rediseño de Pedidos en Curso" (Claude Design, entregado por el responsable) pasa a ser la norma de las pantallas. Queda escrito como skill en `.claude/skills/ui-ctc/SKILL.md`, que dice qué aplicar en cada tipo de pantalla y cómo conservar su funcionalidad.

**Kit (`resources/views/components`, `resources/css/ctc.css`):**
- nuevos: `x-pagina`, `x-indicador`, `x-barra-busqueda`, `x-chip-filtro`, `x-pestana`, `x-etiqueta`, `x-orden-columna`, `x-icono`, `x-icono-simbolos`;
- CSS: `ctc-tabla-caja` + `ctc-tabla` (cabecera fija siempre), `ctc-fija` (columnas fijas), `ctc-accion`;
- `x-encabezado-pagina` con migas y título de 26 px; las dos paginaciones con el estilo del diseño.

**Íconos: Heroicons v2** (decisión del responsable). `blade-ui-kit/blade-heroicons` pasa de 1.7 a 2.6 (solo ese paquete en `composer.lock`). Los nombres de la versión 1 que usaban 12 vistas se cambiaron por los de la 2 (`mail` → `envelope`, `exclamation` → `exclamation-triangle`, `refresh` → `arrow-path`…).

**Pedidos en Curso (`impo/pedidos`) con el diseño:**
- Deja de ser heredada: modo oscuro completo y sin las librerías antiguas.
- Se conserva todo: indicadores que filtran por status, los filtros (procesos, sucursales, tipo de declaración, importador, segmento, status, pendientes, fechas de apertura), búsqueda con Enter, orden por ETA y fecha de arribo, las dos pestañas, las acciones de cada DO con sus mismas condiciones, el detalle (detalle, facturas con ítems, unidades de carga), el diálogo de pendientes y anotaciones, la descarga y el enlace al histórico.
- **Cambia:**
  - la lista se pagina (50 por página y por pestaña); antes salía completa;
  - otro clic en el indicador activo quita el filtro;
  - etiquetas de filtros aplicados, cada una con su botón para quitarla;
  - las fechas de arribo, levante, liberación y entrega se marcan "Pendiente" cuando el hito anterior ya se cumplió (es presentación: se calcula de las mismas fechas);
  - se quitó la gráfica de anticipadas de la tarjeta "Arribo sin levante" (no está en el diseño).
- **Rendimiento:** `etapasEh` ya no es propiedad pública, la descarga ya no sube la lista desde el navegador, y las acciones de fila ya no son un componente Livewire por fila. Con Tenaris, el estado del componente pasa de ~5,7 MB a 0,23 MB.
- **Seguridad:**
  - el detalle del DO (`/detalle-importacion`) y los pendientes se pintaban con HTML armado en PHP sin escapar; ahora son partials Blade (`partials/detalle-importacion/*`, `partials/pendientes-operacion`);
  - `orderBy` (viene de la URL) solo acepta las dos columnas de orden.
- Las otras pantallas que piden `/detalle-importacion` (DTA, zona franca, programación de arribos) reciben el mismo HTML nuevo.

**Probado (Tenaris, solo lectura, claro y oscuro):**
- indicadores, filtros y etiquetas, orden, pestañas, detalle con facturas y unidades, diálogo de pendientes;
- la cabecera y las tres primeras columnas fijas al desplazar;
- sin errores de JavaScript;
- `verificar.php` con 200 en pedidos, histórico, programación de arribos, DTA, zona franca, regalías, RIM, purchase order (v1 y v2), productos y dashboard.

**Para el responsable:** probar la descarga a Excel y las acciones de fila con el grupo 2.

**Sigue:** aplicar el skill a la gemela (`exportaciones.pedidos-curso`) y a las pantallas ya convertidas (L3 y L2a).

**Histórico de pedidos (`impo/ordenes-compra-historico`), con el skill:**
- Pantalla de tabla y filtros (sin indicadores). Usa la misma tabla que Pedidos en curso, sin la columna de declaración, que nunca tuvo.
- El detalle de cada DO sale de los datos que ya trae la consulta. Se normaliza a la forma de la API, para pintarlo con los mismos partials, ahora con el panel compartido `partials/detalle-importacion/panel`.
- Se conserva todo: consulta solo al pulsar Consultar (o con filtros en la URL), filtros, búsqueda, orden, paginación en la base (10 por página), acciones de fila, pendientes, descarga y "Regresar a Pedidos".
- `etapasEh` deja de viajar en el estado. El orden queda con lista blanca, también en la descarga. Los pendientes se pintan escapados: trait `Concerns\ConDetallePendientes`, compartido con Pedidos en curso.
- **Probado (Tenaris, solo lectura):** consulta (2.904 registros), detalle con facturas y unidades, orden, página 2, diálogo, claro y oscuro, sin errores de JavaScript.

**Control de operaciones de exportación (`expo/pedidos`), gemela, con el skill (diseño completo):**
- Se conserva todo: indicadores, filtros (procesos, sucursales, status, importador, fechas de apertura), búsqueda, los filtros por rol (incluido el 35 de bolsa de análisis), semáforos de cut off, el detalle (reserva de contenedores, SAE y unidades de carga), seguimiento, información personalizada, descarga e histórico.
- **Cambia:**
  - los indicadores filtran por status (antes no eran clicables);
  - la lista se pagina (50 por página);
  - etiquetas de filtros aplicados;
  - el encabezado "Cut off físico" ordenaba por el cut off documental: ahora cada uno ordena por su fecha;
  - se quitó la ventana de selectividad, a la que no se llegaba (su botón estaba comentado);
  - ya no carga Font Awesome (no se usaba).
- **Rendimiento:** las acciones de fila ya no son un componente Livewire por fila (cada uno consultaba `ex_po`), y `etapasEh` ya no viaja en el estado.
- **Seguridad:** la descarga vuelve a consultar en el servidor (antes recibía la lista desde el navegador), y el orden tiene lista blanca.
- **Probado (Tenaris, solo lectura, claro y oscuro):** indicadores y filtro, etiquetas, orden, detalle con unidades de carga; sin errores de JavaScript.

**Programación de Arribos (`impo/programacion-arribos`), con el skill:**
- Deja de ser heredada: carga solo Highcharts (cdnjs, versión fija), no el resto de librerías antiguas.
- Las gráficas laterales toman textos y bordes de los tokens, y se redibujan al cambiar de tema. Las porciones sin dato se llaman "Sin información".
- Métricas del mes con `x-indicador estatico` (nuevo modo del indicador, sin acción).
- Se conserva todo:
  - filtros aplicados solo al pulsar Consultar, búsqueda y orden por ETA, paginación (25);
  - semáforo de ETA, filas de pedidos notificados, validación documental con los pendientes en el `title`;
  - detalle del DO (ahora con el panel compartido), descarga, gráficas;
  - para clientes que administran transporte: asignación de transportador y planta, y envío de prealerta.
- El estado del detalle (`ctcDetalleDo()`) pasa a `resources/js/ctc.js`, compartido con Pedidos en curso.
- **Ojo, ya pasaba antes:** al abrir la pantalla se escriben registros en `po_transporte` (`migrarTransporteFase3`, `firstOrCreate` de los DO con transportador en la fase 3). Por eso no se probó en vivo en el navegador.
- **Probado:**
  - la página se pintó con `guardar_html.php` (transacción revertida) y se revisó ese HTML en Chrome, claro y oscuro, sin peticiones de escritura (bloqueadas en la prueba);
  - en esa revisión: indicadores, filtros, tabla con cabecera y columnas fijas, detalle desplegable, gráficas, paginación;
  - sin errores de JavaScript.
- **Para el responsable (grupo 2):** Consultar con filtros, etiquetas, orden, detalle con datos, asignar transporte, prealerta y descarga.

**Control de contenedores V2 (`impo/contenedores`), con el skill (tabla y búsqueda):**
- Se conserva todo: búsqueda mientras se escribe, pestañas En proceso / Finalizados con su conteo, orden por ETA, enlace al seguimiento de cada contenedor y las 23 columnas, con los semáforos de bodegajes y demoras.
- **Cambia:**
  - la lista se pagina (50 por pestaña); antes pintaba las 1.763 filas de Tenaris;
  - el resultado se guarda 5 minutos por usuario y búsqueda (solo los campos de la tabla, 1,6 MB): cambiar de pestaña o de página pasa de ~7 s a ~1 s. El botón Consultar trae los datos al momento;
  - la pantalla usa directamente la consulta: el componente que la envolvía consultaba importadores en cada petición sin mostrarlos;
  - ya no carga Font Awesome.
- **Error previo corregido:** con la sesión abierta por `/login` (página antigua), `control_contenedor` no existe y la pantalla daba error 500. Ahora muestra la lista vacía. La entrada real (`/`) sí carga esa configuración.
- Kit: mensaje de tabla vacía en `ctc-vacio` (visible aunque la tabla sea más ancha que la caja), aplicado también a Pedidos en curso, su gemela y Programación de Arribos.
- **Probado (Tenaris, solo lectura, claro y oscuro):** 1.763 unidades en proceso, pestañas, página 2, búsqueda con etiqueta, orden, cabecera y columnas fijas; sin errores de JavaScript.

**Control de operaciones EXPO, bolsa específica (`expo/control-pedidos`), con el skill (diseño completo):**
- Se conserva todo: indicadores por status con el conteo de posibles inconsistencias, filtros (procesos, sucursales, fechas, status, inconsistencias, importador), búsqueda, orden, semáforos de gestión del grupo de apoyo, cut off, inspecciones, notificaciones, ETS y DEX, asistente, cambio de viaje, filas con posible inconsistencia, y el detalle (reserva, SAE y unidades, con el mismo partial que su gemela).
- **Errores previos corregidos:**
  - la lista solo se armaba en `mount()` y en algunas acciones; al ser privada, cualquier otra petición de Livewire la dejaba vacía. Ahora se arma en cada `render()`, como en su gemela;
  - división por cero en los indicadores cuando todos los DO estaban cerrados;
  - "Cut off físico" ordenaba por el cut off documental.
- **Cambia:**
  - los indicadores ya filtraban; ahora otro clic en el activo quita el filtro;
  - paginación (50) y etiquetas de filtros;
  - los semáforos son datos (`x-semaforo`, componente nuevo del kit) en vez de HTML armado en PHP;
  - sin foto del asistente se muestran sus iniciales (antes, una foto de archivo de una persona cualquiera);
  - ya no carga Font Awesome.
- **Probado (Tenaris, solo lectura, claro y oscuro):** 18 operaciones, indicador y filtro, etiqueta, orden por cut off físico, otra petición sin perder la lista, detalle con unidades; sin errores de JavaScript.

**Órdenes de compra V2 (`impo/purchase-order-v2`, `impo/order-v2`), con el skill (diseño completo):**
- El componente ya estaba bien hecho (paginación en el servidor, agregados en SQL, detalle bajo demanda, alcance de terceros); el cambio es de presentación.
- Se conserva todo:
  - embudo de cuatro fases que filtra (otro clic lo quita), conversión del embudo;
  - búsqueda por pedido, supplier, SED o contenedor; año y segmento al cambiar;
  - estado comercial (Todas, Abiertas, Cerradas, En riesgo), ahora como pestañas de la lista;
  - detalle de la orden bajo la fila, aviso de coincidencias en otros años;
  - editar y anular con sus mismas condiciones, importación masiva, nueva orden;
  - avisos de tercero sin grupos y de enlace mixto.
- **Cambia:**
  - usa los tokens del layout (`--acento` es el color de la empresa activa), así que sigue el modo oscuro. Ya no calcula su propia paleta `--brand` desde `color_p` ni carga IBM Plex;
  - el filtro de fase y la búsqueda salen como etiquetas;
  - el botón de importación masiva usa el kit (su diálogo se convierte con las pantallas de creación y edición, que escriben).
- `_filtros.blade.php` se eliminó (la barra de búsqueda y las pestañas lo reemplazan).
- **Probado (Tenaris, solo lectura, claro y oscuro):** embudo y filtro por fase con etiqueta, pestañas, detalle con líneas, búsqueda sin resultados, cabecera fija; sin errores de JavaScript. No se tocó anular, nueva orden ni importación masiva.

**Formulario de la orden V2 (`impo/purchase-procesar-v2`), con el skill (formulario), 28/10/2026:**
- Deja de ser heredada: `<x-app-layout>` sin `heredado`, sin IBM Plex y sin la paleta `--brand` que el componente calculaba desde `colorp` (se quitaron sus propiedades y funciones de color). Sigue el modo oscuro.
- Se conserva todo: carga del documento (zona de carga, barra en modo manual, resumen de la lectura con Re-extraer, Reemplazar y Quitar), visor, cotejo campo por campo con su etiqueta de confianza y su procedencia, Número DO con su verificación, puerto con sugerencias, proveedor por crear, resumen de errores y avisos de la validación, productos con cotejo por línea, diálogo de producto con búsqueda en el maestro, trazabilidad, modalidad bloqueada con su diálogo "Cambiar" e historial, Guardar borrador y Confirmar con sus mismas condiciones.
- **Cambia:** encabezado de página con las acciones; una tarjeta por sección; tabla de productos con cabecera fija; diálogos del kit (`x-teleport`, Escape cierra); el visor solo aparece cuando hay documento (antes un panel vacío) y se queda fijo al desplazar; en una OC abierta para editar, un campo vacío ya no se pinta como cotejado.
- `x-encabezado-pagina`: las acciones pasan a otra línea en pantallas angostas (antes se salían del borde). Aplica a todas las pantallas que lo usan.
- **Probado (grupo 2, solo lectura, claro, oscuro y 390 px):** nueva, manual, línea agregada, guardar con errores (sin escribir), OC con modalidad (diálogo "Cambiar") y OC legacy; sin errores de JavaScript ni desplazamiento horizontal. Guardar, confirmar, cambiar la modalidad y leer un documento real quedan para el responsable.

#### Lote 7: Reportes específicos y personalizados (25/10/2026)

Siguiente por uso (`/rreport`, 678 clics), con su gemela `/rexreport`. Pantalla de formulario y tabla, con el skill ui-ctc.

**Convertido:**
- `Reportes.index` (`rreport`, `searchreport`) y `ExReportes.index` (`rexreport`, `searchexreport`).
- Partes compartidas: `Reportes._formulario` (reporte, fechas y descripción) y `Reportes._resultado` (tabla con cabecera fija y conteo).
- El rango de fechas aparece según `campo_filtro`, la misma regla de `rvalidar`/`rexvalidar`, pero calculada en la vista con Alpine en vez de una llamada AJAX en cada cambio. Las rutas `rvalidar` y `rexvalidar` se conservan.
- La descripción del reporte, que solo mostraba exportación (con jQuery), sale en las dos cuando tiene texto.

**Se conserva:**
- El SQL configurado por cliente (`reportes.sql`, `?IdGrupo`, `?Filtro`) y su ejecución, sin cambios. El aislamiento sigue en `?IdGrupo` y en el filtro por grupo de la tabla `reportes`.
- Importación pagina de a 20 (`PaginasArray`) y exportación muestra todo, como antes.
- La etiqueta `DIAS_RESTANTES`: roja si es mayor que 0 y verde si no.
- Las descargas: `/exportReporte/{id}/{título}` (importación) **también envía el Excel por correo** al grupo del reporte; el botón lo dice en su `title`. `/exportExReporte/{id}` solo descarga.

**Cambia:**
- Los reportes 229 y 230 de importación abrían sus pantallas (ya en el layout nuevo desde L2a) dentro de un iframe de planti (`iframe.index`). Ahora redirigen a ellas directamente.
- Los reportes de exportación en construcción (24 a 28) muestran el aviso en la misma página, en lugar de `layouts.enconstruccion`. Esa vista se conserva para los KPI.
- Si el reporte no trae filas, aparece el mensaje de tabla vacía. Antes no salía nada.
- Las fechas se deshabilitan al elegir un reporte sin `campo_filtro`. Antes viajaban las de la consulta anterior y el SQL quedaba con `AND  between …` sin campo.
- Se quitó código muerto: el modal de documentos y su `.openModal` (ningún elemento lo usaba) y los estilos de DataTables.

**Errores previos corregidos:**
- `ReportesController@search` ejecutaba el SQL del reporte **dos veces** (bloque repetido).
- `ExReportesController@search` sin reporte elegido fallaba: `compact()` con `$titulo` sin asignar.

**Ya existía y se mantiene:** en `ReportesController@search`, la condición de Paccar (`dtpFechaIni != null && id == 305 || id == 306`) no tiene paréntesis.

**Probado (grupo 2, solo lectura):**
- Las dos pantallas, el resultado y los 302 de 229 y 230: 200 en un solo proceso, con transacción revertida.
- En Chrome, en claro, en oscuro y a 360 px: fechas visibles y ocultas según el reporte; "Reporte Vencimiento Mercancía" con 3.040 registros, página 2, cabecera fija al desplazar; exportación con resultado vacío. Sin errores de JavaScript.
- No se probaron las descargas: la de importación envía correo.

**Para el responsable (grupo 2):** las dos descargas, con y sin fechas.

#### Kit ui-ctc en los lotes 1 a 5 (25/10/2026)

Las pantallas de los lotes 1 a 5, convertidas antes del kit, pasan al diseño del skill. Un commit por lote. Funcionalidad, rutas, nombres de campo y acciones sin cambios, salvo lo que se dice abajo.

**Común:**
- `x-pagina` como contenedor.
- Íconos Heroicons (`x-icono`) en vez de SVG pegados. En las filas, en sprite.
- Tablas con `ctc-tabla-caja` y `ctc-tabla`: cabecera fija y, donde hay acciones, esa columna fija a la izquierda.
- Barra de la tabla con el conteo y la leyenda de colores. Mensaje de tabla vacía en `ctc-vacio`. Paginación dentro de la tarjeta.
- Vacíos como `—`. Excel con `table-cells` en verde.

**Orden de compra y Orden de cargue (lotes 1 y 2):**
- Filas marcadas por estado: el fondo se mezcla con la superficie (opaco), para que la columna fija de acciones tenga el mismo color.
- Estado con `x-etiqueta :color`. Acciones de fila con `ctc-accion`.
- La leyenda de estados pasa a la barra de la tabla y aparece también en los dos históricos (la vista ya recibía los estados).
- En exportación, PDF y XLS de la orden pasan a íconos (`document-text`, `table-cells`).
- Diálogo de facturas y gastos: pestañas `x-pestana` y tablas `ctc-tabla`. La sincronización de órdenes usa `x-etiqueta`.

**Archivo digital (lote 3):**
- Búsqueda en `x-barra-busqueda` (DO, pedido u OC; Enter consulta), con los demás filtros desplegados al abrir. El ZIP pasa al encabezado.
- Etiquetas para texto, régimen y tipos de documento: `limpiarFiltro` y `limpiarFiltros`, con lista blanca.
- **Cambia:** la lista se pagina (50 por página). Antes salía completa: con Tenaris, ~1.200 filas por mes. La consulta y el ZIP no cambian.
- Probado con Tenaris, septiembre–octubre: 3.413 documentos, los mismos del lote 3.
- Con Tenaris desde 2023, la consulta agota la memoria (512 MB) en local. Ya pasaba antes: trae todos los pedidos del rango con sus documentos.

**Torre de Control (lote 4):**
- Indicadores con `x-indicador`, que ahora admite:
  - `href` (la tarjeta es un enlace);
  - slot al pie (varios enlaces, con `estatico`);
  - `:insignia="false"`, cuando el % es una proporción y no un avance.
- La barra de cada indicador es la proporción del estado sobre el total, con el color de categoría del cliente.
- Importación: la tarjeta lleva a la búsqueda del estado, como antes.
- Exportación: conserva sus dos enlaces ("Ver tabla" y "Ver detalle") y el texto de `sql_porcentaje`.
- Tablas por estado con cabecera fija. El doble clic no cambia.

**Productos de Websimex (lote 5):**
- Una barra de búsqueda por pestaña, con sus filtros y etiquetas. Las etiquetas muestran los filtros con que se buscó.
- `limpiarFiltro` con lista blanca vuelve a buscar en esa pestaña.
- Pestañas `x-pestana` con el conteo. La descarga a Excel pasa al encabezado, la de la pestaña activa.

**Probado (solo lectura):**
- Las 14 rutas responden 200 en un solo proceso: lotes 1 a 7, Pedidos en curso y las de anular (solo el formulario).
- En Chrome, en claro y oscuro, sin errores de JavaScript:
  - `/rpo` (grupo 2): cabecera y acciones fijas al desplazar, y el diálogo de facturas. Se abrió solo el de importación: el de exportación sincroniza facturas al abrir;
  - histórico con leyenda y `/rexpo`;
  - Archivo digital (Tenaris): rango, página 2, filtro con etiqueta y quitarla, diálogo de comentarios;
  - las dos torres (grupo 2);
  - Productos (Tenaris): 13.290 productos, página 2, filtro "Aprobado" con etiqueta (10.071), pestaña de exportación.
- `/rpo` también a 360 px.

#### Kit ui-ctc en las pantallas de L2a (25/10/2026, en curso)

Las pantallas que salieron del iframe en L2a con su aspecto de siempre (`<x-app-layout heredado>`) pasan al kit y dejan de ser heredadas: modo oscuro, sin las librerías antiguas y sin Font Awesome. Quedaban **52 vistas** heredadas: las 35 de L2a, sus subpantallas y los contenedores sencillos.

**Orden:** por uso real (`seguimiento_apps`, últimos 180 días). Cada pantalla se prueba con el cliente que más la usa, solo lectura. Un commit por pantalla.

**Hechas:**

| Pantalla | Clics (180 d) | Probada con | Errores previos corregidos |
|---|---|---|---|
| Reportes específicos (`reportes-espe`) | 1.290 | Tenaris | Resultado en el estado dos veces (HTML armado en PHP sin escapar y las filas). Ahora va en caché por usuario y componente, y la tabla está paginada |
| Pedidos en Curso DTA (`impo/pedidos-dta`) | 761 | Fruselva | Vencimiento y pendientes eran HTML armado en PHP (ahora son datos y partials). La descarga recibía la lista del navegador. El orden no tenía lista blanca. Sin resultados, los indicadores no se recalculaban |
| Detalle de la OC (`detalleoc`) | 534 | Adama | **La búsqueda siempre respondía 500**: escribía el texto dentro del SQL (inyección) y además pasaba 4 parámetros que el SQL no tenía. Ahora usa parámetros enlazados, también en la compañía y la descarga |
| Dashboard de importaciones (`dashboard/impo`) | 513 | Imusa | El mapa recibía `'empresa'` como color. Se quitó el enlace "View all", que no llevaba a ninguna parte |
| Indicadores KPI V2 (`kpi-graph`) | 411 | Brinsa | **La gráfica no aparecía tras consultar**: era un `<script>` en la respuesta de Livewire, que Livewire 3 no ejecuta. Las tablas eran HTML sin escapar y la descarga recibía el detalle del navegador |
| Productos (`config/product-fr`) | 398 | Brinsa | Los colores de categoría se armaban como clases con el nombre guardado en la base (no estaban en el Tailwind compilado). La búsqueda no volvía a la primera página |
| Reporte de transporte (`impo/reportes/transporte`) | 261 | Imusa | La lista viajaba entera en el estado y la descarga la recibía del navegador. `orderBy` iba al SQL sin lista blanca. Ordenar por ETA solo cambiaba la flecha |
| Reporte global de importación (`reporte/globalimportacion`) | 223 | Imusa | **Cualquier usuario de otro cliente veía los datos de Groupe SEB** (importador fijo, sin mirar la sesión). Ahora es 403 fuera del grupo. La descarga fallaba en PHP 8 (`range('A', 'BQ')`; también en Brenntag). Resultado en caché 5 minutos: la página 2 pasó de ~10 s a 1,5 s |
| Seguimiento RIM v2 (`rim/seguimiento-v2`) | 217 | Imusa | Diseño propio (≈900 líneas de CSS) reemplazado por el kit, por decisión del responsable. El componente no cambia |
| Sistemas especiales y CERT-DEX (`reportes-especiales/...`) | 65 y 125 | Imusa | Pintaban todo sin paginar (7.865 filas). Sistemas especiales filtraba por la propiedad pública `$compania`; ahora usa `companiasDelGrupo()`. El chevron de cada fila no abría nada y se quitó |
| Reporte DEX (`reporte-dex`) | 119 | Imusa | Diseño propio (`.dex-*`) reemplazado por el kit |
| Recobro de bodegaje (`reporte/recobro`) | 74 | Imusa | El botón editar daba error de JavaScript con DO alfanuméricos (`edit(26A…)` sin comillas). Mostraba 4 filas por página. Armar los datos tarda ~40 s y se repetía en cada clic: ahora va en caché 5 minutos y Guardar la refresca |
| Reporte global KPI Brenntag (`reporte/globalbrenntag`) | 70 | Brenntag | Caché de 5 minutos, como su gemelo de Groupe SEB |
| Global importaciones (`global-importaciones`) | 51 | Brinsa e Imusa | `$data` pública con toda la consulta: pasa a caché (30 min, solo columnas visibles) y se pagina. Los dos componentes comparten vista |
| OTIF Brinsa (`exbrinsa`) y global exportaciones Imusa (`eximusa`) | 47 | Brinsa e Imusa | Vista compartida `livewire/partials/reporte-expo-cliente` |
| Seguimiento CTC (`seguimiento/ctc`) | 40 | ASERCOL | **Listaba los usuarios (nombre y correo) de todos los clientes a cualquier usuario con sesión**: ahora 403 fuera del perfil 1, el de su menú |
| Informe de contenedores Ternium (`contenedores_ternium`) | 39 | Ternium | **Consultaba dos NIT fijos sin mirar la sesión**: ahora `exigirImportadores()`. Usa la vista compartida de exbrinsa |
| Seguimiento de arribo ShipsGo (`shipsgo/ifame`) | 38 | Brinsa | **La cuenta de ShipsGo es una sola y la pantalla listaba todos sus embarques a cualquier cliente**: ahora 403 sin `tiene_shipsgo`. Se quitó Leaflet (no se usaba). Sin credenciales de ShipsGo en local: verificada con el servicio simulado; detalle (escribe la fecha de descargue) y mapa quedan para el responsable |
| Reporte KPI Nespresso (`report-nespresso`) | 35 | Nestlé | **La paginación nunca avanzaba** (leía `request()->get('page')`). Resultado en caché en vez de dos copias en el estado |
| Pedidos en Curso ZF (`impo/pedidos-zf`) | 26 | Brenntag | Como DTA. Además `updatingPage()` reiniciaba la página en cada cambio |
| Seguimiento del DO, impo y expo (`seguimiento-imp/{do}`, `seguimiento-exp/{do}`) | subpantalla | Brenntag, Tenaris, Nestlé, Imusa | **Abría el DO de cualquier cliente** (solo pedía sesión) y editar o eliminar una actividad aceptaba cualquier id: ahora `exigirDoDelGrupo()` (403) y la actividad tiene que ser del DO. **La de exportación daba 500 a nueve clientes** (adama, asercol, alpina, brenntag, greensuperfood, ternium, oben, cargill, yara): sus etapas no tienen `tabla_ws`; ahora esas etapas se omiten. El cuerpo de los correos se pintaba sin escapar. Una sola vista para las dos gemelas (`livewire/partials/seguimiento-do`). Las rutas pasan a `pantallas_layout` |
| Campos dinámicos del DO, impo y expo (`dinamicos-imp/{do}`, `dinamicos-exp/{do}`) | subpantalla | Alpina e Imusa | **Abría el DO de cualquier cliente**: `exigirDoDelGrupo()`. Guardar aceptaba ids de campo de otro grupo (los campos viajan en el estado): ahora solo graba los definidos para el grupo. Vista compartida `seguimiento-do/campos-dinamicos` |
| Pendientes del DO (`impo/seguimiento-pendiente/{do}`) | subpantalla | Imusa | **Abría y escribía en el DO de cualquier cliente**: `exigirDoDelGrupo()`; guardar y finalizar exigen operación E (los mismos que ven el enlace) y un pendiente activo del catálogo. Los mensajes de validación del comentario no salían (claves `observaciones` en vez de `observacion`) |
| Histórico de exportación (`expo/pedidos/historico`) | subpantalla | Nestlé | Filtraba por la propiedad pública `$compania` (ahora `companiasDelGrupo()`). **La lista desaparecía al ordenar** (solo existía en la respuesta de Consultar) y salía completa: ahora va en caché 10 minutos por usuario y filtros, paginada de a 50, y el orden se aplica sobre ella (2 s en vez de 11). Con el rol 32, consultar daba error (`infoUser` solo existía en `mount()`). El diálogo de unidades de carga no tenía botón (estaba comentado): se quitó con su método; las unidades están en el detalle |
| Órdenes de compra v1 (`impo/order`, `impo/purchase-order`) | 1.437 | Brinsa y Brenntag | **Anular aceptaba el id de una orden de cualquier cliente y no pedía operación E** (el botón se ocultaba, el método no): ahora la orden tiene que ser del grupo y el usuario de operación E, como en la V2. Las compañías salían de la propiedad pública `$compania` (ahora `companiasDelGrupo()`). Cada clic repetía la consulta (~5 s) y la página pintaba todas las órdenes con su detalle oculto (4,7 MB de HTML): ahora la lista va en caché 5 minutos por usuario, búsqueda y segmento, paginada de a 50 por pestaña (465 KB; clic ~1,4 s). Los indicadores pasan de la columna derecha a la fila superior: Abiertas y En proceso filtran (otro clic quita el filtro), Cerradas lleva a su pestaña. La pestaña Cerradas no tenía la columna de segmento. Se borró `x-stat-list-card` (solo la usaban los tops). En Brenntag el filtro de segmento sale vacío: todas sus órdenes tienen `IdComex` nulo, igual que antes |

**Cambia en todas:**
- Pedidos en Curso DTA ahora tiene lo mismo que Pedidos en Curso:
  - indicadores que filtran;
  - etiquetas de filtros;
  - la lista paginada de a 50 (antes salía completa);
  - el mismo detalle del DO y el mismo diálogo de pendientes.
- Reportes específicos y KPI paginan o limitan su tabla a la caja con cabecera fija. El resultado dura 30 minutos en caché. Si caduca, la pantalla pide consultar de nuevo.

**Piezas nuevas del kit:**
- `partials/vencimiento-dta`: días hábiles para retirar un DTA.
- `livewire/dashboard/partials/totales-medio`: totales por medio de transporte.
- En `resources/js/ctc.js`, `ctcTokens`, `ctcTemaHighcharts`, `ctcEtiquetasTema` y `ctcDibujarGrafica`: Highcharts con los colores del tema, que se redibuja al evento `tema-cambiado`.
- `x-pagina` ocupa el alto de la ventana. En oscuro se veía el corte del fondo bajo un contenido corto.

**Siguen heredadas (22 vistas). Estadísticas RIM pasó al kit con Configuración (abajo). Seguimiento RIM v1 (`rim/seguimiento`) no se convierte, por decisión del responsable (26/10/2026): la reemplazó la v2 (`rim/seguimiento-v2`, ya en el kit), aunque todavía registra 134 clics en 180 días.**
- las de menos de 20 clics y las subpantallas: formularios de orden de compra y de productos, entre otras.

**Aislamiento entre clientes (corregido el 25/10/2026):**
- Doce componentes Livewire consultaban importadores fijos en el código, sin mirar el grupo de la sesión. Cualquier usuario con sesión de otro cliente veía esos datos al abrir la URL:
  - reportes globales de Groupe SEB y Brenntag;
  - `eximusa`, `exbrinsa`, `report-nespresso` y `consultafechabl`;
  - recobros de Imusa y Yara, temporales de Yara y kardex de Brinsa;
  - `global-importaciones` de Brinsa y Groupe SEB.
- Ahora llaman a `FiltraPorGrupoEmpresarial::exigirImportadores()` en `mount()`, que responde 403 fuera del grupo. Probado: grupo 2 recibe 403 y cada cliente abre los suyos.
- El importador era una propiedad pública que el navegador podía cambiar:
  - en Brenntag, además, se escribía dentro del SQL; ahora tiene lista blanca;
  - en GroupeSeb y Brinsa ahora es `#[Locked]`.
- `exbrinsa`, `exbrinsa-otif`, `eximusa` y `eximusa-otif` no tenían `->middleware('auth')`; ahora sí.
- La regla quedó en CLAUDE.md.

#### Configuración, Declaración de Cambio y Estadísticas RIM (26/10/2026)

Todas las opciones que dependen de **Configuración** (menús 4 de importación y 27 de exportación) salen de Bootstrap al kit, más Declaración de Cambio (impo y expo) y Estadísticas RIM. Productos (`config/product-fr`) ya estaba. Son 41 pantallas de configuración, casi todas catálogos con el mismo esquema.

**Piezas nuevas del kit** (`components/catalogo/`):
- `avisos`: mensajes de sesión y errores de validación.
- `formulario`: plegable, abierto al editar; con `accion` pinta el `<form>`, sin ella sirve a Livewire (`entangle="toggle"`).
- `busqueda`: por formulario (POST a `…srh`, como antes) o por Livewire (`modelo`).
- `lista`: título, conteo, leyenda de inactivos, tabla con cabecera fija y paginación.
- `acciones`: editar y eliminar, con confirmación.

Inactivos: fila ámbar y etiqueta Inactivo; los activos llevan etiqueta Activo.

**Pantallas:**

| Familia | Rutas | Tipo |
|---|---|---|
| Importaciones · Configuración, formulario en línea | `rplanta`, `rsucursales`, `rvencimientopuertos`, `rterminales`, `rdocumentosfacturas`, `rlinea`, `raduana`, `rlugardevolucion`, `rreportescontactos`, `rreportegrupocontacto`, `rnumeralesregimencambiario`, `costos-fletes` | controlador |
| Importaciones · Configuración, resource | `rmoneda`, `rincoterms`, `rpais`, `rpuertos`, `rsupplier`, `rmaterial`, `rempaques`, `retapas`, `rcompania` (y `configuravistas`) | index, create y edit |
| Importaciones · Configuración, Livewire | `categoriaproducto` | Livewire |
| Exportaciones · Configuración | `rexmoneda`, `rexincoterms`, `rexpais`, `rexfolderdocumentos`, `rextipodocumentos`, `rexunidaddemedida`, `rexdocumentosexcept`, `rexpofacturacionconcepto` | controlador |
| Exportaciones · Configuración, Livewire | `ExPoPuerto`, `ExPoClientes`, `ExPoConsignatario`, `ExPoEmpaques`, `ExPoEtapa`, `ExPoExportador`, `ExPoLinea`, `ExPoMaterial` | Livewire |
| Declaración de Cambio | `calculonumerales`, `calculonumerales_ex`, `rdeclaracion` (Régimen cambiario) | Livewire y controlador |
| Estadísticas RIM | `rim/stats`, `rim/show-stats` | Livewire (diseño completo) |

**Aislamiento entre clientes (corregido en todas):**
- Editar, guardar, actualizar, eliminar o inactivar aceptaban el id de un registro de **cualquier cliente**. En los Livewire, `updateOrInsert` por id sobrescribía el registro ajeno y lo pasaba al grupo propio. Ahora el registro tiene que ser del grupo en sesión (404 si no). En los resource, `update()` era el que no lo comprobaba; `rpuertos` tampoco en `destroy()`.
- Búsquedas con `orWhere` suelto devolvían registros de otros grupos (sucursales, navieras, contactos, asignación de reportes, catálogos de exportación, Excel de navieras).
- Declaración de Cambio:
  - `id_grupo_empresarial` era público y editable: ahora `#[Locked]`.
  - Generar CSV y revertir exigen facturas, compañía, cliente y banco del grupo.
  - La descarga del resumen entregaba **cualquier archivo del servidor** (`../../.env`); ahora solo los CSV del dominio en sesión.
  - Régimen cambiario consultaba Websimex sin filtro de importador.
- Estadísticas RIM filtraba por la propiedad pública `$compania`; ahora usa `companiasDelGrupo()`.
- **Inyección SQL corregida:**
  - en la búsqueda de Documentos de facturas (texto y NIT);
  - en `configuravistas` (`idcompania`);
  - en `parametros/guardar` (nombre y cargo).
- `parametros/guardar` leía la primera fila de `parametros_pdf` de cualquier cliente.
- `rreportegrupocontacto` no tenía `auth`: el controlador ahora lo exige.

**Errores previos corregidos:**
- Búsquedas que daban **500** por columnas inexistentes: Planta, Vencimiento de puertos, Terminales, Lugar de devolución, Folder documentos, Tipo documentos, Unidad de medida, Documentos visibles por cliente y Concepto de facturación.
- Documentos de facturas daba 500 a clientes sin compañías activas (`in (')`, p. ej. Alpina). El combo de factura salía sin valor (`id_po` en vez de `id`).
- Numerales régimen cambiario:
  - `update()` redirigía a una ruta inexistente;
  - `show()` y `edit()` daban 500.
- Material de exportación: **eliminar inactivaba una tarifa** con ese id, no el material.
- Al editar se borraban datos:
  - Documentos visibles por cliente: los dos tipos asignados;
  - Tipo documentos: imagen y color;
  - Consignatario de exportación: envío de documento.
- "Nuevo" después de editar sobrescribía el registro editado (clientes, consignatario y exportador de exportación).
- Otros errores:
  - Proveedores guardaba `null` al desmarcar Inactivo.
  - Empaques `show()` apuntaba a una vista inexistente.
  - `Auth::logout()` sin importar en varios controladores: al rechazar un id ajeno daba 500.
- Estadísticas RIM:
  - las 11 gráficas se pintaban con un `<script>` en la respuesta, que Livewire 3 no ejecuta;
  - dos encabezados de columna estaban cambiados;
  - las fechas vacías salían como 1970-01-01.

**Cambia:**
- Eliminar pide confirmación también en los Livewire.
- Las búsquedas Livewire esperan 300 ms al escribir.
- Fletes x Transporte y Régimen cambiario dejan jQuery, select2 y los modales Bootstrap (Alpine y `fetch`). Los selects son nativos: se pierde la búsqueda dentro de la lista.
- Declaración de Cambio: el PDF del resumen sigue saliendo igual (dompdf lee un bloque oculto con el marcado anterior).
- Estadísticas RIM: indicadores arriba, con pestañas Solicitudes y Prórrogas. El resultado queda en caché 10 minutos y las descargas lo leen de ahí, no de lo que manda el navegador.
- `wire:loading.delay.flex` no lo oculta Livewire 3 al cargar ("Consultando…" visible siempre): se usa `wire:loading.delay` con la clase `flex`. También corregido en el detalle del dashboard.

**Probado (solo lectura, claro y oscuro, sin errores de JavaScript ni 500):**
- las 51 rutas responden 200 con el kit;
- navegador:
  - Planta (Alpina): editar y búsqueda;
  - Material, Clientes, País, Documentos visibles y Declaración de Cambio de exportación (Adama, Imusa);
  - Categoría de producto, Naviera, Fletes, Proveedores, Material, Etapas, Compañías, Moneda, Puertos, Declaración de Cambio y Régimen cambiario (Brinsa): editar en Proveedores, Material, Etapas, Compañías, Moneda y Puertos; búsqueda en Proveedores, Material, Puertos y Régimen cambiario;
  - Estadísticas RIM (Nestlé).

**Para el responsable (grupo 2):**
- guardar, editar y eliminar en cada catálogo, en especial el borrado de materiales de exportación y Documentos de facturas;
- Generar CSV y Revertir en Declaración de Cambio;
- las descargas Excel de Estadísticas RIM.

**Búsquedas paginadas (corregido el 26/10/2026):**
- Las búsquedas iban por POST a rutas `…srh` / `search…` que solo aceptaban POST, y los enlaces de página (GET, sin el texto buscado) daban **405**.
- Las 49 rutas que no tenían GET pasan a `Route::match(['get', 'post'], …)`; las otras 10 ya lo tenían. Las pantallas que siguen en Bootstrap mantienen su POST.
- En cada `search()` paginado, `->appends(request()->except(['_token', 'page']))`: los enlaces de página llevan `txtBuscar` y los demás filtros.
- `x-catalogo.busqueda` envía por GET: la URL de una búsqueda se puede guardar y Atrás no pide reenviar el formulario.
- `rresponsablecomexsrh`, `rdefinicioncamposcontensrh` y `rpoderessrh` no tenían `auth`; ahora sí. El resto de esas tres familias sigue sin `auth` (§8 de la base de conocimiento).
- Probado: Material (Brinsa), buscar "0", página 2 y página 3 con el término; Proveedores con 188 resultados, página 2 con 88.

**`configuravistas` (corregido el 26/10/2026):** es una herramienta del personal interno de ASERCOL que configura las vistas de cualquier grupo. Antes cualquier usuario con sesión podía abrirla y guardar. Ahora `configuravistas`, `exvistas/filtrar` y `exvistas/guardar` exigen el perfil 1, el mismo de su menú (77): 403 para los demás. Probado con un usuario del perfil 16.

**Rutas sin `auth` (cerradas el 26/10/2026):**
- Había 72 rutas web sin `auth`. Quedan 11 públicas a propósito: `/`, `logout`, la API por token (`ws`, `ws/insertar`, `ws/informes`, `ws/informes2`, `dev-api/adama/autenticacion`), `credenciales` y `cambiarclave` (recuperación del token de la API con usuario y clave) y `rexpo/notifica/{id}/{formato}/{tok}` (enlace de correo con token).
- Las otras 60 llevan `auth`: pantallas (Poderes, Responsables COMEX, Definición de campos de contenedor, Declaraciones juramentadas, Menús, Revisión de OC por WS, Archivos planos…), endpoints AJAX de pantallas con sesión (`seguimiento/apps`, `ws/envio*`, `ws/encodeBase64`, `rimpodocumentos/leido`, `rjdocumentos`…) y `/orden`. `/orden` buscaba órdenes de cargue con el grupo que deja en la sesión la página de login, así que un visitante sin cuenta podía consultarlas por número.
- `clear-cache` (borraba y regeneraba la caché de configuración, sin sesión) exige además el perfil 1.
- `ws/vererror` se retiró: sin sesión, insertaba actividades de prueba del grupo 6 en producción.
- `rexpo/notifica`: el token de la URL pública iba pegado al SQL; ahora es un parámetro enlazado.
- Probado: sin sesión, las rutas cerradas redirigen al login (302) y `ws/vererror` da 404; con el usuario 14, las pantallas responden 200; `clear-cache` da 403 al perfil 16; la API pública y `credenciales` siguen respondiendo 200.
- Ya fallaba antes, sin tocar: `seguimiento-oc/{id}` da 500 (el componente `po.seguimiento-orden-compra` exige `$numeroDo`); nada en el código enlaza a esa ruta.

**Pendiente (sin tocar):**
- `ExPoEmpaques` no tiene guardar, editar ni eliminar en el componente; sus botones fallan como antes.
- El Excel de clientes de exportación no devuelve datos.
- Incoterms está fijo al grupo 1 en el código y lo comparten todos los clientes.
- La tabla de Régimen cambiario sale vacía hasta buscar: `like '%?%'` sin parámetros.

#### Crear y editar Orden de Compra y Orden de Cargue (plan, 26/10/2026)

Son las pantallas en Bootstrap con más uso: se entra desde `/rpo` (Tenaris 1.318 clics en 180 días, Adama 1.127, Ternium 432, Koba 265, Cargill 142) y `/rexpo` (Adama 1.452). Escriben en la base.

**Tamaño:**

| Vista | Líneas | Contenido |
|---|---|---|
| `Po/edit` | 2.284 | 132 campos; secciones Encabezado, Campos dinámicos, Control de contenedores (contenedores con días libres, carga suelta y el Livewire `po.contenedores-orden-compra`) y Productos; 8 llamadas AJAX al ERP (traer datos del DO, del producto, de la DIM); filas dinámicas de actividades, productos y carga suelta armadas en JavaScript; 33 ramas por cliente (`session('GrupoEmpresarial') == …`, 11 clientes) |
| `Po/create` | 948 | Lo mismo, sin contenedores ni campos dinámicos |
| `ExPo/edit` | 1.887 | Orden de cargue: tarifas, lotes, distribución de contenedores |
| `ExPo/create` | 3.893 | Lo mismo, más grande |

**Fase 0, seguridad (antes de tocar la interfaz):** `POController@update` sobrescribía la OC de cualquier cliente (`PO::find($id)` sin grupo). Se auditan los ~20 endpoints que usan las cuatro vistas: resource `rpo`/`rexpo`, borrado de detalles, carga suelta, lotes y distribución, y las consultas al ERP por DO.

**Fase 0, hecha (26/10/2026):**
- `rpo.update` sobrescribía la OC de cualquier cliente, con sus detalles, contenedores y carga suelta (ids del request). Ahora todo se filtra por el grupo y la PO.
- Borraban registros de cualquier cliente: `rdeldet`, `rdeldetcs`, `rexdeldet`, `rexdeldetpro`, `eliminaLote`. Exportación (`rexpo.store`/`update`, `rudpcontenedor`, `rdistribucion`) escribía en la orden, contenedores, lotes y unidades despachadas de otro cliente. `actualizaMaterial` modificaba materiales ajenos.
- Las consultas al ERP por DO (`rtraerdatosdo`, `rtraerdatosproducto`, `rtraerdim`) devolvían datos de cualquier importador; ahora solo los del grupo.
- Inyección SQL corregida en `rtraerdatosproducto`, `rextraerdatosdo`, `rextraertarifas`, `rextraerproductosCantidad`, `infoContenedor/*`, `eliminaLote` y en el `edit` y el `create/{id}` de exportación.
- `po.contenedores-orden-compra`: `id_po`, `id_grupo_empresarial` e `id_us` eran públicas y editables desde el navegador. Ahora son `#[Locked]` y se valida la PO.
- **Regresión de la rama corregida:** `rexpo/create` (crear y editar la orden de cargue) daba 500 desde Laravel 10. `@php($x = …) @endphp` se compila a `<?php($x…)`, sin espacio, y PHP lo lee como la etiqueta corta `<?` y una llamada a `php()`. Se cambió a la forma de bloque; ninguna otra de las 679 vistas tiene el patrón.
- Probado con sesión de Brinsa y transacción revertida: actualizar la OC de Tenaris, borrar su detalle o su carga suelta, y abrir la orden de cargue de Cargill responden 404 sin escribir. Las pantallas propias responden 200.
- Sin tocar:
  - `actualizaDO_OC` toma el grupo del request (`?grupo=`), aunque ya no puede escribir en otro cliente.
  - `rtraerdatosproducto` y `rexdeldetpro` no coinciden con sus rutas (404 y error previos).
  - `lineaOrdenCargue` apunta a `ExpoController` (mayúsculas distintas: fallaría en Linux).
  - Alpina, Disan y Yara dan 500 al editar una OC: no tienen compañías activas y la plantilla anterior falla al armar el menú ("aplicaciones" on null).

**Cómo se convierten (decidido):**
1. **Primero la presentación, con la misma lógica.** Mismos `name`, `id`, valores y orden de campos; el mismo JavaScript. jQuery, select2 y moment se cargan **solo en estas pantallas** (`@push('scripts')`). Las plantillas HTML que arma el JavaScript para las filas nuevas se pasan a las clases del kit.
2. **Verificación sin escribir en la base:** en el navegador, `serializeArray()` del formulario con la vista anterior y con la nueva, sobre la misma OC, para varios clientes (Tenaris, Adama, Ternium, Koba, Cargill y los de ramas propias: 20, 3, 28, 7, 4, 27…). Si el envío es idéntico, el `update` recibe lo mismo. También se comprueba que cada botón que agrega filas genere los mismos `name`.
3. **Después, y por separado,** se quitan jQuery, select2 y DataTables (Alpine y `fetch`), pantalla por pantalla.

**Partes (un commit cada una):**

| Parte | Qué | Verificación |
|---|---|---|
| 0 | Seguridad de los endpoints | Lectura con otro grupo: 403/404 |
| 1 | `Po/edit` | `serializeArray()` igual antes y después en 5 o más clientes; filas agregadas iguales |
| 2 | `Po/create` | Igual, con el formulario vacío y tras "traer datos del DO" |
| 3 | `ExPo/edit` | Igual (Adama, Cargill) |
| 4 | `ExPo/create` | Igual; por su tamaño puede dividirse por secciones |
| 5 | Sin jQuery, select2 ni DataTables | Mismas pruebas |

**Parte 1, hecha (26/10/2026): `Po/edit` y el Livewire `po.contenedores-orden-compra`.**
- **Huella idéntica en 9 clientes** (Tenaris, Adama, Ternium, Koba, Cargill, ASERCOL, Sealed Air, Brinsa, Imusa). Se compararon los campos del formulario en orden (name, tipo, valor, opciones, required), los `onclick` y las funciones de JavaScript. También las filas que agregan `AgregarActividad`, `DuplicarActividad` y `AgregarCS`: mismos name, id, valores y contadores.
- El JavaScript es el mismo; solo cambian los íconos de dos plantillas HTML.
- Secciones Datos de la orden, Factura y declaración, Campos dinámicos, Control de contenedores y Productos. Las tres últimas son plegables (componente nuevo `x-seccion-plegable`), con `x-show`, así que los campos se envían aunque estén cerradas. Las tablas tienen cabecera fija. Actualizar y Cancelar quedan en una barra al pie.
- Librerías solo en esta pantalla (cdnjs):
  - jQuery 3.7.1 (planti traía 2.2.4);
  - select2 4.1.0-rc.0 (la misma versión);
  - moment 2.29.4.
- **Cambio de comportamiento:** el error previo "moment is not defined" (moment se cargaba después de usarse) queda corregido. Por eso `CalcularFechaDiasLibres()` ahora sí corre al abrir y recalcula la fecha de devolución de los contenedores que tengan fecha de descargue (descargue + días libres). Si esas fechas estaban desfasadas, el valor guardado puede cambiar al Actualizar.
- **Errores previos sin tocar:**
  - `txtTrm` duplicado con la etiqueta "Peso Neto BL";
  - `traerDatosDO` escribe en `#txtSelectividad`, que no existe;
  - cabeceras de Productos desalineadas en los grupos 6 y 27;
  - `DuplicarActividad` cambia mal dos ids;
  - `cboCentrodeCosto` sale cuatro veces con el mismo nombre;
  - `EliminarCS` muestra un `alert` de depuración.

**Parte 2, hecha (26/10/2026): `Po/create`.**
- Huella idéntica en los mismos 9 clientes, incluidas las filas de `AgregarActividad` y `DuplicarActividad`; sin errores de JavaScript. Mismo diseño que editar. Solo carga jQuery 3.7.1 y select2 (crear no usa moment).
- Errores previos sin tocar:
  - `DuplicarActividad` no renombra `cboMaterial`;
  - en la fila nueva, Material y Empaque quedan vacíos (`val(0)`);
  - `CalcularDetalle` tiene un `debugger;`;
  - el selector de material lleva todo el catálogo en cada fila (8.856 opciones en Tenaris).

**Ajuste del plan:** `ExPo/edit.blade.php` (1.887 líneas) no se usa: no existe `ExPOController@edit` y nada la abre. La orden de cargue se crea **y se edita** con `ExPo/create` (`rexpo/create/{id}`). Queda para borrar en L5. Las partes siguientes son 3, `ExPo/create` (3.893 líneas, por secciones), y 4, quitar jQuery, select2 y DataTables.

**Parte 3, hecha (26/10/2026): `ExPo/create` (crear y editar orden de cargue).**
- Abrir la pantalla no escribe: `create`, `Util::compania()` y `mostrarCampos()` solo leen.
- **Huella idéntica en 6 casos:** Adama nueva y tres órdenes con 4 contenedores; Cargill nueva y una existente. Son 88 a 316 campos por caso, comparados en orden con name, id, tipo, valor, opciones, required, readonly, disabled y eventos.
- **Filas probadas:** `AgregarContenedor`, `AddRowEmpty`, `AddEstiba`, `AgregarLotes` y `mostrarTab` generan los mismos campos e ids.
- **HTML previo mal formado:** el navegador sacaba de sus tablas `txtIdCont`, `txtIdContEstado`, `txtTamano` y `txtEstadoProducto{n}`. Se escribieron en esa misma posición para que el envío no cambie.
- **Sin lotes de prueba:** ninguna orden de ningún cliente tiene lotes activos, así que la lista de lotes guardados se copió tal cual sin datos para probarla.
- **Diseño:** tarjetas Datos de la orden y Factura; Campos adicionales y Contenedores plegables; cada contenedor en su tarjeta con las pestañas Información, Lotes y Ocultar; productos con cabecera fija; barra al pie con Limpiar, Guardar/Actualizar y Cancelar. Los tintes fijos verde y amarillo de pesos y gastos pasan a tintes que funcionan en oscuro.
- **Librerías (solo esta pantalla):** jQuery 3.7.1 (antes se cargaban tres versiones: 2.2.4, 2.1.1 y otra al final), select2 4.1.0-rc.0 y jQuery Mask 1.14.16. Se quitó bootstrap-select: no se usaba y causaba el error JS `Constructor`.
- **Corregido de paso:**
  - la plantilla imprimía el texto `$desactivado="disabled";` en la página (los campos nunca se desactivaban, y siguen igual);
  - el exceso de comprobación de perfil del commit `4c51b7af` en `mostrarCampos()` daba **403 a los usuarios de cliente** al abrir esta pantalla: se corrigió en `432e04c9`, probado con un usuario de Adama del perfil 16;
  - las subpantallas con parámetros (`rexpo/create/{id}`, `rpo/{id}/edit`) marcaban el módulo equivocado en la barra: `MenuNavegacion::actual()` ahora busca la pantalla padre recortando segmentos y marca su módulo y grupo.
- **Errores previos sin tocar:**
  - `AddRowEmpty` (botones Producto y Estiba) falla con `valores.split` porque `#cboProducto` no existe: agrega la fila pero no llama a `/rudpcontenedor`;
  - en los contenedores guardados, el div de lotes tiene una comilla de más en el id (`t1'contenidoLotes`) y "+ Lote" no hace nada;
  - un contenedor nuevo no tiene `txtIdContEstado`, así que "Eliminar contenedor" fallará;
  - `actualizaMat` recibe el número de contenedor en vez del de fila;
  - las cabeceras de producto solo cuadran para Adama.

**Para el responsable:** guardar una OC de prueba del grupo 2 después de cada parte (crear, editar, agregar y eliminar filas de actividades, productos y carga suelta).

#### L3 por uso: Detalle de OC, Pedidos WSIMEX, Archivos planos y Control de contenedores (26/10/2026)

Siguientes pantallas Bootstrap por uso tras la OC. Seguimiento RIM v1 queda fuera (ver arriba).

| Pantalla | Clics (180 d) | Probada con | Errores previos corregidos |
|---|---|---|---|
| Detalle de órdenes de compra (`rpores`, `searchpores`) | 465 | Adama | Sin fugas. El color de STATUS podía salir de la fila anterior. Se conservan filtros, columnas dinámicas, seguimiento con doble clic y Excel. Ya tardaba ~7 s en cargar (consulta de columnas dinámicas, sin tocar). Pendiente: su gemela de exportación (`rexpores`) |
| Pedidos WSIMEX (`ordenescomprawsimex`) | 443 | Ternium | **La importación aceptaba un DO de cualquier importador de Websimex y lo grababa en el grupo en sesión** (con proveedores, materiales y contenedores): ahora solo los importadores del grupo. Sin DO válido respondía "Pedido Importado" y además ponía en `status = 0` los contenedores del grupo. Confirmación en diálogo Alpine (antes SweetAlert y axios); el cuerpo enviado es idéntico. Sin tocar: "ya existe" no mira el grupo |
| Archivos planos (`archivosPlanosImusa`) | 407 | Imusa | **Cualquier usuario con sesión pedía los planos de cualquier DO**: ahora el DO debe ser de una compañía del grupo. Fuera del repositorio: el servicio de Central que genera los planos (`central.asercolgroup.com/imusa/archivoplano`) responde a cualquiera por URL |
| Control de contenedores, versión anterior (`control-contenedores`) | 311 | Tenaris | **`id_grupo_empresarial` era pública y editable** (se veían contenedores de otro cliente) y seis acciones aceptaban pedidos y contenedores ajenos: `#[Locked]` y validación contra el grupo. SQL de búsqueda y fechas con parámetros. Los productos se consultaban para toda la base |

**Control de contenedores:**
- **Lista:** se pagina de a 50 por pestaña; antes, la lista en curso iba de a 10 con enlaces que perdían los filtros y el histórico salía completo (28,7 MB de HTML con Tenaris, ahora 0,7 MB). La lista queda en caché 5 minutos y Consultar la refresca.
- **Semáforo de documentos:** hacía una consulta a `cti` por fila (~13 s con 50 filas). Ahora `Semaforo::estados()` calcula la página en dos consultas y el componente recibe el estado; si no lo recibe, como en Programación de arribos, consulta como antes. Comparado con el cálculo fila por fila en 335 DO (Sin subir, Pendientes, Sin Información): idéntico. `documentos_aws` guarda DO con espacios al final; las claves se normalizan como lo hace MySQL.
- **Tiempo con Tenaris:** 15 s → 4,2 s con la lista en caché.
- **Diálogos:** notificación, asignación de transportador y documentos pasan a Alpine.
- **Otros cambios de presentación:** el semáforo compartido solo cambió su rama `origen == 1`. El formulario del contenedor usa `wire:model` (antes `.live`, que consultaba en cada tecla).

**Probado (solo lectura, navegador, claro y oscuro, sin errores JS ni 500):** las cuatro cargan con el kit; página 2 en `rpores`, `ordenescomprawsimex` y Control de contenedores; cabecera fija.

**Para el responsable (grupo 2):**
- Pedidos WSIMEX: importar un DO y comprobar que un DO de otro cliente da error.
- Archivos planos: vista previa y descarga.
- Control de contenedores: Registrar, citas, productos, asignar transportador, notificar (envía correos), importar contenedores y sincronizar CTI, y que el diálogo de documentos siga consultando Central desde el layout nuevo.

#### L3 por uso, segundo lote (26/10/2026)

| Pantalla | Probada con | Errores previos corregidos |
|---|---|---|
| KPI de importación y exportación (`rkpi`, `rexkpi`) | Adama (medición 17, 11 filas) e Imusa | Vista común `KPI/_contenido` para las gemelas. `searchkpi` sin medición daba 500 (ahora 404). El id de medición y el nombre de la fila iban crudos en el `ondblclick` (inyección JS). La gráfica (Chart.js 2, solo en esta pantalla) toma los colores del tema y se redibuja al cambiarlo. Se agregaron indicadores Registros, Total, Cumple y No cumple |
| Definición de campos de la OC y de la orden de cargue (`rdefcampospo`, `rexdefinicioncampospo`) | Sealed Air y Brinsa | **`update` (impo) y `store` con id (expo) escribían el registro de otro cliente**; editar en expo abría registros ajenos. La búsqueda de expo daba 500 (columnas inexistentes, `orWhere` suelto, faltaban listas) |
| Sincronizar pedidos de exportación (`expo/sincronizar-pedidos`) | Cargill | **`id_grupo_empresarial` e `id_wsimexGE` eran públicas y editables** (se escribía en otro grupo y se leían otros importadores): `#[Locked]`. Los duplicados se buscaban en todos los grupos. La compañía se buscaba por NIT sin grupo |
| Detalle de órdenes de cargue (`rexpores`) | Adama (solo pintura) | Gemela de `rpores`; sin fugas. El doble clic abría `/rexbactivivad`, que no existe |

**Detalle de órdenes de cargue no carga:** la consulta (`EXSqlPOGeneral("Detalle")`, unas 20 uniones, varias a `asercolg_estadisticas`) tarda ~90 s con Adama y el servidor corta a los ~30 s (502). Ya era así; explica sus 2 clics en 180 días. Causa (EXPLAIN): `ex_po_detalle` no tiene índice en `id_contenedor` y se recorre completa por cada contenedor. Se propone el índice del paso 5 de §7 (sin aplicar). Se descartó una reescritura en dos pasos del SQL (expresiones regulares sobre el SQL armado, con varios caminos de respaldo): frágil para una pantalla con 2 clics en 180 días; queda guardada fuera del repositorio por si se retoma.

**Pendientes para decidir (previos, sin tocar):**
- Definición de campos de exportación: "Eliminar" no borra (`$obj->delete()` comentado) aunque dice "eliminado".
- Definición de campos de importación: editar deja vacía la Clase (el formulario de edición no tiene `txtClase` y `update` la asigna).
- Sincronizar pedidos: la importación falla si el incoterm no existe (`facturacionWsimex` en vez de `facturasWsimex`) o si el DO no tiene documento de transporte, y en ambos casos muestra la excepción completa (`dd($e)`).
- KPI de importación: con el grupo 6, cada búsqueda envía datos a `central.asercolgroup.com/api/registrarTablaDinamica`.
- `session('SQLPO')`: la búsqueda de `rexpores` guarda su SQL en la clave de importación.

**Probado (navegador, solo lectura, claro y oscuro, sin errores):** KPI con datos en las dos gemelas, Definición de campos en las dos, Sincronizar pedidos. `rexpores` responde `200 kit` desde PHP (sin límite de tiempo); en el navegador, 502 por el tiempo.

**Para el responsable (grupo 2):** crear, editar y eliminar campos en las dos definiciones; importar pedidos y sincronizar contenedores con un rango corto; KPI: Generar reporte y el detalle con doble clic.

#### L3 por uso, tercer lote: importadores, integración, declaraciones y pantallas sueltas (26-27/10/2026)

| Pantalla | Probada con | Errores previos corregidos |
|---|---|---|
| Importadores de archivos (`rimportpo`, `imporitemlegajo`, `reximportpo`, `reximportarifa`, `reximportocargue`) | Koba, Ternium, Brinsa, Adama | Sin fugas: se revisaron las 21 clases de `app/Imports` que usan. Los mensajes de éxito y de error no se mostraban. Tarjeta común `Po/_importar-archivo`; botón desactivado mientras importa |
| Integración Ternium (`integracion/*`) | Ternium | **Consultas y envíos a Ternium aceptaban DO de cualquier cliente**; `PDFtoBase64` descargaba cualquier URL del formulario y la enviaba a Ternium: ahora 403 fuera del grupo 20, DO de los importadores del grupo y documento de `documentos_aws` de ese DO. Inyección SQL en `integracion_data_dec_impo_pdf`. Tablas HTML del controlador a partials escapados. jQuery solo en esas pantallas |
| Declaraciones juramentadas (`declaraciones`) | Imusa | **Cualquier grupo podía escribir**: `new`, `store` y `export` exigen el grupo 32 |
| Abastecedores (`gruposeb/abastecedor`) | Imusa | **La tabla no tiene grupo y cualquier cliente la veía y editaba**: `exigirImportadores()`. Bloquear, editar, guardar y actualización masiva usaban ids o la propiedad pública `$GE`. Inyección SQL con ids del Excel. Modales Bootstrap a Alpine |
| Tarifas e itinerario de exportación (`ExPoTarifas`) | Adama | Guardar, editar y eliminar aceptaban tarifas de cualquier cliente |
| Gestión programación de arribos (`visibilidad`) | Koba | Ocho acciones (asignar transportador, órdenes de servicio, notificaciones, granel) aceptaban registros de cualquier cliente. HTML armado en el controlador a partials escapados; `public/js/programacion-arribos.js` (jQuery, select2, SweetAlert, axios) reemplazado por Alpine y `fetch`. Los filtros superiores no hacían nada (ahora filtran) y "Total de pedidos" daba 0 |
| Auditoría ETA (`auditorias`) | Adama | La página 2 perdía las fechas |
| Reporte ISF (`orden`) | Adama | Sin fugas |
| Visibilidad operación (`cia/clientes`) | Adama | Consulta del usuario con parámetro enlazado |
| Poderes (`rpoderes`) | Koba | Editar y guardar con id aceptaban poderes de cualquier cliente |
| Menús (`menues`) | ASERCOL | **Cualquier usuario con sesión administraba los menús de todos los clientes**: exige el perfil 1 (como su menú 158), comprobado en cada petición |

**Probado (navegador, solo lectura, claro y oscuro, sin errores):** las once cargan con el kit; página 2 en Declaraciones y Auditoría. Con un usuario de Adama del perfil 16, Menús y Abastecedores responden 403.

**Gestión programación de arribos optimizada (27/10/2026):** tardaba ~5 min con Koba (1.766 consultas): la orden de servicio de cada contenedor (y su transportador en `cti`), el depósito y el tipo de carga de Websimex se cargaban fila por fila, y la orden de servicio vigente se consultaba por pedido. Ahora se precargan y la orden vigente se calcula una vez por transportador (conservando que solo se usaba el primer transportador de la lista). Tabla idéntica byte a byte con Koba (319 pedidos: 1.747 consultas y 308 s → 39 y 7,2 s) y Tenaris (195: 231 y 44,6 s → 37 y 6,6 s). Pantalla completa con Koba: 327 s → 13,5 s (59 consultas); en el navegador ~19 s, sin errores.

**Errores previos sin tocar:**
- Abastecedores: `guardarRegistro` tiene un `dd()` (editar un registro de la base nunca guardó).
- Integración: `/ws/envioFacturas` no tiene método; `envioquimica` no aplica el filtro de pedido; en PDF, `enviarDocumento()` da error de JavaScript.
- Menús: `menuAdd` termina en `dd()`.
- Declaraciones: la paginación pierde el filtro de fechas.

**Para el responsable (grupo 2 o el cliente, con cuidado: Integración envía a la API real de Ternium):** importar cada plantilla; enviar química, declaración y PDF a Ternium; guardar una declaración (Imusa) y el Excel; Abastecedores (crear, bloquear, actualización masiva, importar plantilla, notificar); tarifas; en arribos, asignar transportador, órdenes de servicio y notificaciones.

### Fase L4 — Selector de empresa

Ver §4. Puede ir en paralelo con L2 y L3.

### Fase L5 — Limpieza

Se retiran Bootstrap, `layouts.planti`, `layouts.tailwind`, `planti-contenido`, los componentes prohibidos (§6.4), la carga de Font Awesome (si ya ninguna vista la usa) y las claves de sesión heredadas (`color`, `colorp`, `colors`, `color_tw_p`…).

Si el registro de L1.5 quedó limpio, se activa el bloqueo por ruta (§5.3, paso 2).

---

## 4. Selector de empresa (D2)

### 4.1 Qué hace

- **Universo:** las compañías que el login ya calcula en `session('companias')`: las del grupo y, si el grupo es tercero (`es_tercero`), las de `companias_tercero`.
- **Selección:** "Todas" (por defecto) o una sola compañía del universo (decidido 27/10/2026; antes "una o varias"). Se guarda en la sesión (`companias_seleccionadas`) y dura lo que dura la sesión.
- **Visible solo si** el universo tiene más de una compañía **y** el usuario tiene el permiso `empresa-seleccionar`. Hoy aplica a 10 grupos: 1, 2, 8, 20, 22, 28, 36, 39, 41 y 45. El grupo 45 tiene 35 compañías, así que el selector necesita búsqueda.
- **Nunca** cambia `GrupoEmpresarial` ni amplía el universo.

### 4.2 Cómo se implementa (decidido 24/10/2026)

En lugar de restringir las consultas (lo que obligaba a refactorizar 92 archivos), la selección **preselecciona** el filtro de compañía que cada módulo ya tiene. Unos 12 componentes Livewire tienen ese filtro, cada uno con su propio nombre (`compania`, `idCompania`, `company`, `importadorSeleccionado`, `filtroCompania`…) y guardando `compania.id` o `id_websimex`.

| # | Tarea |
|---|---|
| L4.1 | Trait de preselección: en `mount()`, si el filtro del componente está vacío, le pone la compañía elegida, traducida al valor que use ese componente. Cada componente declara su propiedad y su tipo de valor. La compañía elegida se valida contra `session('companias')`. |
| L4.2 | Aplicar el trait a los componentes con filtro de compañía, con su gemelo de exportaciones. |
| L4.3 | Componente `SelectorGrupo` (Alt + S, ↑ ↓ ↵, búsqueda), con la autorización `can('empresa-seleccionar')` comprobada también en el servidor al guardar. Al cambiar de empresa, la pantalla abierta se recarga. |
| L4.4 | Pantallas Bootstrap con combo de compañía (`compania`, `cboCompania`): se preseleccionan al migrarlas en L3. |
| L4.5 | Verificación: con un usuario del grupo 2, cada módulo con filtro abre con la compañía elegida; con "Todas", igual que hoy. |

**Límite conocido:** los módulos sin filtro de compañía siguen mostrando todas las compañías del grupo. El aislamiento entre clientes no cambia: sigue dependiendo solo del grupo empresarial.

### 4.3 Avance de L4 (27/10/2026)

- **L4.1:** `App\Services\SeleccionEmpresa` (universo, selección en `companias_seleccionadas`, `disponible()` = permiso + más de una compañía; guardar exige el permiso y descarta lo que no sea del universo) y el trait `PreseleccionaCompania` (`preseleccionarCompania(['propiedad', 'valor' => id|id_websimex|razon_social, 'multiple', 'opciones', 'vacio'])`, llamado al final de `mount()`; solo actúa con el filtro vacío).
- **L4.3:** `Layout\SelectorGrupo` bajo "Empresa activa": búsqueda, casillas, Alt + S, ↑ ↓, Enter, Escape; Aplicar guarda y recarga la pantalla. Solo se monta si el usuario puede usarlo.
- **L4.2, preselección aplicada a 10 componentes:**

| Componente | Propiedad | Valor |
|---|---|---|
| Dashboard de importaciones (`ImportacionesAdmin`) | `idCompania` | id_websimex (sin selección sigue eligiendo la primera) |
| Dashboard v2 (`Tablero`) | `idCompania` | id_websimex (ídem) |
| Detalle de la OC (`DetalleOrdenCompra`) | `compania` | razón social |
| Reporte de control de contenedores (`ContenedoresReporte`) | `importadorSeleccionado` | id_websimex |
| Pedidos en Curso (`OrdenesCompra`) | `idCompania` | id_websimex (acota los segmentos como si el usuario eligiera) |
| Indicadores KPI V2 | `idCompania` | id_websimex |
| Reporte global Brenntag | `importadorSeleccionado` | id_websimex |
| Reportes específicos | `idCompania` | id_websimex |
| Seguimiento RIM v2 | `filtroCompania` | id_websimex |
| Regalías | `idCliente` | id_websimex |

  Probado en cada uno, con el permiso concedido solo en el proceso de prueba: sin selección queda como hoy; con una compañía, sale preseleccionada; con dos, el filtro queda en "Todas". El selector aparece solo con el permiso.
  Excluidos: formularios donde la compañía es un dato del registro (crear/editar OC, Declaración de Cambio), `seguimientoctc` (global) y componentes sin filtro real de compañía (Pedidos en Curso de exportación, DTA, ZF, Sistemas especiales…).
- **Una sola compañía (decidido 27/10/2026):** ningún filtro de los módulos admite varias, así que el selector pasó a opciones únicas ("Todas" o una compañía) y el servidor guarda como mucho una.
- **Quién lo tiene (decidido 27/10/2026):** rol `in-house` para todos los usuarios activos (`estado = 1`) que no son admin ni directivo: 319 hoy. `empresa-personalizar` sin rol.
- **Fuga corregida de paso:** el Reporte de control de contenedores (`impo/control-contenedores/reporte`) listaba todos los importadores de Websimex fuera de los roles 29/31/32 (la sesión `app_importadores` nunca se escribe) y consultaba el elegido: se veían contenedores de cualquier cliente. Ahora el universo son las compañías del grupo y un importador fuera de él no trae nada.
- **Pendiente para activarlo:** aplicar el SQL de §7 (permisos, rol `in-house`, usuarios) y `php artisan permission:cache-reset`. Hasta entonces nadie ve el selector y todo funciona como hoy.

---

## 5. Permisos (D4)

### 5.1 Reparto

| Pregunta | Quién la responde |
|---|---|
| ¿Qué **pantallas** ve el usuario? | Perfiles + `compania.aplicaciones`, como hoy: `menus.perfiles` por perfil (`$.perfil`) o por usuario (`$.usuario`), administrado desde `Menues`. |
| ¿Puede hacer esta **acción**? (elegir empresa, personalizar colores y, más adelante, editar, anular o aprobar) | spatie/laravel-permission, con permisos asignados por rol. |

Una misma pregunta nunca la responden los dos modelos.

### 5.2 Por qué no se migra el modelo de perfiles a spatie

El modelo actual tiene 60 perfiles, unos 470 usuarios y 153 pantallas, y 55 de ellas tienen además excepciones por usuario. Llevarlo a spatie obliga a:
- convertir esa matriz, con sus excepciones, en permisos y roles;
- reasignar a todos los usuarios;
- reescribir el administrador de menús.

No aporta nada funcional y va contra la regla 1.2.5. Un error dejaría a un cliente viendo pantallas que no debe o sin las suyas.

Si en el futuro se quiere un solo modelo, la conversión es mecánica: un permiso por pantalla (`menu.{id}`) y un rol por perfil. Sería un proyecto aparte.

**Datos heredados de la versión que no salió:**
- Los 25 permisos (`importaciones-ver`, `menus-ver`…) **no se usan**: duplican lo que ya resuelve el menú y crearían dos fuentes de verdad. Se borran más adelante, con SQL que se aplica a mano.
- Los roles `admin` (5 usuarios) y `directivo` (1) se pueden aprovechar para los permisos nuevos.

### 5.3 Control de acceso por ruta

Hoy el menú oculta pantallas, pero las URLs siguen abiertas: el chequeo de `Authenticate` está comentado y unas 90 rutas no tienen `auth`. Con `MenuNavegacion`, la misma lógica del menú puede proteger las rutas:

1. **Paso 1, modo registro (L1.5):** se anotan en el log los accesos que se negarían, sin negarlos. Así aparecen las subrutas que no están en el menú (ediciones, exportaciones, AJAX).
2. **Paso 2, bloqueo (L5):** se activa cuando el registro queda limpio.

`Util::acceso()` **no** se reactiva tal cual, porque tiene dos fallos:
- `A OR B AND estado = 1` sin paréntesis deja pasar pantallas con `estado = 0`.
- No filtra por `compania.aplicaciones`.

Además, concatena valores en el SQL.

Las cinco comprobaciones de perfil que están fijas en el código se mantienen hasta que se toque su pantalla: `perfil==1` en Etapas y ExEtapas, `!=14` en la vista de Torre de Control y `==20` en `TorreControlController`.

---

## 6. Verificación

- **Prueba de humo** (`migracion:humo`): tras cada fase y cada módulo de L3, con 0 regresiones de estado.
- **La comparación de HTML contra `main` deja de servir** desde L1, porque cambia el marcado. La comparación pasa a ser **visual**, pantalla por pantalla:
  - en modo claro y en modo oscuro;
  - con el grupo 2 y con otro grupo de colores muy distintos;
  - a 360 px y en escritorio.
- **Datos:** en cada pantalla migrada, los conteos y totales coinciden con la versión anterior (mismo filtro y mismo grupo).
- **Aislamiento:** en cada pantalla se comprueba explícitamente que no aparecen datos de otro grupo.

---

## 7. SQL a aplicar a mano

Se ejecuta en `asercolg_po` cuando llegue L0.7, después de revisar las columnas reales de las tablas de spatie.

```sql
-- 1. Interruptor "Módulos en gris"
ALTER TABLE grupo_empresarial
  ADD COLUMN modulos_neutros TINYINT(1) NOT NULL DEFAULT 0 AFTER color_s;

-- 2. Permisos del layout
INSERT INTO permissions (name, guard_name, created_at, updated_at) VALUES
  ('empresa-personalizar', 'web', NOW(), NOW()),
  ('empresa-seleccionar',  'web', NOW(), NOW());

-- 3. Rol in-house con el selector de empresa (P1, decidido 27/10/2026)
INSERT INTO roles (name, guard_name, created_at, updated_at) VALUES ('in-house', 'web', NOW(), NOW());

INSERT INTO role_has_permissions (permission_id, role_id)
SELECT p.id, r.id FROM permissions p JOIN roles r ON r.name = 'in-house' AND r.guard_name = 'web'
WHERE p.name = 'empresa-seleccionar' AND p.guard_name = 'web';

-- 4. Usuarios con el rol: todos los activos (estado = 1) que no son admin ni directivo (decidido 27/10/2026).
--    model_type debe ser 'App\\User' (el modelo de autenticación). Los admin/directivo actuales están en
--    model_has_roles con 'App\\Models\\User' (filas heredadas que no aplican); se excluyen por id con cualquier model_type.
INSERT INTO model_has_roles (role_id, model_type, model_id)
SELECT r.id, 'App\\User', u.id
FROM users u
JOIN roles r ON r.name = 'in-house' AND r.guard_name = 'web'
WHERE u.estado = 1 AND u.deleted_at IS NULL
  AND u.id NOT IN (SELECT mr.model_id FROM model_has_roles mr JOIN roles r2 ON r2.id = mr.role_id
                   WHERE r2.name IN ('admin', 'directivo'));
```

`empresa-personalizar` (colores de la empresa) queda creado pero sin rol (decidido 27/10/2026).

```sql
-- 5. Índice para Detalle de órdenes de cargue (rexpores, ~90 s con Adama). EXPLAIN: ex_po_detalle se recorre
--    completa (type=ALL, ~11.400 filas) por cada contenedor (~15.000) porque no tiene índice en id_contenedor.
--    Solo añade un índice: no cambia datos. Medir rexpores después de crearlo.
CREATE INDEX idx_ex_po_detalle_id_contenedor ON ex_po_detalle (id_contenedor);
```

Los usuarios que se creen después no reciben el rol solos: hay que asignarlo al crearlos (o repetir el paso 4, que no duplica si se añade `AND u.id NOT IN (SELECT model_id FROM model_has_roles WHERE role_id = r.id)`).

Después de cualquier cambio en las tablas de spatie hecho por SQL: `php artisan permission:cache-reset`.

Más adelante (cuando se decida, §5.2): borrar los 25 permisos heredados y sus filas en `role_has_permissions`.

Los cambios de `menus.ruta` de L2 y L3 se van agregando aquí, pantalla por pantalla.

---

## 8. Pendientes

| # | Pendiente |
|---|---|
| P1 | **Decidido (27/10/2026):** rol nuevo `in-house` con `empresa-seleccionar` (SQL en §7). Pendiente: a qué usuarios se asigna, y quién recibe `empresa-personalizar`. |
| P2 | DataTables y select2: reemplazarlos o envolverlos con estilos de tokens (se decide en la primera pantalla de L3 que los use). |
| P3 | Orden de los módulos en L3 (propuesta: Importaciones → Exportaciones → Dashboard → CIA → Administración). |
| P4 | Si la selección de empresa debe recordarse entre sesiones (hoy: no). |
