# Base de conocimiento — Seguimiento RIM

**Estado:** Documentación del módulo vigente + inventario de hallazgos para una V2
**Fecha:** 2026-09-09
**Módulo:** Operaciones → Seguimiento RIM
**Componente:** [SeguimientoRim.php](../app/Http/Livewire/Rim/SeguimientoRim.php) (220 líneas)
**Gemelo estadístico:** [EstadisticasRim.php](../app/Http/Livewire/Rim/EstadisticasRim.php)

> Todas las cifras de este documento fueron medidas contra las bases reales el 2026-09-09
> (conexión `websimex` → `comex` en `awsslave.asercol.com:3307`, MySQL 5.7.42) usando el
> grupo empresarial **41** (4 compañías: importadores `2436, 2437, 2438, 2439`), que es el
> tenant con más compañías activas. El procedimiento para reproducirlas está en §10.

---

## 0. Resumen ejecutivo

El módulo muestra, en una sola pantalla, los **registros de importación (RIM) aprobados** de las
compañías del cliente conectado, clasificados por cercanía a su vencimiento, con cinco tarjetas de
conteo que actúan como filtro. Es un módulo **de solo lectura**: no escribe en ninguna base, no
tiene acciones, no exporta.

Funciona y está en producción para 38 de las 39 compañías activas. Pero el análisis encontró tres
cosas que condicionan cualquier V2:

1. **La métrica central está invertida.** Lo que el módulo llama `saldo_total` es
   `SUM(CANTIDAD − CANTIDAD_DISPONIBLE)`, es decir la cantidad **ya consumida** del RIM, no el saldo
   disponible. La consecuencia visible es que la tarjeta y la etiqueta *"Sin saldo"* marcan en rojo
   exactamente los RIM **intactos** (100 % disponibles), mientras los RIM **agotados** aparecen como
   "Vigente". Evidencia en §7 · D1.
2. **Las tarjetas y la tabla cuentan universos distintos.** Las tarjetas cuentan RIM únicos; la tabla
   lista líneas de producto. Para el grupo 41: tarjeta "Críticos" = **34**, filas críticas en la tabla
   = **116**. La tarjeta "Todos" suma **367** y el listado por defecto trae **1 058 filas / 378 RIM**.
   Evidencia en §7 · D2.
3. **Cada interacción cuesta ~2,4 s.** Seis consultas por render, subconsulta dependiente evaluada dos
   veces por fila, `select('*')` sobre tres tablas unidas (**217 columnas**, ~230 000 atributos
   hidratados) y `wire:model` sin `.defer` — cada tecla del buscador vuelve a correr todo. §6 y §7 · P1-P5.

Ninguno de los tres es un error de sintaxis ni rompe la pantalla: por eso llevan tiempo ahí. El (1)
requiere confirmación de negocio antes de tocarlo, porque cambia el significado de una columna que
los usuarios ya leen.

---

## 1. Piezas del módulo

| Archivo | Líneas | Rol |
|---|---|---|
| [app/Http/Livewire/Rim/SeguimientoRim.php](../app/Http/Livewire/Rim/SeguimientoRim.php) | 220 | Toda la lógica: consulta, clasificación y conteos |
| [resources/views/livewire/rim/seguimiento-rim.blade.php](../resources/views/livewire/rim/seguimiento-rim.blade.php) | 181 | Tabla + tarjetas + convención de colores |
| [resources/views/rim/rim-seguimiento.blade.php](../resources/views/rim/rim-seguimiento.blade.php) | 41 | Vista de contenido (Tailwind CDN) que monta el componente |
| [resources/views/rim/rim-seguimiento-iframe.blade.php](../resources/views/rim/rim-seguimiento-iframe.blade.php) | 22 | Vista contenedora (shell Bootstrap `layouts.planti` + iframe) |
| [resources/views/components/rim-card.blade.php](../resources/views/components/rim-card.blade.php) | 27 | Tarjeta de conteo clicable (compartida con nada más) |
| [resources/views/components/input-search.blade.php](../resources/views/components/input-search.blade.php) | 16 | Caja de búsqueda (compartida por ~15 módulos) |

**Rutas** ([routes/web.php:927-928](../routes/web.php#L927-L928)):

```
GET /rim/seguimiento        → rim.rim-seguimiento-iframe   name: rim-seguimiento        [auth]
GET /rim/show-seguimiento   → rim.rim-seguimiento          name: rim-seguimiento-show   [auth]
```

Ambas son `Route::view` — no hay controlador. El patrón es el estándar del repo (iframe + Livewire,
§"Cómo agregar un módulo" de [CLAUDE.md](../CLAUDE.md)).

**Menú** (tabla `menus`, base `asercolg_po`):

| id | nombre | ruta | depende | orden | estado |
|---|---|---|---|---|---|
| 171 | Seguimiento RIM | `/rim/seguimiento` | 3 (*Operaciones*) | 17 | 1 |
| 170 | Estadísticas RIM | `/rim/stats` | 141 | 15 | 1 |

El menú 171 está habilitado para prácticamente todos los perfiles (1–56, 58) y presente en el JSON
`compania.aplicaciones` de **38 de 39** compañías activas. El 170 (estadísticas, uso interno) está
restringido a 28 perfiles y a 10 usuarios nominales.

**Historia git:** `97a4ba34` (módulo inicial) → `ec7f7fe9` (ajuste de conteos) → cambio no commiteado
en `mount()` para soporte de terceros (§4).

---

## 2. Contexto de negocio

Un **RIM** (Registro/Licencia de Importación ante el MinCIT) autoriza importar una lista de productos
por cantidades y valores determinados, y **vence**. Puede prorrogarse hasta **dos veces**. Mientras el
RIM esté vigente y tenga cantidad disponible, se puede seguir declarando mercancía contra él; cuando
vence o se agota, la operación se detiene hasta conseguir uno nuevo.

De ahí la razón de ser del módulo: **avisar antes de que un RIM venza**, para que el cliente pida
prórroga o tramite un nuevo registro a tiempo.

Dos precisiones importantes sobre el alcance:

- La **fuente de la verdad es Websimex** (`comex`). El CTC solo lee. El módulo no crea ni modifica RIM
  ni prórrogas.
- El **flujo operativo de solicitud** de RIM y de prórrogas vive en el sistema CTI
  (`asercolg_bitacora`, tablas `solicitud_registros_importacion`, `solicitud_rim_prorroga`,
  `detalle_solicitud_registros_importacion`) y en el CTC solo lo consume
  [EstadisticasRim.php](../app/Http/Livewire/Rim/EstadisticasRim.php) para medir SLA de los asistentes.
  **Seguimiento RIM no está conectado con ese flujo** — ver §8 · U3.

---

## 3. Modelo de datos

### 3.1 Tablas involucradas (todas en `websimex`, solo lectura)

| Tabla | Modelo | Filas | Rol |
|---|---|---|---|
| `registros_importacion` | [RegistrosImportacionWS](../app/Models/RegistrosImportacionWS.php) | 39 989 (36 004 aprobados) | Cabecera del RIM |
| `detalles_ris` | [DetalleRegistrosImportacionWS](../app/Models/DetalleRegistrosImportacionWS.php) | 138 337 | Líneas de producto del RIM |
| `importadores` | — (join crudo) | — | Razón social del cliente |

El componente **no usa las relaciones Eloquent** de los modelos: hace `join()` manual con
`detalles_ris` e `importadores`.

### 3.2 Columnas que el módulo realmente lee

**`registros_importacion`** (63 columnas en total):

| Columna | Tipo | Uso |
|---|---|---|
| `ID_REGISTRO` | `bigint` PK | Llave del RIM |
| `NUMERO_DO` | `varchar(15)` idx | Se muestra; **string, nunca castear** |
| `ID_IMPORTADOR` | `bigint` idx `RI_ID1` | **Filtro de tenant** |
| `NUMERO_APROBACION` | `varchar(25)` sin índice | Se muestra y se usa para el `COUNT(DISTINCT)` |
| `FECHA_APROBACION` | `datetime` sin índice | `NOT NULL` = "el RIM existe de verdad" |
| `FECHA_VENCIMIENTO` | `datetime` sin índice | Base de la fecha real |
| `FECHA_PRORROGA` | `datetime` sin índice | 1.ª prórroga (nula en 31 475 de 36 004 aprobados) |
| `FECHA_PRORROGA2` | `datetime` sin índice | 2.ª prórroga (nula en 33 119 de 36 004) |

**`detalles_ris`** (38 columnas):

| Columna | Tipo | Uso |
|---|---|---|
| `ID_REGISTRO` | `bigint` idx `DETALLES_RIS_ID6` | Join con la cabecera |
| `ID_PRODUCTO` | `bigint` idx `DETALLES_RIS_ID3` | Correlación de "RIM sustituto" (§5.5) |
| `NOMBRE_COMERCIAL` | `varchar(255)` sin índice | Se muestra y se busca |
| `CANTIDAD` | `decimal(12,2)` NOT NULL | Cantidad autorizada |
| `CANTIDAD_DISPONIBLE` | `decimal(12,2)` NULL | **Cantidad que queda por usar** (§3.3) |

**`importadores`** (116 columnas): solo `ID_IMPORTADOR` (join) y `NOMBRE_IMPORTADOR` (mostrar/buscar).

### 3.3 Semántica de `CANTIDAD_DISPONIBLE` — verificada con datos

| Condición | Filas | % |
|---|---|---|
| `CANTIDAD_DISPONIBLE = CANTIDAD` (RIM intacto) | 34 964 | 25,3 % |
| `0 < CANTIDAD_DISPONIBLE < CANTIDAD` (consumo parcial) | 48 531 | 35,1 % |
| `CANTIDAD_DISPONIBLE = 0` (agotado) | 54 542 | 39,4 % |
| `CANTIDAD_DISPONIBLE > CANTIDAD` | **0** | 0 % |
| `CANTIDAD_DISPONIBLE IS NULL` | 295 | 0,2 % |

Que **ninguna** fila tenga disponible > cantidad, y que el 25 % tenga disponible = cantidad, confirma
que `CANTIDAD_DISPONIBLE` es el **saldo remanente** que decrece con cada declaración; no la cantidad
consumida. Esto es la base del hallazgo D1 (§7).

### 3.4 Índices disponibles

`registros_importacion` tiene 17 índices, **todos de una sola columna**: PK, `ID_IMPORTADOR`,
`NUMERO_DO`, y catálogos (`CODIGO_*`, `ID_EXPORTADOR`, …). **Ninguna de las tres fechas de vencimiento
está indexada**, ni `FECHA_APROBACION`, ni `NUMERO_APROBACION`.

`detalles_ris` tiene 8: PK, `ID_REGISTRO`, `ID_PRODUCTO`, `CODIGO_POSICION`, `CODIGO_SUCURSAL`, etc.

Consecuencia: el plan de ejecución siempre entra por `ID_IMPORTADOR` (`RI_ID1`) y todo el filtrado por
fecha es post-filtro en memoria. Con 40 k filas eso es tolerable; el costo real está en otra parte (§6).

Plan medido de la consulta principal (`EXPLAIN`):

```
1 PRIMARY            importadores           range  PRIMARY              rows 4     Using where; Using index
1 PRIMARY            registros_importacion  ref    RI_ID1               rows 222   Using where
1 PRIMARY            detalles_ris           ref    DETALLES_RIS_ID6     rows 3     Using index
2 DEPENDENT SUBQUERY dr                     ref    DETALLES_RIS_ID6     rows 3
```

La línea 2 es la clave: **subconsulta dependiente**, reejecutada por cada fila del resultado — y en la
consulta real aparece **dos veces** (una en el `CASE`, otra en la columna `saldo_total`).

---

## 4. Aislamiento multi-cliente

`mount()` ([SeguimientoRim.php:34-50](../app/Http/Livewire/Rim/SeguimientoRim.php#L34-L50)) resuelve la
lista de importadores del tenant y la guarda en `$this->compania`:

```php
$compania = Compania::when(session('es_tercero'), function ($query) {
        $query->whereIn('id_websimex', session('companias'));
    })
    ->when(!session('es_tercero'), function ($query) {
        $query->where('id_grupo_empresarial', $this->idGrupoEmpresarial);
    })
    ->where('inactivo', 0)
    ->get();
$this->compania = $compania->pluck('id_websimex')->toArray();
```

Dos caminos:

- **Cliente normal:** compañías activas de su `id_grupo_empresarial`.
- **Tercero** (`grupo_empresarial.es_tercero = 1`): las compañías cuyos `id_websimex` ya vienen
  precalculados en `session('companias')` — sembrado en
  [login.blade.php:52-64](../resources/views/login.blade.php#L52-L64) a partir de
  `grupo_empresarial.companias_tercero` (que contiene **ids de grupo empresarial**, no de compañía).

Ese `when(es_tercero)` es el cambio **aún no commiteado** en la rama `mayala`; replica el patrón ya
usado por [OrdenesCompra.php:102](../app/Http/Livewire/Importaciones/OrdenesCompra.php#L102),
[DashboardArribos.php:23](../app/Http/Livewire/Importaciones/DashboardArribos.php#L23),
[ProgramacionArribos.php:112](../app/Http/Livewire/Importaciones/ProgramacionArribos.php#L112) y
[ConsultaContenedoresCliente.php:79](../app/Http/Livewire/Importaciones/ControlContenedores/ConsultaContenedoresCliente.php#L79).
El mismo bloque de 8 líneas está copiado en 5 componentes: **candidato natural a extraer** en la V2.

El filtro efectivo se aplica en `whereIn('registros_importacion.ID_IMPORTADOR', $this->compania)` en la
consulta base y en `whereIn('ID_IMPORTADOR', …)` en las de conteo. **Está presente en las 6 consultas**
— no hay fuga de tenant. Dos matices:

- Si `$this->compania` queda vacío, `whereIn` genera `0 = 1` → cero filas. Falla cerrado, correcto.
- Si `session('es_tercero')` es verdadero pero `session('companias')` es `null` (sesión reconstruida sin
  pasar por el login), `whereIn('id_websimex', null)` lanza excepción. Ver §8 · L4.

---

## 5. Lógica actual, expresión por expresión

Todo ocurre dentro de `render()`, que se ejecuta completo en **cada** request de Livewire.

### 5.1 Fecha real de vencimiento

```sql
DATE(COALESCE(
  NULLIF(NULLIF(FECHA_PRORROGA2, '0000-00-00'), '0000-00-00 00:00:00'),
  NULLIF(NULLIF(FECHA_PRORROGA,  '0000-00-00'), '0000-00-00 00:00:00'),
  NULLIF(NULLIF(FECHA_VENCIMIENTO,'0000-00-00'), '0000-00-00 00:00:00')
)) AS FECHA_REAL_VENCIMIENTO
```

**Regla de negocio:** la 2.ª prórroga manda sobre la 1.ª, y la 1.ª sobre el vencimiento original.
Correcta y central — cualquier V2 debe preservarla.

Los `NULLIF` protegen contra *zero-dates* de MySQL. Medido: **0 zero-dates** en las tres columnas
(`sql_mode = NO_ENGINE_SUBSTITUTION`, sin `NO_ZERO_DATE`, así que podrían aparecer). Son defensivos
pero hoy código muerto que se repite 6 veces por consulta.

### 5.2 El "saldo"

```sql
COALESCE((SELECT SUM(COALESCE(dr.CANTIDAD,0) - COALESCE(dr.CANTIDAD_DISPONIBLE,0))
          FROM detalles_ris dr
          WHERE dr.ID_REGISTRO = registros_importacion.ID_REGISTRO), 0) AS saldo_total
```

Es la suma, **a nivel de RIM completo**, de `cantidad − disponible`: la cantidad **consumida**. Ver D1.

La misma expresión, palabra por palabra, está duplicada en
[PurchaseOrderProductForm.php:282](../app/Http/Livewire/Importaciones/PurchaseOrderProductForm.php#L282)
(`consultarInformacionRim()`), incluida la clasificación por estado. Son dos copias del mismo
conocimiento de negocio en módulos distintos.

### 5.3 Clasificación (`status_alerta`)

Calculada en SQL, en este orden de precedencia:

| Orden | Condición | Estado |
|---|---|---|
| 1 | `fecha_real < CURDATE()` | `vencido` |
| 2 | `saldo_total = 0` | `sin_saldo` |
| 3 | `fecha_real <= CURDATE() + 7 días` | `critico` |
| 4 | `fecha_real <= CURDATE() + 30 días` | `proximo` |
| 5 | resto (incluye `fecha_real IS NULL`) | `vigente` |

El orden importa: `sin_saldo` **gana** a `critico` y a `proximo` (D3).

### 5.4 Las tres ramas del listado

`render()` arma tres consultas mutuamente excluyentes según `$filtroStatus`:

| Rama | `WHERE` adicional | Notas |
|---|---|---|
| `sin_saldo` | `saldo = 0` **y** (`fecha_real IS NULL` **o** `>= CURDATE()`) | Única rama que admite RIM sin fecha |
| `vencido` | `fecha_real` entre `hoy − 30 d` y `hoy` **y** `NOT EXISTS` sustituto (§5.5) | Ventana de 30 días |
| resto | `fecha_real >= hoy`, y si hay filtro `critico/proximo/vigente`: `saldo <> 0` + `CASE = filtro` | También es la rama "Todos" |

Las tres cierran con el mismo bloque de búsqueda (`LIKE %term%` sobre `NUMERO_DO`,
`NUMERO_APROBACION`, `NOMBRE_COMERCIAL`, `NOMBRE_IMPORTADOR`), `orderBy('FECHA_REAL_VENCIMIENTO')` y
`->get()`. El bloque está **copiado tres veces**, idéntico.

Detalle: en la rama por defecto el `CASE` del `WHERE` **omite** el brazo `sin_saldo`, mientras el
`CASE` del `SELECT` lo incluye. Por eso el listado sin filtro puede mostrar filas etiquetadas
"Sin saldo" que la tarjeta de ese estado no cuenta igual.

### 5.5 La regla no documentada: "RIM vencido sin sustituto"

En la rama `vencido` (y en su conteo) hay un `whereNotExists`
([SeguimientoRim.php:108-117](../app/Http/Livewire/Rim/SeguimientoRim.php#L108-L117)) que excluye el
RIM vencido si existe **otro** RIM que:

- tenga el **mismo `ID_PRODUCTO`**,
- pertenezca a los mismos importadores del tenant,
- esté aprobado,
- tenga `CANTIDAD_DISPONIBLE > 0`, y
- venza a **más de 30 días**.

Es decir: *"no me molestes con este vencido, ya hay un registro nuevo que cubre ese producto"*. Regla
de negocio valiosa y **completamente invisible en la interfaz** — el usuario no sabe por qué unos
vencidos aparecen y otros no. Documentarla (o exponerla) es trabajo de la V2.

### 5.6 Los cinco conteos

Se calculan aparte, con dos bases distintas (con y sin join a `detalles_ris`) y
`COUNT(DISTINCT COALESCE(NUMERO_APROBACION, ID_REGISTRO))` — el comentario del código dice
explícitamente *"evita duplicados por join con detalles_ris"*, así que la desalineación con la tabla
es deliberada, pero el usuario no lo sabe.

| Tarjeta | Universo | ¿Exige `saldo <> 0`? | Ventana |
|---|---|---|---|
| `vencido` | con join | no | `hoy − 30 d` … `hoy`, sin sustituto |
| `critico` | sin join | sí | `hoy` … `hoy + 7 d` |
| `proximo` | sin join | sí | `hoy + 7 d` … `hoy + 30 d` |
| `vigente` | sin join | sí | `> hoy + 30 d` |
| `sin_saldo` | con join | `= 0` | `fecha IS NULL` o `>= hoy` |

La tarjeta **"Todos"** no consulta nada: la vista suma en Blade `critico + proximo + vigente`
([seguimiento-rim.blade.php:170-172](../resources/views/livewire/rim/seguimiento-rim.blade.php#L170-L172)),
lo que deja fuera `sin_saldo` y `vencido`, y al hacer clic muestra un listado que **sí** los incluye.

---

## 6. Rendimiento medido

Render completo del componente para el grupo 41, sin filtro y sin búsqueda:

| # | Consulta | Tiempo |
|---|---|---|
| 1 | Listado principal (`select *` + 3 tablas + 2 subconsultas dependientes) | **1 459,8 ms** |
| 2 | Conteo `vencido` (con `NOT EXISTS` correlacionado) | 200,3 ms |
| 3 | Conteo `critico` | 165,4 ms |
| 4 | Conteo `proximo` | 197,4 ms |
| 5 | Conteo `vigente` | 165,7 ms |
| 6 | Conteo `sin_saldo` | 173,8 ms |
| | **Total SQL** | **2 362,5 ms** |
| | **Render total** | **2 416,1 ms** |

Volumen de datos por render, mismo escenario:

| Métrica | Valor |
|---|---|
| Filas devueltas al listado | **1 058** |
| RIM distintos en esas filas | **378** (factor de duplicación **2,8×**) |
| Columnas por fila (`63 + 38 + 116`) | **217** |
| Atributos hidratados en modelos Eloquent | **~229 600** |
| Consultas por render | 6 |

Y por filtro:

| Filtro | Filas | RIM distintos | Tiempo | Conteo de la tarjeta |
|---|---|---|---|---|
| (ninguno) | 1 058 | 378 | 2 107 ms | 367 ("Todos") |
| `critico` | 116 | 34 | 938 ms | 34 |
| `vencido` | 74 | 36 | 1 526 ms | 36 |
| `sin_saldo` | 17 | 10 | 998 ms | 10 |

Contexto que amplifica todo lo anterior:

- `websimex` es una base **remota** (`awsslave.asercol.com:3307`), no local: transferir 1 058 × 217
  columnas cuesta red, no solo CPU.
- `QUEUE_CONNECTION=sync` y el patrón iframe hacen que este costo sea íntegramente latencia percibida.
- Este render se repite **en cada clic de tarjeta y en cada tecla del buscador** (§7 · P3).

---

## 7. Hallazgos

Severidad: **A** = corregir en la V2 con confirmación de negocio · **B** = corregir en la V2 ·
**C** = limpieza oportunista.

### Corrección y semántica

**D1 · A — `saldo_total` mide lo consumido, no el saldo; "Sin saldo" señala los RIM intactos**

`SUM(CANTIDAD − CANTIDAD_DISPONIBLE)` es la cantidad **usada** (§3.3). Por tanto:

| Situación real del RIM | `saldo_total` | Estado que muestra el módulo | Fila en rojo |
|---|---|---|---|
| Intacto, 100 % disponible | `0` | **Sin saldo** | **sí** |
| Consumido a medias | > 0 | por fecha (vigente/próximo/crítico) | no |
| **Agotado**, 0 disponible | máximo | por fecha (**vigente**) | no |

La columna "Saldo" de la tabla muestra el consumo, y el rojo de la fila
([seguimiento-rim.blade.php:68](../resources/views/livewire/rim/seguimiento-rim.blade.php#L68)) se
enciende cuando el consumo es 0. Es exactamente al revés de lo que un usuario espera de la palabra
"saldo". Consecuencia operativa: **el módulo no puede avisar de un RIM agotado**, que es la otra mitad
de su razón de ser.

Propuesta para la V2 (requiere validar con negocio, porque cambia lo que la gente ya lee):

```sql
saldo_disponible = COALESCE(SUM(dr.CANTIDAD_DISPONIBLE), 0)      -- lo que queda
cantidad_consumida = COALESCE(SUM(dr.CANTIDAD - dr.CANTIDAD_DISPONIBLE), 0)
agotado = (saldo_disponible = 0)
```

y renombrar el estado `sin_saldo` → `agotado`, agregando (si el negocio lo quiere) un estado
`sin_usar` para los intactos.

**D2 · A — Tarjetas y tabla cuentan cosas distintas**

Las tarjetas cuentan RIM únicos (`COUNT(DISTINCT NUMERO_APROBACION)`); la tabla lista líneas de
producto. Para el grupo 41: tarjeta "Críticos" 34 vs 116 filas; "Todos" 367 vs 378 RIM en el listado.
El usuario hace clic en "34" y ve 116 renglones. Además el mismo RIM aparece 2,8 veces en promedio,
repitiendo en cada fila el `saldo_total` del RIM completo — lo que sugiere falsamente que ese saldo es
del producto de la fila.

Decisión para la V2: **elegir una granularidad**. Recomendado: agrupar por RIM (una fila por
`ID_REGISTRO`, con sus productos expandibles o concatenados) y que las tarjetas cuenten lo mismo que
la tabla, calculadas con la misma expresión.

**D3 · B — `sin_saldo` oculta la urgencia por fecha**

En el `CASE` del `SELECT`, `saldo = 0` se evalúa **antes** que las ventanas de 7 y 30 días. Un RIM que
vence en 3 días y aún no se ha usado sale etiquetado "Sin saldo", no "Crítico", y no se cuenta en la
tarjeta de críticos (que exige `saldo <> 0`). Los dos ejes —tiempo y cantidad— están mezclados en una
sola dimensión.

Propuesta: separarlos. Un eje de vencimiento (`vencido | critico | proximo | vigente`) y un eje de
consumo (`agotado | parcial | sin_usar`), independientes y combinables.

**D4 · B — La ventana de "vencidos" es de 30 días, con nombre de variable engañoso**

```php
$twoDaysAgo = Carbon::today()->subDays(30)->toDateString();   // línea 55
```

La variable se llama `twoDaysAgo` y vale 30 días. Un RIM vencido hace 31 días **desaparece** de la
tarjeta y del listado sin ninguna señal. Si es regla de negocio, hay que documentarla y hacerla
configurable; si es un remanente de pruebas, corregirla. Renombrar en cualquier caso.

**D5 · C — `COUNT(DISTINCT NUMERO_APROBACION)` colapsa registros distintos**

En el grupo 41 hay 1 424 RIM aprobados y solo 1 418 `NUMERO_APROBACION` distintos: **6 aprobaciones
tienen 2 registros** (probablemente versiones, columna `VERSION`). Los conteos los subestiman. Contar
por `ID_REGISTRO` es más fiel al dato; si el negocio quiere agrupar versiones, hay que decidirlo
explícitamente y no dejarlo como efecto colateral de un `COALESCE`.

**D6 · C — 41 RIM aprobados sin fecha de vencimiento son casi invisibles**

`FECHA_VENCIMIENTO IS NULL` en 41 de los 36 004 aprobados. Como `fecha_real` queda `NULL`, la rama por
defecto (`fecha_real >= hoy`) los descarta y solo aparecen si además están intactos (rama
`sin_saldo`). La V2 debería mostrarlos en un cubo explícito ("sin fecha") en vez de dejarlos caer.

**D7 · C — `NULLIF` contra zero-dates es código muerto**

0 zero-dates en las tres columnas. Se puede conservar como defensa, pero centralizado en un solo
lugar en vez de repetido 6 veces por consulta.

### Rendimiento

**P1 · B — Subconsulta dependiente evaluada dos veces por fila**

`$saldoExpr` aparece en el `CASE` y en la columna `saldo_total`; MySQL 5.7 la reejecuta por fila.
Reemplazar por un `JOIN` contra una agregación pre-calculada:

```sql
LEFT JOIN (SELECT ID_REGISTRO,
                  SUM(CANTIDAD_DISPONIBLE) AS saldo_disponible,
                  SUM(CANTIDAD - CANTIDAD_DISPONIBLE) AS consumido
           FROM detalles_ris GROUP BY ID_REGISTRO) s
       ON s.ID_REGISTRO = registros_importacion.ID_REGISTRO
```

**P2 · B — `select('*')` sobre 3 tablas: 217 columnas y colisiones de nombre**

La vista usa **7 campos**. Se traen 217 por fila. Además, con `select *` sobre tres tablas los nombres
repetidos se sobreescriben en el array de atributos (gana la última tabla del `SELECT`):

| Colisión | Tablas |
|---|---|
| `USUARIO_CREACION`, `FECHA_CREACION`, `USUARIO_MODIFICACION`, `FECHA_MODIFICACION` | las tres |
| `ID_REGISTRO`, `CODIGO_PAIS_ORIGEN` | `registros_importacion` ↔ `detalles_ris` |
| `ID_IMPORTADOR`, `CODIGO_ADMINISTRACION`, `CODIGO_OFICINA_INCOMEX` | `registros_importacion` ↔ `importadores` |

Hoy no rompe nada porque ninguno de esos campos se muestra, pero una V2 que use `CODIGO_PAIS_ORIGEN` o
`FECHA_CREACION` leerá silenciosamente la tabla equivocada. Enumerar columnas explícitamente y usar
`->toBase()` (filas planas, sin hidratar modelos) resuelve las dos cosas.

**P3 · B — Cada tecla del buscador dispara las 6 consultas**

```blade
<x-input-search name="search" id="searh" wire:model="search"></x-input-search>
```

`wire:model` sin `.defer` → Livewire 2 emite un request con ~150 ms de *debounce*, es decir un ciclo de
2,4 s por pulsación sostenida. **Todos los módulos gemelos** usan el patrón correcto —
`wire:model.defer="search"` + `wire:keydown.enter="consultarPedidos()"` — ver
[ordenes-compra.blade.php:203](../resources/views/livewire/importaciones/ordenes-compra.blade.php#L203),
[pedidos-curso.blade.php:101](../resources/views/livewire/exportaciones/pedidos-curso.blade.php#L101),
[programacion-arribos.blade.php:101](../resources/views/livewire/importaciones/programacion-arribos.blade.php#L101).
Adoptarlo aquí es el cambio de mayor relación beneficio/costo de todo el documento.

**P4 · B — `WithPagination` presente pero inutilizado**

El componente usa el trait y `updatedFiltroStatus()` llama `resetPage()`, pero **las tres ramas usan
`->get()`**, no `paginate()`, y `{{ $rims->links() }}` está comentado en la vista
([línea 137](../resources/views/livewire/rim/seguimiento-rim.blade.php#L137)). Resultado: 1 058 filas
en el DOM dentro de un contenedor de `max-h-[50vh]`. Paginar (o virtualizar) resuelve simultáneamente
P1, P2 y el peso del DOM.

**P5 · C — Búsqueda `LIKE '%term%'` sobre 4 columnas de 3 tablas**

Ninguna es indexable así. Con el volumen actual es aceptable, pero conviene: buscar solo al presionar
Enter (P3), y anclar el patrón (`term%`) al menos en `NUMERO_DO` y `NUMERO_APROBACION`, que son las
búsquedas reales de un usuario.

**P6 · C — Los 5 conteos podrían ser una sola consulta**

Cinco *round-trips* de ~180 ms cada uno a una base remota (900 ms del total) que se resuelven con una
sola agregación condicional (`SUM(CASE WHEN … THEN 1 ELSE 0 END)` por estado) sobre la agregación de
P1. Además dependen únicamente de `CURDATE()` y del tenant: son **cacheables por día** con
`Cache::remember(…, hoy)`.

**P7 · C — La vista de contenido carga librerías que no usa**

[rim-seguimiento.blade.php](../resources/views/rim/rim-seguimiento.blade.php) trae Highcharts
**highmaps** (~500 KB), jQuery 3.7.1 y Alpine 3.12 en un módulo **sin gráficas y sin jQuery**. Es una
copia de la vista de estadísticas. El `<title>` también quedó mal: *"Pedidos Importaciones"*.

### Limpieza

**L1 · C — Código sin uso en el componente**

- Imports nunca usados: `DetalleSolicituRegistrosImportacion`, `Pedido`, `ProductosWsimex`,
  `SolicitudRegistrosImportacionModel` ([líneas 6-10](../app/Http/Livewire/Rim/SeguimientoRim.php#L6-L10)).
- Propiedades públicas nunca usadas: `$role`, `$idUsuario`, `$rolesEditaRim = [6, 47]`
  ([líneas 19-22](../app/Http/Livewire/Rim/SeguimientoRim.php#L19-L22)). `$rolesEditaRim` sugiere una
  intención de edición que nunca se implementó.
- `$color` se lee de sesión y nunca se usa en la vista.
- `protected $listeners = ['clonarRim']` comentado ([línea 27](../app/Http/Livewire/Rim/SeguimientoRim.php#L27)).
- `$saldoExprRi2` se construye, se pasa al closure del `whereNotExists` (dos veces) y **nunca se usa**
  ([líneas 71, 108, 172](../app/Http/Livewire/Rim/SeguimientoRim.php#L71)).
- `$importadores = $this->compania` es un alias redundante.

**L2 · C — Triplicación**

El bloque `when(!empty($this->search), …)` está copiado 3 veces; el `CASE` de clasificación, 3 veces
(2 en PHP + 1 en `PurchaseOrderProductForm`); las expresiones de fecha, 2 veces (versión normal y
versión `ri2`); el bloque de resolución de tenant, en 5 componentes.

**L3 · C — Lógica de negocio en `render()`**

Las ~165 líneas de `render()` son 100 % consulta. Cualquier V2 debería mover esto a un lugar reutilizable
— un *scope* en [RegistrosImportacionWS](../app/Models/RegistrosImportacionWS.php) o una clase de
consulta— y así compartirlo con
[PurchaseOrderProductForm::consultarInformacionRim()](../app/Http/Livewire/Importaciones/PurchaseOrderProductForm.php#L276),
que hoy mantiene su propia copia de las mismas reglas. El repo tiene dos precedentes válidos:
`app/Http/Clases/` y `app/Services/`.

**L4 · C — `$compania` es propiedad pública**

La lista de importadores del tenant viaja al navegador en cada request. Livewire 2 firma el *payload*
con un checksum HMAC, así que **no es manipulable** desde el cliente; pero expone ids internos, engorda
cada request y depende de la hidratación en lugar de derivarse de la sesión. Volverla `protected` y
resolverla en un método (con `?: []` para el caso de `session('companias')` nula, §4) es más robusto.

**L5 · C — Detalles de la vista**

- `id="searh"` (typo) — y en realidad ni `name` ni `id` llegan al `<input>`: el componente
  [input-search](../resources/views/components/input-search.blade.php) hace `$attributes->merge()`
  sobre el `<div>` contenedor, no sobre el input. `wire:model` sí funciona porque el evento `input`
  burbujea, pero cualquier `label for=`, autocompletado o test por selector falla.
- La tarjeta "RIM Vencidos" está escrita a mano en la vista
  ([líneas 146-161](../resources/views/livewire/rim/seguimiento-rim.blade.php#L146-L161)) con la
  versión `<x-rim-card>` comentada justo encima, solo para tener la animación `animate-ping`. Eso
  debería ser un prop de `rim-card`.
- `class="rounded- grid …"` — clase Tailwind truncada
  ([línea 144](../resources/views/livewire/rim/seguimiento-rim.blade.php#L144)).
- `@push('scripts')` vacío.

---

## 8. Oportunidades funcionales para la V2

**U1 — No se ve qué filtro está activo.** Las cinco tarjetas se ven iguales antes y después del clic.
Falta estado seleccionado y un "quitar filtro" explícito (hoy el único camino es "Todos", que además
muestra un universo distinto al que anuncia, D2).

**U2 — 2,4 s sin ninguna señal.** Ni un `wire:loading`, ni `wire:target`, ni deshabilitar las tarjetas
mientras carga. El usuario hace clic dos veces.

**U3 — Es un tablero sin salidas.** No hay: exportación a Excel (el repo tiene ~60 exportadores en
`app/Exports/` como precedente), enlace al detalle del DO o al RIM, ordenamiento por columna, filtro
por compañía cuando el tenant tiene varias (el grupo 41 tiene 4 y no puede distinguirlas), ni filtro
por rango de fechas. Y sobre todo: **ninguna acción**. Detecta un RIM por vencer y no ofrece
"solicitar prórroga", aunque el flujo existe en CTI (`solicitud_rim_prorroga`,
[SolicitudProrrogaRimModel](../app/Models/SolicitudProrrogaRimModel.php)) y
[EstadisticasRim](../app/Http/Livewire/Rim/EstadisticasRim.php) ya lo lee. Conectar detección con
acción es la mejora funcional de mayor valor.

**U4 — Espacio útil muy reducido.** El iframe contenedor fija relación 16:9 (`padding-top: 56.25%`) y
dentro la tabla se limita a `max-h-[50vh]`: sobre 1 058 filas quedan visibles ~10. La columna de
tarjetas ocupa el 20 % del ancho.

**U5 — La convención de colores del encabezado está incompleta.** Explica Crítico / Próximo / Vigente
y omite Vencido y Sin saldo, que son justamente los dos estados confusos.

**U6 — Prórrogas y fechas poco legibles.** Las prórrogas se muestran como dos círculos "1" y "2" cuya
fecha solo aparece en el `title` (tooltip, invisible en móvil y al imprimir). La fecha real se formatea
en Blade con `strtotime`/`date` en vez de Carbon, y las fechas crudas de prórroga se pintan como
`datetime` completo.

**U7 — Sin alertas proactivas.** El módulo requiere que alguien entre a mirarlo. El repo ya tiene 12
comandos programados en [Kernel.php](../app/Console/Kernel.php) y el helper
[JMail](../app/JMetodos.php); un correo semanal de "RIM por vencer" es una extensión natural. Ojo con
`QUEUE_CONNECTION=sync`.

**U8 — Sin gemelo de exportaciones.** Por la regla §7 de [CLAUDE.md](../CLAUDE.md) conviene decidir
explícitamente que no aplica: el RIM es un instrumento de importación y no tiene equivalente `ex`.
Dejarlo escrito evita que alguien lo "complete" más adelante.

---

## 9. Invariantes que la V2 debe preservar

1. **Filtro de tenant en todas las consultas**: `ID_IMPORTADOR IN (importadores del grupo)`, con las
   dos rutas (cliente normal / tercero) de §4. Sin excepción y sin confiar en propiedades públicas.
2. **Solo RIM aprobados**: `FECHA_APROBACION IS NOT NULL`.
3. **Jerarquía de fecha real**: `FECHA_PRORROGA2` → `FECHA_PRORROGA` → `FECHA_VENCIMIENTO`.
4. **`websimex` es solo lectura.** Cualquier estado propio de la V2 (marcas, notas, acuses) va en
   `mysql` (`asercolg_po`) o `cti`, nunca en `comex`.
5. **`NUMERO_DO` es string.** `varchar(15)`; no castear a entero.
6. **Umbrales vigentes**: 7 días = crítico, 30 días = próximo. Si se hacen configurables, el default
   debe reproducir el comportamiento actual.
7. **Regla del sustituto** (§5.5): conservarla, pero documentada y visible.
8. **Compatibilidad de PHP.** [CLAUDE.md](../CLAUDE.md) fija PHP 7.3; el binario de desarrollo es
   **7.4.33** y hay código en el repo que ya usa *arrow functions* de 7.4
   ([PurchaseOrderProductForm.php:305](../app/Http/Livewire/Importaciones/PurchaseOrderProductForm.php#L305)).
   Mientras no se aclare (§11 · Q5), lo seguro es escribir código válido en 7.3.
9. **Patrón iframe + Livewire** y las dos rutas con `middleware('auth')`. Cualquier ruta nueva
   necesita `auth`: la autorización por perfil está comentada en
   [Authenticate.php](../app/Http/Middleware/Authenticate.php) y el menú solo **oculta**, no protege.
10. **El menú es dato, no código.** Si la V2 convive con la V1 hay que insertar en `menus` y agregar el
    id al JSON `compania.aplicaciones` de cada compañía piloto; sin eso el módulo no aparece.

---

## 10. Cómo reproducir las mediciones

Las cifras de §6 se obtuvieron instanciando el componente fuera de Livewire e inyectando el tenant, con
un *listener* de consultas. Guion usado (adaptar el arreglo de importadores):

```php
// php artisan tinker <archivo.php>   — solo lectura
$importadores = [2437, 2439, 2438, 2436];   // grupo empresarial 41

$queries = [];
DB::connection('websimex')->listen(function ($q) use (&$queries) {
    $queries[] = ['sql' => $q->sql, 'time' => $q->time];
});

$c = new \App\Http\Livewire\Rim\SeguimientoRim();
$c->compania = $importadores;   // sustituye a mount()
$c->filtroStatus = null;
$c->search = '';

$t0 = microtime(true);
$data = $c->render()->getData();
printf("filas=%d  consultas=%d  render=%.1f ms\n",
    count($data['rims']), count($queries), (microtime(true) - $t0) * 1000);
print_r($data['conteos']);
```

Para elegir un tenant representativo:

```sql
-- grupos con más compañías activas
SELECT id_grupo_empresarial, COUNT(*) c, GROUP_CONCAT(id_websimex) ids
FROM compania WHERE inactivo = 0 GROUP BY id_grupo_empresarial ORDER BY c DESC;

-- importadores con más RIM aprobados
SELECT ID_IMPORTADOR, COUNT(*) c FROM registros_importacion
WHERE FECHA_APROBACION IS NOT NULL GROUP BY ID_IMPORTADOR ORDER BY c DESC LIMIT 10;
```

Verificación funcional (sigue siendo manual, el repo no tiene tests):

```bash
php -l app/Http/Livewire/Rim/SeguimientoRim.php
php artisan route:list | grep rim
```

y luego abrir `/rim/seguimiento` en Herd con un usuario de **dos** grupos empresariales distintos,
comprobando que los `NOMBRE_IMPORTADOR` listados pertenecen únicamente al grupo conectado.

> ⚠️ El `.env` apunta a bases **reales compartidas**. Todo lo anterior es `SELECT`/`EXPLAIN`/`DESCRIBE`.
> No ejecutar escrituras contra `websimex` ni `websystem`.

---

## 11. Preguntas abiertas para negocio

| # | Pregunta | Bloquea |
|---|---|---|
| Q1 | ¿"Saldo" debe significar **cantidad disponible** (lo que queda) o **cantidad consumida**? ¿Y qué debe alertar: el RIM agotado, el intacto, o ambos con estados distintos? | D1, D3 — el corazón de la V2 |
| Q2 | ¿La tabla debe listar **un RIM por fila** (con productos agrupados) o **una línea de producto por fila**? De la respuesta depende cómo se cuentan las tarjetas. | D2 |
| Q3 | ¿La ventana de 30 días para "vencidos" es regla de negocio o remanente? ¿Debe ser configurable? | D4 |
| Q4 | ¿La regla del "RIM sustituto" (§5.5) se conserva tal cual? ¿Debe verse en la interfaz por qué un vencido se omitió? | D4, U5 |
| Q5 | ¿El objetivo de compatibilidad es PHP 7.3 (CLAUDE.md) o 7.4 (binario real y código existente)? | Invariante 9 |
| Q6 | ¿La V2 debe poder **actuar** (solicitar prórroga hacia CTI) o mantenerse como tablero de consulta? | U3 |
| Q7 | ¿Se requiere exportación a Excel y/o correo periódico de RIM por vencer? | U3, U7 |
| Q8 | ¿Los umbrales 7/30 días son iguales para todos los clientes o deben configurarse por grupo empresarial (como `columnas_a_mostrar` y los semáforos de [JMetodos](../app/JMetodos.php))? | Invariante 6 |
