# SPEC — Seguimiento RIM V2

**Estado:** Plan de trabajo para aprobación
**Autor:** Miguel Ayala · **Fecha:** 2026-09-09
**Módulo:** Operaciones → Seguimiento RIM
**Base de conocimiento:** [SeguimientoRIM.MD](SeguimientoRIM.MD)
**Diseño base:** Claude Design — proyecto *Control de Unidades de Carga*, `Canvas.dc.html`, artboard **2a** (*"Seguimiento RIM — modelo elegido"*)
**Componente actual:** [SeguimientoRim.php](../app/Http/Livewire/Rim/SeguimientoRim.php)

---

## 0. Resumen ejecutivo

La V2 reemplaza el tablero actual por el **triaje por vencimiento** del artboard 2a: la fila deja de
ser la línea de producto y pasa a ser el registro de importación —un saldo, una fecha, dos prórrogas—
con las líneas anidadas y el listado ordenado por urgencia en lugar de por número de DO.

Se entrega en **dos etapas desplegables por separado**:

| Etapa | Alcance |
|---|---|
| **1** | Núcleo de datos con la semántica corregida + listado agrupado por urgencia + tarjetas KPI + búsqueda + expansión de líneas |
| **2** | Panel de detalle del registro (solo lectura) + agrupar por cliente + exportar a Excel |

La V2 **convive** con la V1 en el primer despliegue (ruta y menú nuevos, cliente piloto); la V1 se
retira cuando el piloto valide (§11).

### Decisiones ya tomadas (2026-09-09)

| # | Decisión |
|---|---|
| **A** | **Saldo = cantidad disponible.** `saldo = SUM(CANTIDAD_DISPONIBLE)`. El estado *agotado* (saldo = 0) es la alerta; los RIM intactos dejan de marcarse como problema. Corrige el hallazgo D1 |
| **B** | La ventana de 30 días de *vencidos* se conserva como valor por defecto pero pasa a **configuración**, y la interfaz lo dice explícitamente |
| **C** | **PHP 7.3 estricto**: sin `fn()`, sin `?->`, sin `match`, sin propiedades tipadas, sin `str_contains` nativo |
| **D** | El dinero se muestra **solo en la fila**, en la moneda propia del RIM, **con el mismo énfasis en la cantidad disponible** (ambas magnitudes importan). Las tarjetas KPI muestran solo el conteo |
| **E** | **"Gestionar" abre un panel de detalle de solo lectura.** Ninguna escritura: `websimex` sigue siendo solo lectura y no se toca el flujo de CTI |
| **F** | Se implementan **dos agrupamientos**: Vencimiento y Cliente. El tercero del diseño, *Aprobación*, se descartó al validarlo (§8.3). Exportar queda **oculto** tras una bandera (§8.4) |
| **G** | *Agotado* es **estado de cierre**, salvo que **ninguna otra RIM vigente con saldo cubra sus códigos de producto** — en ese caso se convierte en **alerta**. La validación es por **código de producto**, no por subpartida |

### Lo que el análisis de datos obliga a apartarse del diseño

Tres cifras del mock no son implementables tal cual, y el plan lo resuelve así:

1. **El subtexto monetario de las tarjetas** (*"$ 1.842 M en saldo"*) se elimina. En el ERP
   `TOTAL_ITEM = CANTIDAD × VALOR_UNITARIO_DTO` en **138 318 de 138 339** filas, y las cantidades son
   cupos globales redondos (900 000 aparece 12 151 veces; el valor `1`, 5 038 veces). El agregado da
   *USD 435 250 023 985 autorizados* solo en el bucket de vencidos. → Decisión **D**.
2. **Los porcentajes del mock son ilustrativos.** Las 4 aprobaciones dibujadas son reales
   (`50046320`, `50047262`, `50046024`, `50237100`) pero el mock dice "72 % utilizado" donde el dato es
   99,4 %. El promedio real por bucket es **73–92 %**, con 238 de 1 045 vencidos por encima del 99 %.
   La barra se implementa igual, pero **no** se usa como señal de alerta (§7.4 · R2).
3. **La tarjeta "Sin saldo · 10" pasará a mostrar 116.** Es consecuencia directa de la decisión **A**.
   Bajo la semántica nueva la etiqueta *"Sin saldo"* por fin significa literalmente lo que dice.
   De esos 116, **78** quedan como alerta por falta de cobertura de producto (decisión **G**).

Todas las cifras están medidas sobre el grupo empresarial **41** (Nestlé: importadores 2436–2439), el
mismo tenant del que el diseñador tomó los ejemplos. Procedimiento en [SeguimientoRIM.MD §10](SeguimientoRIM.MD).

---

## 1. Lectura del diseño — artboard 2a

### 1.1 Anatomía

Tarjeta de 1300 px de ancho, seis bandas verticales:

| # | Banda | Contenido |
|---|---|---|
| 1 | **Encabezado** | Cintillo `SEGUIMIENTO RIM` + titular calculado *"70 registros exigen acción esta semana"* · a la derecha: buscador (`DO, aprobación, cliente o producto`, min 280 px) y botón `Filtros` |
| 2 | **Tira KPI** | `grid-template-columns: repeat(5,1fr)`, divisores verticales. Orden: Vencidos · Críticos ≤ 7 días · Próximos ≤ 30 días · Vigentes · Sin saldo. Cada celda: punto de color + etiqueta + número 26 px + subtexto |
| 3 | **Barra de herramientas** | Izquierda: *"365 registros · agrupados por urgencia de vencimiento"* · derecha: `Agrupar por: **Vencimiento** · Cliente · Aprobación` y `Exportar` |
| 4 | **Grupo Vencidos** | Cabecera *"Vencidos · acción inmediata — 36 registros"* sobre fondo rojo tenue; tarjetas de registro con borde rojo, **expandido por defecto** |
| 5 | **Grupo Críticos** | Cabecera *"Críticos · vencen en menos de 7 días — 34 registros"*; tarjetas neutras, **expandido por defecto**; incluye un registro con las líneas de producto desplegadas |
| 6 | **Pie colapsado** | `Próximos · 60 registros [Mostrar]` y `Vigentes · 271 registros [Mostrar]` |

El titular es **calculado**: 70 = 36 vencidos + 34 críticos. El *"365 registros"* del mock es
34 + 60 + 271; en la implementación ese contador será **el total de registros del resultado actual**
(§7.2 · nota).

### 1.2 Anatomía de la fila (registro)

`grid-template-columns: 250px 1fr 190px 170px 130px`, `gap: 18px`, `padding: 14px 16px`,
`border-radius: 11px`.

| Col | Ancho | Contenido en el diseño | Origen del dato |
|---|---|---|---|
| 0 | 26 px | **Chevron** que abre y cierra la fila, fuera de la grilla de columnas | §1.2.1 |
| 1 | 250 px | `50046320` (mono, 13,5 px, 600) sobre `DO 262112751 · NESTLE DE COLOMBIA S.A` | `NUMERO_APROBACION` / `NUMERO_DO` · `NOMBRE_IMPORTADOR` |
| 2 | 1fr | `Saldo disponible` + `72% utilizado` · barra 6 px · cifras + `4 líneas` (enlace) | §4.2 — **se reescribe** para dar el mismo peso a la cantidad (decisión **D**) |
| 3 | 190 px | Rótulo `VENCIMIENTO` + `2025-12-01 · hace 9 días` / `en 3 días` | `FECHA_REAL_VENCIMIENTO` + días relativos |
| 4 | 170 px | Rótulo `PRÓRROGAS` + dos cuadros 24×24 (`1`, `2`) + leyenda `agotadas` / `1 libre` / `2 libres` | `FECHA_PRORROGA`, `FECHA_PRORROGA2` |
| 5 | 130 px | Un solo botón alineado a la derecha: `Gestionar` | §8.2 |

#### 1.2.1 Separación por naturaleza (artboard **3b**)

Abrir la fila **no es una acción**: es cambiar cuánto se ve de ella. Por eso el disparador no compite
con `Gestionar` en la columna de acciones:

- **Chevron a la izquierda** de la fila, antes de la aprobación, que gira al abrirse.
- **El conteo de líneas es el otro disparador**, sobre el dato que el usuario ya está leyendo
  (`5.000 de 15.000.000 KG · 1 línea`), no un botón aparte.
- **A la derecha queda `Gestionar` y nada más**, sin competencia visual.
- La insignia `4 de 13 líneas sin reemplazo` también abre la fila (§7.3).

El chevron va **fuera** de la grilla de 5 columnas —la cabecera de la fila es un flex de
`chevron + grilla`— para no descuadrarla; las líneas anidadas se indentan los mismos 54 px
(16 de padding + 26 del chevron + 12 de separación) y así sus columnas siguen alineadas con las de la
fila.

La leyenda de prórrogas dice **`libres`** y no `disponibles` para no chocar con el *saldo disponible*,
que va en la misma fila y significa otra cosa.

**Líneas de producto anidadas** (misma grilla, `padding: 10px 16px`, separador superior
`1px solid #f2f0ed`): `Línea 1` · nombre comercial · valor mono · `Subpartida 2106.90.79` · vacío.

> **Desviación deliberada.** La quinta celda de la línea, vacía en el diseño, se deja vacía: la marca
> de *sin reemplazo* va junto al nombre del producto (columna ancha), para no sacrificar ninguna de las
> dos cifras —cantidad disponible y valor— que la decisión **D** manda mostrar con el mismo peso. Ver
> §7.3.

**Estado de las prórrogas:**

| Estado | Estilo |
|---|---|
| Usada | fondo `#fada99`, texto `#6c4302`, peso 600 |
| Disponible | borde `1px dashed #d1cdc8`, texto `#9d9792`, peso normal |

**Color de la barra de consumo:** varía por bucket — rojo `#c92f33` en vencidos, ámbar `#e58212` en
críticos.

### 1.3 Tokens de color

El diseño usa `oklch()`. Se define un bloque de *custom properties* con **hex equivalente** como
valor (no `oklch()` directo) para no depender del soporte del navegador (§13 · R5):

| Token | oklch del diseño | Hex |
|---|---|---|
| `--rim-acento` | `oklch(0.58 0.16 45)` | `#c4530f` |
| `--rim-acento-hover` | `oklch(0.5 0.16 45)` | `#a83a00` |
| `--rim-boton` | `oklch(0.62 0.17 45)` | `#d55c13` |
| `--rim-texto-fuerte` | `oklch(0.26 0.012 60)` | `#29231e` |
| `--rim-texto-medio` | `oklch(0.4 0.01 60)` | `#4c4742` |
| `--rim-texto-tenue` | `oklch(0.55 0.01 60)` | `#76706c` |
| `--rim-texto-placeholder` | `oklch(0.62 0.01 60)` | `#8b8580` |
| `--rim-texto-deshabilitado` | `oklch(0.68 0.01 60)` | `#9d9792` |
| `--rim-superficie` | `oklch(0.985 0.004 75)` | `#fcfaf7` |
| `--rim-superficie-barra` | `oklch(0.978 0.004 75)` | `#f9f7f5` |
| `--rim-superficie-roja` | `oklch(0.988 0.008 25)` | `#fff9f8` |
| `--rim-kpi-roja` | `oklch(0.985 0.008 25)` | `#fff8f7` |
| `--rim-borde-suave` | `oklch(0.955 0.004 75)` | `#f2f0ed` |
| `--rim-borde` | `oklch(0.94 0.005 75)` | `#edebe7` |
| `--rim-borde-card` | `oklch(0.93 0.006 75)` | `#eae7e3` |
| `--rim-borde-input` | `oklch(0.9 0.006 75)` | `#e0ddda` |
| `--rim-borde-boton` | `oklch(0.88 0.006 75)` | `#dad7d3` |
| `--rim-borde-dashed` | `oklch(0.85 0.008 75)` | `#d1cdc8` |
| `--rim-borde-card-roja` | `oklch(0.88 0.05 25)` | `#f7cbc7` |
| `--rim-vencido` | `oklch(0.55 0.19 25)` | `#c92f33` |
| `--rim-vencido-fuerte` | `oklch(0.48 0.18 25)` | `#ac1922` |
| `--rim-vencido-etiqueta` | `oklch(0.45 0.1 25)` | `#843c38` |
| `--rim-critico` | `oklch(0.62 0.2 30)` | `#e54533` |
| `--rim-critico-barra` | `oklch(0.7 0.16 60)` | `#e58212` |
| `--rim-proximo` | `oklch(0.8 0.14 85)` | `#e7b643` |
| `--rim-vigente` | `oklch(0.72 0.13 155)` | `#57bc80` |
| `--rim-agotado` | `oklch(0.55 0.01 60)` | `#76706c` |
| `--rim-prorroga-bg` | `oklch(0.9 0.09 85)` | `#fada99` |
| `--rim-prorroga-texto` | `oklch(0.42 0.09 70)` | `#6c4302` |

**Multi-marca:** los colores de **estado** (vencido/crítico/próximo/vigente/agotado) son semánticos y
**no** cambian por cliente. Solo `--rim-acento`, `--rim-acento-hover` y `--rim-boton` se enlazan al
color del tenant (`session('color')`, que trae nombres de paleta Tailwind: `red`, `green`, `sky`,
`cyan`, `blue`), con el terracota del diseño como valor por defecto. Requiere una tabla de
equivalencias de 5 entradas en `config/rim.php`.

### 1.4 Lo que el diseño **no** trae

El propio canvas lo anota al pie: *«agrega el panel de detalle del registro» · «diseña el flujo de
solicitud de prórroga» · «estados de carga, vacío y error»*. Por tanto:

- El **panel de detalle** (decisión **E**) se diseña en implementación siguiendo los tokens de §1.3.
- El **flujo de prórroga** queda **fuera de alcance** (§14).
- Los **estados de carga / vacío / error** se especifican en §7.5 como propuesta propia.
- El botón `Filtros` del encabezado aparece dibujado pero sin panel asociado → §7.2.

---

## 2. Decisiones de arquitectura

| # | Decisión | Motivo |
|---|---|---|
| **A1** | La fila se agrupa por **`ID_REGISTRO`**, no por `NUMERO_APROBACION` | El diseño dice *"el registro (aprobación): un saldo, una fecha, dos prórrogas"*. Hay 6 aprobaciones con 2 registros en el grupo 41, y cada registro tiene su propia fecha: fusionarlos rompería la premisa. `NUMERO_APROBACION` es el **título** de la fila, `ID_REGISTRO` la identidad. Corrige el hallazgo D5 |
| **A2** | **Una consulta agregada** a nivel de RIM sustituye a las 6 actuales | Medido: **651 ms** contra 2 362 ms, devolviendo buckets, cantidades, montos y % utilizado de los 1 424 RIM del grupo 41 (§5) |
| **A3** | Los **conteos KPI se derivan del mismo resultado**, no de consultas aparte | Elimina de raíz el hallazgo D2: tarjeta y listado no pueden discrepar si salen de la misma fila |
| **A4** | Columnas **enumeradas explícitamente**; nunca `select('*')` | Hoy se traen 217 columnas para mostrar 7, con 11 nombres en colisión entre las tres tablas (hallazgo P2) |
| **A5** | La lógica de consulta vive en **`app/Http/Clases/Rim/`**, no en `render()` | Precedente del repo (`UnirMundos`, `ValidarCutoff`). Permite reutilizarla desde el exportador y desde [PurchaseOrderProductForm](../app/Http/Livewire/Importaciones/PurchaseOrderProductForm.php#L276), que hoy mantiene su propia copia de las mismas reglas |
| **A6** | Umbrales y reglas en **`config/rim.php`** | Decisión **B**. Y deja la puerta abierta a configurarlos por grupo empresarial más adelante |
| **A7** | **Divulgación progresiva** en lugar de paginación clásica: vencidos y críticos expandidos, próximos y vigentes tras `Mostrar`; dentro de un grupo, bloques de 50 con `Mostrar más` | Es la respuesta del propio diseño al problema de las 1 058 filas (hallazgo P4). Para el grupo 41 el DOM inicial pasa de 1 058 filas a **70** |
| **A8** | El **conjunto de cobertura** (códigos de producto con RIM vigente y saldo) se calcula una vez y se **cachea por tenant y día** | Solo depende de `CURDATE()` y del tenant. Medido: 511 códigos en 763 ms para el grupo 41 — inaceptable por render, trivial una vez al día |
| **A9** | El buscador **no** usa el componente compartido `x-input-search` | Ese componente vuelca los atributos en el `<div>` contenedor, no en el `<input>` (hallazgo L5), y lo comparten ~15 vistas: tocarlo es riesgo colateral. El diseño trae su propio buscador con estilo propio |
| **A10** | `$compania` pasa a `protected`, resuelto desde sesión | Hallazgo L4 |
| **A11** | El módulo es un **panel de altura fija**: encabezado, tira KPI y barra de herramientas quedan quietos y solo el listado desplaza; la cabecera de cada grupo se pega al recorrerlo | `padding-top: 56.25%` + `max-h-[50vh]` dejaban visibles ~10 de 1 058 filas (hallazgo U4), y con los grupos desplegados las estadísticas se iban de pantalla. El iframe usa altura de viewport con los offsets ya calibrados para este shell en [arribos-iframe.blade.php](../resources/views/importaciones/arribos-iframe.blade.php). Por debajo de 720 px se vuelve al desplazamiento normal del documento, donde un panel de altura fija dejaría muy poco espacio al listado |

**Restricción técnica a tener presente:** `websimex`, donde vive todo el dato del módulo, es MySQL
**5.7.42** — sin CTE (`WITH`) y sin funciones de ventana. Todo se resuelve con tablas derivadas y
`GROUP BY`. Su `sql_mode` es `NO_ENGINE_SUBSTITUTION` (sin `ONLY_FULL_GROUP_BY`), pero el `GROUP BY` se
escribe completo para no depender de eso. La base propia del CTC (`asercolg_po`) es otra cosa:
**MariaDB 10.6.28**.

---

## 3. Semántica nueva — fórmulas exactas

Por registro (`ID_REGISTRO`), agregando sus líneas de `detalles_ris`:

```
lineas             = COUNT(*)
cantidad_total     = SUM(CANTIDAD)
saldo_cantidad     = SUM(CANTIDAD_DISPONIBLE)            -- el "saldo" (decisión A)
consumido_cantidad = cantidad_total - saldo_cantidad
pct_utilizado      = cantidad_total > 0
                     ? 100 * consumido_cantidad / cantidad_total
                     : null
valor_total        = SUM(TOTAL_ITEM)                     -- moneda del RIM (CODIGO_MONEDA)
saldo_valor        = SUM(CANTIDAD_DISPONIBLE * VALOR_UNITARIO_DTO)
```

`fecha_real` no cambia respecto de la V1 — es la única regla del módulo actual que estaba bien:

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

El doble `NULLIF` de la V1 se reduce a uno: medido, **no hay una sola zero-date** en las tres columnas
(hallazgo D7), y el `datetime` compara bien contra `'0000-00-00'`.

**Unidad de medida:** `saldo_cantidad` se muestra siempre junto a su unidad comercial
(`CODIGO_UNI_CCIAL_INCOMEX`). Hay **68 unidades distintas** (U, KG, PO, TNE, L, CS…), así que **jamás
se suman cantidades entre registros** — ni en las tarjetas ni en los encabezados de grupo. Si un RIM
mezcla unidades entre sus líneas, la fila muestra `varias` y el desglose queda en el panel de detalle.

**Moneda:** `valor_total` y `saldo_valor` se muestran con `CODIGO_MONEDA` (91 % USD, 7 % EUR, 1 % COP).
Tampoco se suman entre registros.

---

## 4. Clasificación en buckets

Dos ejes independientes, en lugar del `CASE` de un solo eje de la V1 (corrige el hallazgo D3):

**Eje tiempo** (`bucket_tiempo`), sobre `fecha_real`:

| Bucket | Condición |
|---|---|
| `vencido` | `fecha_real < CURDATE()` y `fecha_real >= CURDATE() - ventana` (ventana = 30 días, configurable — decisión **B**) |
| `critico` | `CURDATE() <= fecha_real <= CURDATE() + 7 días` |
| `proximo` | `CURDATE() + 7 días < fecha_real <= CURDATE() + 30 días` |
| `vigente` | `fecha_real > CURDATE() + 30 días` |
| `sin_fecha` | `fecha_real IS NULL` — 41 registros en toda la base (corrige el hallazgo D6) |

**Eje consumo** (`bucket_saldo`):

| Bucket | Condición |
|---|---|
| `agotado` | `saldo_cantidad = 0` |
| `parcial` | `0 < saldo_cantidad < cantidad_total` |
| `sin_usar` | `saldo_cantidad = cantidad_total` |

**Cómo se combinan en la interfaz:** el listado se agrupa por `bucket_tiempo`; los `agotado` se
extraen de esos grupos y van a la 5.ª tarjeta (`Sin saldo`), salvo los que la regla de cobertura marque
como alerta (§5.2), que suben al grupo `Vencidos · acción inmediata`. Un RIM `sin_usar` que vence en
3 días aparece en `Críticos`, que es exactamente lo que la V1 no podía hacer.

---

## 5. Reglas de cobertura

**Una sola regla, con dos umbrales.** La cobertura se evalúa siempre por **código de producto**
(`productos.CODIGO_PRODUCTO`, alcanzado desde `detalles_ris.ID_PRODUCTO`), nunca por subpartida.

> **Definición de cobertura.** Un código de producto está *cubierto* si existe otro RIM del mismo
> tenant, **aprobado**, con `CANTIDAD_DISPONIBLE > 0` en ese mismo código, cuya `fecha_real` supere el
> umbral que aplique.

| Regla | Umbral del sustituto | Efecto |
|---|---|---|
| **5.1 · Vencidos** (existente, se conserva) | `fecha_real > CURDATE() + 30 días` | El RIM vencido **desaparece** del bucket si todas sus líneas están cubiertas |
| **5.2 · Agotados** (nueva — decisión **G**) | `fecha_real >= CURDATE()` (basta que esté vigente) | El RIM agotado **sube a alerta** si alguna de sus líneas no está cubierta |

### 5.1 Vencidos

La V1 aplicaba la regla **por línea**, porque su consulta era a nivel de detalle. Al pasar a nivel de
RIM se traduce así: **el RIM permanece en vencidos si al menos una línea no tiene reemplazo**, y la
fila muestra la insignia `N de M líneas sin reemplazo`. Es la traducción fiel y además hace visible una
regla que hoy es completamente invisible (§5.5 de la base de conocimiento).

Se cambia el criterio de `ID_PRODUCTO` a `CODIGO_PRODUCTO`: **medido, el resultado es idéntico** (36
registros en el grupo 41 con ambos), pero el código del cliente tolera que el ERP tenga el mismo
producto en dos registros distintos —hay 20 casos así en el grupo 41— donde el `ID_PRODUCTO` daría una
alerta falsa.

### 5.2 Agotados

Un RIM agotado (`saldo_cantidad = 0`) es **estado de cierre**, salvo que alguno de sus códigos de
producto **no** esté cubierto. En ese caso pasa a **alerta**: no hay con qué seguir importando ese
producto.

Medido en el grupo 41:

| | |
|---|---|
| RIM agotados | **116** |
| Códigos de producto con cobertura vigente | **511** |
| Agotados **sin** cobertura → alerta | **78** |
| (contraste) Si el criterio fuera subpartida | 22 |

**Por qué producto y no subpartida:** la subpartida agrupa productos legalmente distintos, así que
"hay otro RIM en la misma subpartida" no garantiza que se pueda importar el producto concreto. La
medición lo confirma en el otro extremo: si la regla de **vencidos** se cambiara a subpartida, el
bucket caería de **36 a 4** registros, vaciando la alerta principal del módulo.

El criterio y ambos umbrales quedan en `config/rim.php`.

---

## 6. Consulta objetivo

Una sola pasada, agregando primero con el filtro de tenant dentro y uniendo después para las columnas
de presentación:

```sql
SELECT  ri.ID_REGISTRO, ri.NUMERO_APROBACION, ri.NUMERO_DO, ri.ID_IMPORTADOR,
        imp.NOMBRE_IMPORTADOR, ri.CODIGO_MONEDA,
        ri.FECHA_VENCIMIENTO, ri.FECHA_PRORROGA, ri.FECHA_PRORROGA2,
        ri.NUMERO_PRORROGA1, ri.NUMERO_PRORROGA2,
        COUNT(dr.ID_DETALLE_RI)                            AS lineas,
        SUM(dr.CANTIDAD)                                   AS cantidad_total,
        SUM(dr.CANTIDAD_DISPONIBLE)                        AS saldo_cantidad,
        SUM(dr.TOTAL_ITEM)                                 AS valor_total,
        SUM(dr.CANTIDAD_DISPONIBLE * dr.VALOR_UNITARIO_DTO) AS saldo_valor,
        COUNT(DISTINCT dr.CODIGO_UNI_CCIAL_INCOMEX)        AS unidades_distintas,
        MIN(dr.CODIGO_UNI_CCIAL_INCOMEX)                   AS unidad,
        <fecha_real>                                       AS fecha_real_vencimiento
FROM    registros_importacion ri
JOIN    importadores  imp ON imp.ID_IMPORTADOR = ri.ID_IMPORTADOR
JOIN    detalles_ris  dr  ON dr.ID_REGISTRO    = ri.ID_REGISTRO
WHERE   ri.ID_IMPORTADOR IN (:importadores)      -- filtro de tenant, obligatorio
  AND   ri.FECHA_APROBACION IS NOT NULL
GROUP BY ri.ID_REGISTRO, ri.NUMERO_APROBACION, ri.NUMERO_DO, ri.ID_IMPORTADOR,
         imp.NOMBRE_IMPORTADOR, ri.CODIGO_MONEDA,
         ri.FECHA_VENCIMIENTO, ri.FECHA_PRORROGA, ri.FECHA_PRORROGA2,
         ri.NUMERO_PRORROGA1, ri.NUMERO_PRORROGA2
```

- El **bucket** se calcula en PHP a partir de `fecha_real_vencimiento` y `saldo_cantidad`, no en SQL:
  así la expresión existe **una sola vez** (la V1 la repite 3 veces en PHP y una cuarta en
  `PurchaseOrderProductForm`) y es testeable sin base de datos.
- La **búsqueda** se aplica como `WHERE` adicional sobre `NUMERO_DO`, `NUMERO_APROBACION`,
  `NOMBRE_IMPORTADOR` y `EXISTS(detalles_ris.NOMBRE_COMERCIAL LIKE …)`.
- Las **líneas de producto** se consultan aparte, solo para los registros expandidos
  (`WHERE ID_REGISTRO IN (…)`), no de entrada.
- El **conjunto de cobertura** se calcula en dos variantes (sustituto *vigente* y sustituto *a más de
  30 días*, §5), con un `JOIN` a `productos` para llegar a `CODIGO_PRODUCTO`. Son dos consultas cortas,
  cacheadas por tenant y día (§2 · A8), y la comparación contra las líneas de cada RIM se hace en PHP.

**Presupuesto de rendimiento** (grupo 41, el más grande):

| Métrica | V1 medido | Objetivo V2 |
|---|---|---|
| Consultas por render inicial | 6 | **≤ 3** |
| Tiempo de render inicial | 2 416 ms | **≤ 700 ms** |
| Cambio de filtro / agrupación | 2 416 ms | **≤ 250 ms** (cobertura y conteos en caché) |
| Filas en el DOM al abrir | 1 058 | **70** |
| Columnas por fila | 217 | **≤ 20** |
| Tecleo en el buscador | 1 render por tecla | **0** — `wire:model.defer` + Enter |

---

## 7. Etapa 1 — Núcleo de datos y listado

### 7.1 Entregables

| Archivo | Acción | Contenido |
|---|---|---|
| `config/rim.php` | **nuevo** | Umbrales (7/30 días), ventana de vencidos (30), criterios de cobertura, mapa `color_tw` → acento |
| `app/Http/Clases/Rim/ConsultaSeguimientoRim.php` | **nuevo** | Consulta agregada, búsqueda, líneas on-demand, conjuntos de cobertura cacheados |
| `app/Http/Clases/Rim/ClasificadorRim.php` | **nuevo** | Buckets de tiempo y consumo, reglas de cobertura, días relativos. PHP puro, sin BD |
| `app/Http/Livewire/Rim/SeguimientoRimV2.php` | **nuevo** | Componente: estado de filtros, agrupación, expansión, búsqueda |
| `resources/views/livewire/rim/seguimiento-rim-v2.blade.php` | **nuevo** | Vista principal |
| `resources/views/livewire/rim/partials/_grupo.blade.php` | **nuevo** | Cabecera de grupo + cuerpo |
| `resources/views/livewire/rim/partials/_fila-rim.blade.php` | **nuevo** | La fila de 5 columnas del diseño |
| `resources/views/livewire/rim/partials/_lineas-rim.blade.php` | **nuevo** | Líneas de producto anidadas |
| `resources/views/components/rim/kpi.blade.php` | **nuevo** | Celda de la tira KPI |
| `resources/views/components/rim/prorrogas.blade.php` | **nuevo** | Los dos cuadros `1`/`2` + leyenda |
| `resources/views/components/rim/consumo.blade.php` | **nuevo** | Barra + cifras de saldo (cantidad y valor) |
| `resources/views/rim/rim-seguimiento-v2.blade.php` | **nuevo** | Vista de contenido (Tailwind) — **sin** Highcharts ni jQuery (hallazgo P7) |
| `resources/views/rim/rim-seguimiento-v2-iframe.blade.php` | **nuevo** | Contenedor con altura de viewport (A11) |
| `routes/web.php` | **editar** | Dos rutas nuevas con `middleware('auth')` |

Nada de la V1 se borra en esta etapa.

### 7.2 Mapeo diseño → datos

| Elemento del diseño | Implementación |
|---|---|
| Titular *"70 registros exigen acción esta semana"* | `vencidos + criticos + agotados_sin_cobertura`. Si es 0: *"Ningún registro exige acción esta semana"*. **Ojo:** el mock dice 70 (= 36 + 34); al sumar los 78 agotados sin cobertura el grupo 41 daría **148**, y el titular queda dominado por los agotados. Alternativa a decidir en el piloto: dejar el titular en `vencidos + criticos` y mostrar los agotados-alerta como segunda línea (*"78 sin reemplazo"*) |
| Tarjeta `Vencidos` | Conteo del bucket, con la ventana declarada en el subtexto: *"últimos 30 días"* (decisión **B**) |
| Tarjeta `Críticos ≤ 7 días` · `Próximos ≤ 30 días` · `Vigentes` | Conteo del bucket, solo registros con saldo > 0 |
| Tarjeta `Sin saldo` | Conteo de `agotado`; subtexto *"Listos para cierre"*, y si hay alertas: *"N sin reemplazo"* |
| Subtexto monetario de las tarjetas | **Se elimina** (decisión **D**) |
| Contador *"365 registros"* | Total de registros del resultado actual (todos los buckets visibles), no la suma parcial del mock |
| `Agrupar por: Vencimiento · Cliente · Aprobación` | Etapa 1 implementa **Vencimiento**; `Cliente` llega en Etapa 2. `Aprobación` **no se implementa** (§8.3) |
| `Exportar` | Etapa 2 — en Etapa 1 se dibuja deshabilitado |
| Botón `Filtros` | Etapa 1: abre un panel con *compañía* (cuando el tenant tiene más de una) y *estado de consumo* (agotado / parcial / sin usar). El diseño no lo detalla |
| Buscador | `wire:model.defer` + `wire:keydown.enter`, patrón del resto del repo (hallazgo P3) |
| Columna 2 de la fila | **Reescrita** respecto del mock: `Saldo: 8.571 U de 15.000.000` en primera línea y `USD 460.777 de USD 806.400.000` en segunda, con la barra de % utilizado entre ambas (decisión **D**) |
| Columna 3 | `2025-12-01 · hace 9 días` / `en 3 días` / `hoy` / `sin fecha` |
| Columna 4 | Cuadros según §1.2; leyenda `2 libres` / `1 libre` / `agotadas` |
| Columna 5 | Un solo botón: `Gestionar`, que abre el panel de detalle (§8.2). Abrir la fila se hace con el chevron o con el conteo de líneas (§1.2.1) |

### 7.3 Comportamiento

- Al abrir: grupos `Vencidos` y `Críticos` expandidos; `Próximos`, `Vigentes`, `Sin saldo` y
  `Sin fecha` colapsados con su conteo y un `Mostrar`.
- Dentro de un grupo, 50 registros por bloque con `Mostrar más`.
- Abrir la fila (chevron, conteo de líneas o insignia — §1.2.1) carga sus líneas con una consulta
  acotada a los registros desplegados.
- **Las líneas sin reemplazo quedan señaladas una por una.** La insignia de la fila dice *"4 de 13
  líneas sin reemplazo"*, pero eso no basta para saber **cuáles** son esas 4 entre las 13. Al
  desplegar, cada línea sin cobertura lleva fondo tenue, borde rojo a la izquierda, punto rojo antes
  del número de línea y una etiqueta `SIN REEMPLAZO` junto al nombre del producto. La insignia de la
  fila **es un botón** que despliega las líneas, para llegar a la marca en un clic.
  La regla que se evalúa depende del estado del RIM: en un vencido, falta de sustituto que venza a más
  de 30 días; en un agotado, falta de cualquier sustituto vigente (§5).
  La marca se muestra solo en RIM que están en alerta; en el resto la columna de cobertura no aplica.
- Orden dentro de cada grupo: `fecha_real` ascendente; los `sin_fecha`, al final.
- Las tarjetas KPI son filtro, con **estado activo visible** (corrige el hallazgo U1) y un
  `Quitar filtro` explícito.
- Todas las interacciones llevan `wire:loading` sobre su propio objetivo (corrige el hallazgo U2).

### 7.4 Estados no diseñados (propuesta propia)

| Estado | Propuesta |
|---|---|
| **Carga** | Esqueleto de 5 tarjetas KPI + 3 filas grises. `wire:loading.delay` para no parpadear |
| **Vacío — sin RIM** | *"Este cliente no tiene registros de importación aprobados en Websimex."* |
| **Vacío — filtro sin resultados** | *"Ningún registro coincide con la búsqueda."* + botón `Quitar filtro` |
| **Error de conexión al ERP** | *"No se pudo consultar Websimex. Reintentar."* — `websimex` es remota (`awsslave.asercol.com:3307`) y el fallo es plausible; hoy la V1 revienta con excepción |
| **Tenant sin compañías** | *"No hay compañías activas asociadas a este usuario."* — cubre el caso `session('companias')` nulo (hallazgo §4 de la base de conocimiento) |

### 7.5 Criterios de aceptación — Etapa 1

| # | Criterio | Resultado medido (grupo 41, 2026-09-10) |
|---|---|---|
| 1 | El conteo de cada tarjeta **coincide exactamente** con el número de filas de su grupo | **Los 6 grupos coinciden**: vencido 39, crítico 36, próximo 49, vigente 216, sin saldo 72, sin fecha 0. Total 412 registros |
| 2 | `Sin saldo` muestra los agotados y marca los que no tienen reemplazo | **72** agotados, **47** con insignia de alerta |
| 3 | Render inicial ≤ 700 ms y ≤ 3 consultas | **3 consultas** (registros + 2 de cobertura), **368–670 ms** de SQL con la conexión caliente; 4 consultas cuando se despliegan líneas. La primera consulta del día sube a ~2,1 s por establecimiento de conexión y llenado de buffers |
| 4 | Teclear en el buscador no dispara consultas hasta Enter | `wire:model.defer` + `wire:keydown.enter` + `submit.prevent` |
| 5 | Un RIM intacto que vence pronto aparece en `Críticos`, no en `Sin saldo` | Correcto: los dos ejes son independientes |
| 6 | Un RIM con `CANTIDAD_DISPONIBLE = 0` en todas sus líneas aparece en `Sin saldo` | Verificado con la aprobación **50089688** (cantidad 15 000 000, disponible 0, vence a más de 30 días): la V1 lo mostraba como **Vigente**, la V2 lo clasifica **agotado** |
| 7 | `php -l` limpio y compatible con PHP 7.3 | Sin `fn()`, `??=`, `?->`, `match` ni propiedades tipadas |
| 8 | Aislamiento entre clientes | 4 tenants probados (41, 22, 1, 2): **ningún importador ajeno** al tenant y ningún cliente compartido entre tenants |
| 9 | Aislamiento para terceros | Grupo 44 (`transborder`, `es_tercero = 1`): 4 compañías, 412 registros. Un tercero **sin** lista en sesión falla cerrado (`sin_companias`, 0 registros) |
| 10 | Ninguna consulta escribe en `websimex` | Solo `SELECT` |
| 11 | La página no carga librerías que no usa | 177 KB de HTML, sin Highcharts, jQuery, select2 ni Tailwind (la V1 cargaba highmaps ~500 KB) |
| 12 | Las líneas marcadas coinciden con el conteo de la insignia | Verificado en 6 RIM en alerta: en todos, líneas traídas = `lineas` y líneas marcadas = `sin_cobertura`. Caso de referencia: aprobación **50105664**, *"4 de 13"* → 13 líneas pintadas, **4** marcadas (líneas 4, 8, 9 y 10), 4 etiquetas `SIN REEMPLAZO`. Costo: **158 ms** para 81 líneas de 6 RIM, contra 121 ms sin las banderas |

**Corrección respecto de las cifras estimadas en §0 y §5.2.** Aquellas se midieron sobre *todos* los
RIM aprobados del tenant; el módulo aplica antes la **ventana de vencidos** (decisión **B**), que
descarta lo vencido hace más de 30 días. Con la ventana aplicada:

| Cifra | Estimada (§0/§5.2) | Real en el módulo |
|---|---|---|
| Agotados (`Sin saldo`) | 116 | **72** |
| Agotados sin reemplazo (alerta) | 78 | **47** |
| Vencidos | 36 | **39** |

Los agotados que desaparecen son RIM vencidos hace mucho y ya consumidos: exactamente lo que la
ventana debe filtrar. El punto 2 de §12 se mantiene en pie —la tarjeta pasa de 10 a 72, no a 116— y el
titular queda en **75** (39 + 36), cerca de los 70 del diseño.

---

## 8. Etapa 2 — Panel, agrupaciones y exportación

### 8.1 Entregables

| Archivo | Acción | Contenido |
|---|---|---|
| [_panel-detalle.blade.php](../resources/views/livewire/rim/partials/_panel-detalle.blade.php) | ✅ nuevo | Panel lateral del registro |
| [app/Exports/Rim/](../app/Exports/Rim/) | ✅ nuevo | `SeguimientoRimExport` + `HojaRegistros` + `HojaLineas` |
| [SeguimientoRimV2.php](../app/Http/Livewire/Rim/SeguimientoRimV2.php) | ✅ editado | `verDetalle()`, `cerrarDetalle()`, `agruparPor()`, `exportar()` |
| [ConsultaSeguimientoRim.php](../app/Http/Clases/Rim/ConsultaSeguimientoRim.php) | ✅ editado | `detalle()`, `porProducto()`, `importadoresDeSesion()` |
| [_grupo.blade.php](../resources/views/livewire/rim/partials/_grupo.blade.php) | ✅ editado | Un solo parcial sirve los tres agrupamientos |
| [PurchaseOrderProductForm.php](../app/Http/Livewire/Importaciones/PurchaseOrderProductForm.php) | ✅ editado | Su copia de las reglas sustituida por las clases compartidas (hallazgo L3) |

### 8.2 Panel de detalle (decisión **E** — solo lectura)

`Gestionar` es el único botón de la fila (§1.2.1) y está en todas, no solo en los vencidos como en el
artboard 2a: el detalle no debería depender del bucket. Abre un panel lateral con:

- Cabecera: `NUMERO_APROBACION`, `NUMERO_DO`, cliente, `FECHA_APROBACION`, `NUMERO_RADICADO`.
- Datos del registro: tipo de solicitud, aduana (`CODIGO_ADMINISTRACION`), exportador, país de origen
  y de compra, medio de transporte, moneda, `TASA_CAMBIO`.
- Cronología de vencimiento: original, prórroga 1 (`NUMERO_PRORROGA1`), prórroga 2
  (`NUMERO_PRORROGA2`), y cuál manda.
- Tabla de líneas: nombre comercial, código de producto, subpartida, cantidad disponible y autorizada,
  unidad, valor — **y para cada una, si tiene reemplazo vigente** (§5.1), que es la información que
  hoy el módulo esconde.

**Sin el estado del DO.** El panel llegó a traer una sección *Estado del DO en el ERP* (etapa,
apertura, cierre, pedido, vía `estados_do` y `etapas_do`) y **se retiró**: es información de otro
dominio, no ayuda a decidir sobre el registro, y en los datos reales se contradice —hay DO con la
bandera `CERRADO` en `N` cuya etapa es *CERRADO* y con fecha de cierre puesta—, así que mostrarla
generaba más dudas que respuestas. Al quitarla desaparecen dos `LEFT JOIN`, uno de ellos contra una
tabla de 100 columnas.

**Sin botones de escritura.** Si el negocio aprueba después el flujo de prórroga, el panel es el lugar
donde entra (§14).

### 8.3 Agrupaciones (decisión **F**)

| Modo | Agrupa por | Cabecera de grupo |
|---|---|---|
| `Vencimiento` (default) | `bucket_tiempo` | *"Vencidos · acción inmediata — 39 registros · últimos 30 días"* |
| `Cliente` | `ID_IMPORTADOR` | `NOMBRE_IMPORTADOR` + conteo + desglose por bucket en puntos de color, ordenado por número de alertas |

En `Cliente` el orden interno sigue siendo por urgencia. La agrupación se hace **en PHP** sobre el
resultado ya cargado: cambiar de modo no vuelve a consultar (presupuesto de 250 ms).

**El modo `Aprobación` del diseño se descartó al validarlo.** Se implementó y se retiró: no aportaba
una forma útil de leer la información y sí confusión. Las razones, para que no se reponga sin
revisarlas:

- La fila **ya es** la aprobación (decisión **A1**), así que agrupar por ella agrupa por sí misma.
- Casi ninguna aprobación tiene más de un registro: **1 de 412** en el tenant más grande. Agrupar tal
  cual generaba ~410 cabeceras de una sola fila.
- Para que fuera usable había que juntar las de un registro en un grupo aparte
  (*"Aprobaciones con un solo registro"*), lo que dejaba un modo con dos grupos de naturaleza
  distinta: uno real y uno de sobrantes. Ese cálculo es justo lo que resultaba confuso.

Si en el futuro hace falta ver las versiones de una aprobación, el lugar natural es el panel de
detalle (que ya muestra `VERSION`), no un modo de agrupación del listado.

### 8.4 Exportación — **oculta inicialmente**

> Entra desactivada: `config('rim.exportar') = false`. La bandera gobierna el botón **y** el método
> `exportar()`, así que con ella en `false` no hay forma de generar el archivo. Habilitarla es cambiar
> ese `false` por `true` — el código está completo y verificado (§8.5, criterio 7).

Excel con dos hojas: **Registros** (una fila por RIM, con bucket, fechas, prórrogas, saldo en cantidad
y valor, unidad, moneda, alertas de cobertura) y **Líneas** (una fila por `detalles_ris`). Respeta el
filtro y la búsqueda activos, no la expansión.

⚠️ `QUEUE_CONNECTION=sync`: la exportación corre dentro del request. Para el grupo 41 son 1 424
registros y ~5 000 líneas — aceptable, pero el criterio de aceptación fija un tope de 10 s y, si se
excede, se acota el export al filtro activo.

### 8.5 Criterios de aceptación — Etapa 2

| # | Criterio | Resultado medido (grupo 41, 2026-09-10) |
|---|---|---|
| 1 | `Gestionar` abre el panel con los catálogos del ERP resueltos | Aprobación **50105664**: tipo *Solicitud de licencia o registro*, aduana *Buenaventura*, exportador *Nestlé México*, país de compra *México*, medio *Marítimo*, moneda *USD*. Tres secciones: vencimiento y prórrogas, datos del registro, líneas de producto |
| 2 | El panel marca las líneas sin reemplazo y coincide con la fila | 13 líneas en el panel (la fila dice 13), **4** marcadas (la fila dice 4) |
| 3 | El panel respeta el aislamiento de tenant | Pidiendo el `ID_REGISTRO` **1093**, de otro cliente: devuelve `null` y limpia `detalleId`. Sin fuga |
| 4 | Render con el panel abierto | **5 consultas** (registros + 2 de cobertura + detalle + líneas del detalle), 808 ms de SQL |
| 5 | Los dos agrupamientos suman lo mismo que el listado y **no** consultan de más | `vencimiento` 5 grupos · `cliente` 3 grupos — los dos con **412 registros** y **3 consultas**. Un modo no reconocido (`aprobacion`) se ignora y deja el listado en `vencimiento` |
| 6 | Agrupar por cliente ordena por urgencia | PURINA 148 (52 alertas) · NESTLE DE COLOMBIA 252 (31) · LA ROSA 12 (3) |
| 7 | El Excel sale del mismo universo que la pantalla | 2 hojas, **413** filas de registros (412 + encabezado) y **1 189** de líneas, 125 KB, **1,3 s** (tope acordado: 10 s) |
| 8 | El modal de órdenes de compra funciona con las clases compartidas | Producto *Leche en polvo descremada*: 77 RIM, estados `vigente` 2 · `agotado` 2 · `vencido` 73, saldo del **producto** (37 500 de 100 000 KG), no del RIM completo |
| 9 | Ninguna escritura en `websimex` ni en `cti` | Solo `SELECT` |

**Dos correcciones que salieron de la verificación:**

1. **Fuga de tenant en el modal de órdenes de compra (código preexistente).** La consulta anterior
   filtraba por importador solo con `when($this->importador, …)`, y `$this->importador` viene de
   `Compania::find($importador)->id_websimex ?? null`: si la orden no tenía compañía asociada, el modal
   listaba RIM de **cualquier cliente**. Al pasar por `ConsultaSeguimientoRim`, el filtro por los
   importadores del tenant es incondicional.
2. **Bucket interno filtrándose a la interfaz.** `vencido_antiguo` (lo vencido hace más de la ventana)
   es un bucket que el tablero descarta, pero el modal no aplica esa ventana y lo habría pintado como
   *Vigente*. Ahí se normaliza a `vencido`: en ese contexto un RIM vencido hace un año sigue siendo un
   vencido. Además el orden del modal pasó a poner primero los RIM que todavía sirven — con 77 RIM para
   un producto, el orden por fecha ascendente dejaba arriba justo los inservibles.

---

## 9. Plan de trabajo

### Etapa 1

| # | Paso | Estado |
|---|---|---|
| 1 | `config/rim.php` con umbrales, ventana, criterio de cobertura y mapa de color | ✅ [config/rim.php](../config/rim.php) |
| 2 | `ClasificadorRim` (buckets, días relativos, leyenda de prórrogas) | ✅ [ClasificadorRim.php](../app/Http/Clases/Rim/ClasificadorRim.php) |
| 3 | `ConsultaSeguimientoRim`: consulta agregada + tenant + búsqueda | ✅ [ConsultaSeguimientoRim.php](../app/Http/Clases/Rim/ConsultaSeguimientoRim.php) |
| 4 | Reglas de cobertura §5, acotadas a sus candidatos | ✅ `lineasSinCobertura()` |
| 5 | `FormatoRim` (cantidades, dinero, porcentajes, fechas) | ✅ [FormatoRim.php](../app/Http/Clases/Rim/FormatoRim.php) |
| 6 | Componentes Blade `kpi`, `prorrogas`, `consumo` con los tokens de §1.3 | ✅ [components/rim/](../resources/views/components/rim/) |
| 7 | `SeguimientoRimV2` + vista principal + parciales de grupo, fila y líneas | ✅ [SeguimientoRimV2.php](../app/Http/Livewire/Rim/SeguimientoRimV2.php) |
| 8 | Divulgación progresiva, `Mostrar más`, expansión de líneas | ✅ |
| 9 | Estados de carga / vacío / error / sin compañías (§7.4) | ✅ |
| 10 | Vista de contenido + contenedor iframe + 2 rutas con `auth` | ✅ `/rim/seguimiento-v2`, `/rim/show-seguimiento-v2` |
| 11 | Verificación de §7.5, incluido aislamiento con 4 tenants y un tercero | ✅ |
| 12 | Paso en base de datos (menú + `aplicaciones` del piloto) | ⏳ pendiente de elegir el cliente piloto |

**Optimización que apareció al implementar.** `login.blade.php` ya deja en `session('companias')` la
lista de `id_websimex` del tenant, calculada con el mismo filtro para cliente normal y para tercero.
El componente la toma de ahí, así que el camino normal **no gasta ninguna consulta** en resolver el
tenant (la V1 hacía una a `compania` en cada `mount`). La consulta a `compania` queda como respaldo si
la sesión no la trae, y la razón social —que solo hace falta para el filtro por compañía— se consulta
únicamente cuando el tenant tiene más de una y se cachea 10 minutos con clave derivada del tenant.

### Etapa 2

| # | Paso | Estado |
|---|---|---|
| 12 | Panel de detalle (consulta `detalle()` + vista) | ✅ |
| 13 | Agrupar por cliente, con un solo parcial de grupo para los dos modos | ✅ |
| 14 | `SeguimientoRimExport` (2 hojas) + botón `Exportar` | ✅ |
| 15 | Migrar `PurchaseOrderProductForm` a las clases compartidas | ✅ |
| 16 | Verificación de §8.5 | ✅ |

### Paso en base de datos — **aprobado, pendiente de ejecución por el equipo**

Como en [SPEC_PurchaseOrderV2.MD](SPEC_PurchaseOrderV2.MD), la V2 no aparece sin tocar datos. El SQL
está listo en **[specs/sql/SeguimientoRIM_V2_menu.sql](sql/SeguimientoRIM_V2_menu.sql)**, con `SELECT`
de verificación antes y después, transacción, y un solo comando para deshacer.

| | Decisión |
|---|---|
| **Piloto** | Grupo empresarial **41 (Nestlé)** — compañías 56, 57, 62 y 63. Es el tenant más grande (412 registros) y el que el diseñador usó para el mock |
| **Perfiles** | Restringidos durante el piloto: `{"perfil":[1,2], "usuario":[291,292,319,348]}` — no la lista completa de 57 perfiles del menú 171 |
| **Ejecución** | La dispara el equipo. El `.env` apunta a bases reales compartidas y el cambio afecta el menú de clientes en producción |

1. `INSERT` en `menus`: `Seguimiento RIM V2`, `ruta = '/rim/seguimiento-v2'`, `depende = 3` (Operaciones),
   `orden = 18`, `nivel = 3`, `regimen = 'I'`, `estado = 1`.
2. `JSON_ARRAY_APPEND` del id resultante en `compania.aplicaciones` de las 4 compañías del piloto.
3. Tras validar el piloto (y avisar del cambio del punto 2 de §12): abrir perfiles, propagar a las 38
   compañías que hoy tienen el menú 171, y poner `estado = 0` en el 171. Está al pie del mismo archivo.

El script es idempotente y **no toca el menú 171 ni la V1**. No se ejecuta ningún
`php artisan migrate` (regla 3 de [CLAUDE.md](../CLAUDE.md)).

**Nota sobre los motores:** `asercolg_po` (el menú, `compania`) es **MariaDB 10.6.28** y admite las
funciones JSON que usa el script — verificado en lectura. `websimex`, donde corren las consultas
pesadas del módulo, es **MySQL 5.7.42** y no tiene CTE ni funciones de ventana (§2).

---

## 10. Riesgos y supuestos

| # | Riesgo | Mitigación |
|---|---|---|
| **R1** | El dinero del ERP está inflado por el cupo global (`TOTAL_USD` > 100 M USD en 3 303 RIM aprobados) | Decisión **D**: dinero solo a nivel de fila y en su moneda; nunca agregado |
| **R2** | El `% utilizado` está saturado (promedio 73–92 %; 238 de 1 045 vencidos ≥ 99 %): la barra parecerá casi siempre llena | La barra informa, **no** alerta. El color lo da el bucket de tiempo, como en el diseño. Si en el piloto resulta ruido visual, se degrada a texto |
| **R3** | La tarjeta `Sin saldo` salta de 10 a 116 y algunos "Vigentes" pasan a "Sin saldo": el usuario verá un cambio grande | Es el objetivo de la decisión **A**. Requiere aviso al cliente piloto antes del despliegue y una nota en la interfaz durante la convivencia V1/V2 |
| **R4** | MySQL 5.7 sin CTE ni funciones de ventana | Tablas derivadas y `GROUP BY`; ya validado en las mediciones |
| **R5** | `oklch()` no lo soportan navegadores anteriores a Chrome 111 / Safari 15.4 | Los tokens se declaran en hex (§1.3) |
| **R6** | Tailwind por CDN genera clases en runtime; las clases dinámicas del tenant pueden no generarse | Los colores van por *custom properties* en un `<style>` propio, no por clases Tailwind construidas con interpolación |
| **R7** | La regla de cobertura de agotados es nueva y no está validada en producción: marca **78 de 116** agotados como alerta en el grupo 41, un volumen alto | Se entrega con el conteo visible (`78 sin reemplazo`) y el detalle por línea en el panel, para que el negocio la contraste antes de que dispare correos o decisiones. El umbral y el criterio son configurables |
| **R8** | El `.env` apunta a bases reales compartidas | Todo el desarrollo es de lectura; el guion de medición usa `SELECT`/`EXPLAIN` |
| **R9** | La V1 y la V2 mostrarán números distintos durante la convivencia | Documentado en el menú piloto y en la nota de interfaz |

**Supuestos:** el PHP de producción es ≥ 7.3 y se escribe para 7.3 (decisión **C**);
`CANTIDAD_DISPONIBLE` seguirá siendo el remanente que decrece con cada declaración (verificado: 0 de
138 337 filas con disponible > cantidad); el sincronizador del ERP mantiene el ritmo actual, de modo
que cachear la cobertura por día es correcto.

---

## 11. Fuera de alcance

- **Flujo de solicitud de prórroga** hacia CTI (`solicitud_rim_prorroga`). No está diseñado y exige
  definir permisos y notificaciones. El panel de detalle queda preparado para recibirlo.
- **Correo periódico de RIM por vencer** (hallazgo U7). Depende de `QUEUE_CONNECTION=sync`.
- **Gemelo de exportaciones.** El RIM es un instrumento de importación; no aplica la regla 7 de
  [CLAUDE.md](../CLAUDE.md). Se deja escrito para que nadie lo "complete" después.
- **Retirar la V1.** Es un paso posterior a la validación del piloto.
- **Refactor del componente compartido `x-input-search`** (hallazgo L5): lo usan ~15 vistas.
- **Estadísticas RIM** ([EstadisticasRim.php](../app/Http/Livewire/Rim/EstadisticasRim.php)), que mide
  SLA de asistentes y es otro módulo.

---

## 12. Puntos que requieren aprobación explícita

| # | Punto | §|
|---|---|---|
| 1 | Eliminar el subtexto monetario de las tarjetas KPI (el diseño lo pide, los datos no lo sostienen) | §0, §7.2 |
| 2 | `Sin saldo` pasa de 10 a 116 registros para el cliente piloto | §10 · R3 |
| 3 | La regla de cobertura de agotados marca **78 de 116** como alerta | §5.2 |
| 4 | Unificar ambas reglas en `CODIGO_PRODUCTO` (resultado idéntico al `ID_PRODUCTO` actual en vencidos, y tolera productos duplicados en el ERP) | §5.1 |
| 5 | Enlazar solo el color de acento al tenant y fijar los colores de estado | §1.3 |
| 6 | Los tres pasos en base de datos de §9 | §9 |
| 7 | Que la Etapa 2 cierre sin capacidad de escritura (sin solicitud de prórroga) | §8.2, §11 |
