# Implementation Plan: Corrección de Bugs de Rendimiento y Deuda Técnica Frontend

**Feature Branch**: `010-correccion-bugs-rendimiento-frontend`

**Created**: 2026-07-04

**Status**: Draft

**Input**: `spec.md` + análisis de rendimiento (20 problemas identificados)

> **Constitution (II. Skinny Controllers, IV. Inertia.js Frontera)**: Este plan define CÓMO
> se implementarán los requisitos técnicos del spec. Es un plan exclusivamente frontend;
> no hay cambios de backend ni migraciones.

---

## User Review Required

> [!IMPORTANT]
> **Grupo A (Memory Leaks / Correctitud):** Los puntos 1–4 son correcciones de bug puro
> sin cambio de comportamiento observable. Se pueden desplegar sin revisión extendida de QA.

> [!WARNING]
> **Grupo B (Refactor de arquitectura):** El punto 5 (selectores Redux a nivel de módulo)
> y el punto 6 (eliminar MUI) modifican puntos de integración. Requieren prueba manual en
> staging en las páginas de contratos multi-punto y el layout global antes de merge.

> [!CAUTION]
> **Punto 7 (RightSidebar lazy):** La carga diferida del panel de configuración puede
> producir un flash vacío en el montaje inicial si el usuario tiene un tema no-default
> guardado. Decidir si se acepta ese trade-off o si se añade un skeleton mínimo.

---

## Open Questions

- ¿El `RightSidebar` carga configuración de tema guardada en localStorage al montarse?
  Si es así, la carga lazy puede provocar un destello del tema default antes de que se
  aplique el guardado. ¿Es aceptable o se necesita un placeholder de skeleton?
- ¿Los layouts Horizontal y TwoColumn están efectivamente deshabilitados en producción?
  Si es así, sus imports lazy en `Sidebar.tsx` pueden eliminarse completamente (reducción
  de bundle adicional), no solo dejarse lazy.
- ¿Existe ya alguna convención de guard de entorno para logs en el proyecto
  (ej. `if (import.meta.env.DEV)`)? De lo contrario, se usará esa convención estándar de Vite.

---

## Proposed Changes

### Grupo A — Memory Leaks & Correctitud (P1, sin riesgo)

---

#### [MODIFY] `resources/js/Layouts/Sidebar.tsx`

**Problema:** `useEffect` sin array de dependencias → acumula event listeners en cada render.

**Cambio:** Añadir `[]` como segundo argumento y retornar función de cleanup con `removeEventListener`.

```diff
- useEffect(() => {
-   var verticalOverlay = document.getElementsByClassName("vertical-overlay");
-   if (verticalOverlay) {
-     verticalOverlay[0].addEventListener("click", function () {
-       document.body.classList.remove("vertical-sidebar-enable");
-     });
-   }
- });
+ useEffect(() => {
+   const overlay = document.getElementsByClassName("vertical-overlay")[0] as Element | undefined;
+   if (!overlay) return;
+   const handler = () => document.body.classList.remove("vertical-sidebar-enable");
+   overlay.addEventListener("click", handler);
+   return () => overlay.removeEventListener("click", handler);
+ }, []);
```

---

#### [MODIFY] `resources/js/Layouts/VerticalLayouts/index.tsx`

**Problema:** `useEffect` de resize sin cleanup → listeners acumulados en cada cambio de `resizeSidebarMenu`.

**Cambio:** Retornar función de cleanup.

```diff
  useEffect(() => {
    window.addEventListener("resize", resizeSidebarMenu, true);
+   return () => window.removeEventListener("resize", resizeSidebarMenu, true);
  }, [resizeSidebarMenu]);
```

---

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/hooks/useContratoUniClienteUniPunto.ts`

**Problema:** `data` en las dependencias del `useEffect` de sincronización de cliente → re-evaluación
en cada keystroke.

**Cambio:** Eliminar `data` de las dependencias. El cuerpo del efecto debe usar solo `selectedCliente`
para derivar los valores a establecer, pasando un callback funcional a `setData` para leer `prevData`.

```diff
  useEffect(() => {
-    const cliente = selectedCliente.length > 0 ? selectedCliente[0] : null;
-    if (
-        data.documento_fiscal !== (cliente?.NumCifCli || '') ||
-        data.razon_social !== (cliente?.RazSocCli || '')
-    ) {
-        setData(prevData => ({
-            ...prevData,
-            documento_fiscal: cliente?.NumCifCli || '',
-            razon_social: cliente?.RazSocCli || ''
-        }));
-    }
-  }, [selectedCliente, data]);
+    const cliente = selectedCliente[0] ?? null;
+    setData(prev => ({
+        ...prev,
+        documento_fiscal: cliente?.NumCifCli ?? '',
+        razon_social: cliente?.RazSocCli ?? '',
+    }));
+  }, [selectedCliente]);
```

> [!NOTE]
> La guarda condicional era necesaria precisamente porque `data` estaba en las dependencias.
> Al quitarla, el callback funcional de `setData` siempre lee el estado más reciente sin
> crear un ciclo de dependencias, por lo que la guarda deja de ser necesaria.

---

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/Contrato.tsx`

**Problema (US-5):** `fetchCupsElectricosIfNeeded` y `fetchCupsGasIfNeeded` no cancelan
peticiones anteriores cuando `selectedCliente` cambia rápidamente → race condition.

**Cambio:** Introducir `AbortController` en ambos `useEffect` de carga de CUPS.

```diff
  useEffect(() => {
+   const controller = new AbortController();
    fetchCupsElectricosIfNeeded();
    if (!localidadesLoaded) fetchLocalidadesIfNeeded();
+   return () => controller.abort();
  }, [showCupsElectric, selectedCliente]);
```

Las funciones `fetchCupsElectricosIfNeeded` y `fetchCupsGasIfNeeded` deben aceptar
un `AbortSignal` y pasarlo a `axios` (opción `{ signal }`). Los errores de tipo
`AbortError` deben silenciarse (no mostrar al usuario).

---

### Grupo B — Peticiones duplicadas & caché (P2)

---

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/hooks/useCupsElectrico.ts`
#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/hooks/useCupsGas.ts`

**Problema:** `buscarTarifas()` se llama en el `useEffect` de inicialización de cada hook → 2 peticiones
HTTP al mount cuando ambos suministros están activos, sin caché.

**Cambio:** Eliminar la llamada a `buscarTarifas()` del `useEffect` de inicialización interno de cada hook.
Los hooks deben aceptar las tarifas como **prop externa** (`tarifasElectricas` / `tarifasGas`).

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/Contrato.tsx`

**Cambio:** Centralizar la carga de tarifas en `Contrato.tsx` usando `dataService` con caché de sesión
(el `simpleCache` ya existente en `dataService.ts`). Las tarifas se cargan una vez al mount y se pasan
como props a `useCupsElectrico` y `useCupsGas`.

```typescript
// En Contrato.tsx — nuevo useEffect de carga centralizada
const [tarifasElectricas, setTarifasElectricas] = useState<any[]>([]);
const [tarifasGas, setTarifasGas] = useState<any[]>([]);

useEffect(() => {
    // dataService.getTarifasElectricas() ya usa simpleCache internamente
    getTarifasElectricas().then(r => r?.success && setTarifasElectricas(r.data ?? []));
    getTarifasGas().then(r => r?.success && setTarifasGas(r.data ?? []));
}, []);
```

---

### Grupo C — Selectores Redux fuera del ciclo de render (P1)

---

#### [MODIFY] `resources/js/Layouts/index.tsx`

**Problema:** `createSelector` instanciado dentro del cuerpo del componente `Layout` → caché
vacía en cada render.

**Cambio:** Mover `selectLayoutProperties` y `selectLayoutState` al ámbito de módulo (fuera del
cuerpo de la función del componente).

```diff
+ // ── Nivel de módulo ─────────────────────────────────────────
+ const selectLayoutState = (state: any) => state.Layout;
+ const selectLayoutProperties = createSelector(
+     selectLayoutState,
+     (layout: any) => ({
+         layoutType: layout.layoutType,
+         leftSidebarType: layout.leftSidebarType,
+         layoutModeType: layout.layoutModeType,
+         layoutWidthType: layout.layoutWidthType,
+         layoutPositionType: layout.layoutPositionType,
+         topbarThemeType: layout.topbarThemeType,
+         leftsidbarSizeType: layout.leftsidbarSizeType,
+         leftSidebarViewType: layout.leftSidebarViewType,
+         leftSidebarImageType: layout.leftSidebarImageType,
+         preloader: layout.preloader,
+         sidebarVisibilitytype: layout.sidebarVisibilitytype,
+         backgroundImageType: layout.backgroundImageType,
+     })
+ );
+ // ────────────────────────────────────────────────────────────

  const Layout = ({children, props}: any) => {
-   const selectLayoutState = (state: any) => state.Layout;
-   const selectLayoutProperties = createSelector(
-       selectLayoutState,
-       (layout: any) => ({ ... })
-   );
    const { layoutType, ... } = useSelector(selectLayoutProperties);
    ...
  };
```

#### [MODIFY] `resources/js/Layouts/Header.tsx`

**Problema:** `selectDashboardData` creado dentro del cuerpo de `Header`.

**Cambio:** Mover al ámbito de módulo, idéntica estrategia al punto anterior.

#### [MODIFY] `resources/js/Layouts/VerticalLayouts/index.tsx`

**Problema:** `selecVerticaltLayoutProperties` creado dentro del componente.

**Cambio:** Mover al ámbito de módulo.

---

### Grupo D — RightSidebar lazy (P1 — bundle)

---

#### [MODIFY] `resources/js/Layouts/index.tsx`

**Problema:** `RightSidebar` (~101 KB) importado de forma síncrona en el layout global.

**Cambio:** Convertir a lazy con `React.Suspense`.

```diff
- import RightSidebar from '../Components/Common/RightSidebar';
+ const RightSidebar = React.lazy(() => import('../Components/Common/RightSidebar'));

  // En el JSX:
- <RightSidebar />
+ <React.Suspense fallback={null}>
+     <RightSidebar />
+ </React.Suspense>
```

Eliminar también `PropTypes` redundante (líneas 177–179 de `index.tsx`):

```diff
- Layout.propTypes = {
-     children: PropTypes.node,
- };
```

---

### Grupo E — Eliminar MUI de ContratoMultiPunto (P3 — bundle)

---

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteMultiPunto/ContratoMultiPunto.tsx`

**Problema:** `Dialog`, `DialogContent`, `DialogTitle`, `IconButton` de `@mui/material` y
`CloseIcon` de `@mui/icons-material` importados solo para el modal de edición de CUPS.

**Cambio:** Reemplazar `Dialog` de MUI con el `Modal` de `react-bootstrap` ya usado en
el resto del proyecto. Adaptar el markup del modal de edición de CUPS para usar la API
de `Modal` de React-Bootstrap (compatible con los estilos existentes).

```diff
- import { Dialog, DialogContent, DialogTitle, IconButton } from '@mui/material';
- import CloseIcon from '@mui/icons-material/Close';
+ import { Modal } from 'react-bootstrap';

  // En el JSX (modal de edición de CUPS eléctrico):
- <Dialog open={openDialogElectrico} onClose={handleCloseElectrico} maxWidth="xl" fullWidth>
-     <DialogTitle>
-         Editar Suministro Eléctrico
-         <IconButton onClick={handleCloseElectrico}><CloseIcon /></IconButton>
-     </DialogTitle>
-     <DialogContent>...</DialogContent>
- </Dialog>
+ <Modal show={openDialogElectrico} onHide={handleCloseElectrico} size="xl">
+     <Modal.Header closeButton>
+         <Modal.Title>Editar Suministro Eléctrico</Modal.Title>
+     </Modal.Header>
+     <Modal.Body>...</Modal.Body>
+ </Modal>
```

> [!NOTE]
> Verificar que `@mui/material` y `@mui/icons-material` no sean importados en ningún otro
> archivo del proyecto antes de eliminar las dependencias del `package.json`.

---

### Grupo F — Console.log silenciados en producción (P3)

---

#### [MODIFY] `resources/js/services/dataService.ts`

**Cambio:** Envolver todos los `console.log` de diagnóstico con guard de entorno Vite:

```diff
- console.log(response);
+ if (import.meta.env.DEV) console.log('[dataService]', response);
```

Afecta a las líneas: 54, 128, 249, 250, 251.

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/hooks/useCupsElectrico.ts`

Línea 244 — mismo patrón de guard.

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteUniPunto/hooks/useCupsGas.ts`

Línea 231 — mismo patrón de guard.

#### [MODIFY] `resources/js/Pages/Eneon/PuntosSuministro/Edit.tsx`

Línea 18 — mismo patrón de guard.

#### [MODIFY] `resources/js/Pages/Eneon/Dashboard/Components/ClientDetailPanel.tsx`

Línea 25 — mismo patrón de guard.

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/ReferenciasCodigo/CupsGasSection.tsx`
#### [MODIFY] `resources/js/Pages/Eneon/Contratos/ReferenciasCodigo/CupsElectricSection.tsx`

Múltiples `console.log` (>10 y >8 respectivamente) — mismo patrón de guard o eliminación
directa si se confirma que estos componentes son exclusivamente legacy y no se usan en ruta crítica.

---

## Orden de ejecución recomendado

| Fase | Grupo | Riesgo | Archivos afectados |
|------|-------|--------|--------------------|
| 1 | A — Memory Leaks | 🟢 Bajo | `Sidebar.tsx`, `VerticalLayouts/index.tsx`, `useContratoUniClienteUniPunto.ts` |
| 2 | A — Race condition | 🟢 Bajo | `Contrato.tsx` |
| 3 | F — Console.log | 🟢 Bajo | `dataService.ts`, 4 hooks/páginas |
| 4 | C — Selectores Redux | 🟡 Medio | `Layouts/index.tsx`, `Header.tsx`, `VerticalLayouts/index.tsx` |
| 5 | D — RightSidebar lazy | 🟡 Medio | `Layouts/index.tsx` |
| 6 | B — Caché tarifas | 🟡 Medio | `Contrato.tsx`, `useCupsElectrico.ts`, `useCupsGas.ts` |
| 7 | E — Eliminar MUI | 🟠 Moderado | `ContratoMultiPunto.tsx`, `package.json` |

---

## Verification Plan

### Automated Tests

No hay tests automatizados de frontend configurados actualmente. La verificación es manual.

### Manual Verification

1. **Memory leaks (Fase 1):**
   - Abrir DevTools → pestaña Performance → navegar entre 5 páginas distintas.
   - Verificar en la pestaña Elements → Event Listeners que `vertical-overlay` tiene
     exactamente 1 listener, no N.

2. **Re-renders excesivos (Fase 1):**
   - Abrir DevTools → React DevTools → Profiler.
   - Seleccionar un cliente en el formulario de contrato y teclear en 3 campos distintos.
   - Verificar que solo el componente del campo editado se re-renderiza, no toda la sección de gas/eléctrico.

3. **Race condition CUPS (Fase 2):**
   - Seleccionar cliente A → inmediatamente seleccionar cliente B antes de 1 segundo.
   - Verificar que los CUPS mostrados pertenecen a cliente B.

4. **Console.log (Fase 3):**
   - Ejecutar `npm run build` y cargar la app en modo producción (o con `NODE_ENV=production`).
   - Abrir la consola y navegar por contratos → 0 logs de diagnóstico.

5. **Selectores Redux (Fase 4):**
   - Navegar entre páginas: contratos → clientes → dashboard → contratos.
   - Verificar que el layout no parpadea ni aplica clases incorrectas.

6. **RightSidebar lazy (Fase 5):**
   - Medir `bundle` con `npm run build` antes y después.
   - Verificar que el panel de temas sigue abriéndose y aplicando cambios correctamente.

7. **Caché tarifas (Fase 6):**
   - Abrir Network DevTools → filtrar por `/tarifa`.
   - Abrir formulario de contrato (ambos suministros) → máximo 2 requests.
   - Cerrar y reabrir → 0 requests nuevas (servidas por caché).

8. **Eliminar MUI (Fase 7):**
   - Verificar que el modal de edición de CUPS en multi-punto abre y funciona con React-Bootstrap Modal.
   - Verificar con `npm run build` que el bundle ya no incluye `@mui`.
