# Implementation Plan: Ficha de Cliente 360 — CRUD Completo y Consistente

**Feature Branch**: `014-ficha-cliente-dashboard-crud`

**Created**: 2026-09-18

**Status**: Draft

**Input**: `spec.md` + auditoría de código de `resources/js/Pages/Eneon/Dashboard/` y sus rutas/
controladores asociados (18 sep 2026)

> **Constitution (II. Skinny Controllers, IV. Inertia.js Frontera, V. Frontend Modular)**: Este plan
> define CÓMO completar los CRUD de Cuentas Bancarias, Contactos y Puntos de Suministro dentro de
> `ClientDetailPanel.tsx`, manteniendo la cuadrícula de tarjetas actual (decisión de diseño ya
> tomada: sin pestañas ni acordeón).

---

## Resumen técnico

`ClientDetailPanel.tsx` (877 líneas) ya recibe todos los datos necesarios en una sola llamada
(`DataController::getClienteDetalle`, con `with([...])` completo). El trabajo es, por tanto,
mayormente de **frontend + rutas/controladores puntuales de backend que faltan**, no de modelado de
datos nuevo:

1. Añadir la operación de **eliminar** donde falta (Cuentas Bancarias, Contactos) reutilizando o
   creando rutas Laravel delgadas.
2. **Exponer** en el panel capacidades que el backend ya tiene pero el frontend no muestra
   (activar/suspender/eliminar Punto de Suministro).
3. Quitar la restricción artificial de "un solo contacto" en la UI.
4. Introducir un **componente compartido de estado de tarjeta** (loading/empty/error) y una
   **función compartida de notificación** de éxito/error, y aplicarlos en los 5 bloques sin exigir
   que los 4 bloques compartan la misma arquitectura interna de guardado (ver User Review Required).

No hay cambios de esquema de base de datos. No se modifica la disposición general de la pantalla.

---

## User Review Required

> [!IMPORTANT]
> **Semántica de "eliminar contacto" (US2):** el modelo de datos separa el **Contacto** (persona,
> puede estar vinculada a más de un cliente) del vínculo **Cliente↔Contacto**
> (`contacto_detalle_cliente`). Ya existen dos rutas distintas: `contactos.destroy` (borra el
> contacto globalmente) y `contactos.detalle-clientes.destroy` (desvincula del cliente actual). Este
> plan asume que "eliminar" desde la ficha del cliente MUST usar la segunda (desvincular), nunca la
> primera, para no borrar un contacto que podría estar vinculado a otro cliente. **Confirmar este
> criterio con negocio antes de implementar** si existe algún caso donde sí deba borrarse
> globalmente.

> [!IMPORTANT]
> **Regla de rechazo al eliminar Cuenta Bancaria (FR-006):** no hay hoy una regla de negocio
> documentada sobre qué hace que una cuenta bancaria no se pueda eliminar (¿un contrato con
> domiciliación activa sobre esa cuenta?). Debe definirse antes de implementar el `destroy`; mientras
> tanto se implementa como baja lógica simple (marcar inactiva) si no se define una restricción
> específica, siguiendo el patrón ya visto en `cuentasBancariasActivas` (sugiere que ya existe un
> concepto de estado activo/inactivo para cuentas bancarias).

> [!WARNING]
> **No se fuerza un refactor arquitectónico de `EditarPuntoSuministroModal.tsx`/
> `CrearPuntoSuministroModal.tsx` (771/697 líneas).** Hoy gestionan su propio `axios` internamente,
> a diferencia de Contactos/Cuentas/Documentos donde `ClientDetailPanel` hace la llamada. Unificar
> completamente la arquitectura sería un refactor grande y arriesgado en los componentes más
> complejos del panel. Este plan solo exige que el **resultado visible** (confirmación de éxito,
> formato del error) sea idéntico, mediante una función de notificación compartida — no que el
> código interno de guardado se mueva. Si se prefiere la unificación completa, es un esfuerzo
> adicional fuera de este plan (candidato a feature separada de refactor).

---

## Open Questions

- ¿Cuál es el mecanismo de notificación estándar a replicar en los 4 bloques — `SweetAlert2` (ya
  usado para confirmar la eliminación de documentos) o algún componente de toast propio del
  proyecto? Definir antes de crear la función de notificación compartida.
- ¿La eliminación de un punto de suministro debe exigir que sus CUPS (eléctrico/gas) se eliminen
  primero, o puede hacerse en cascada desde la misma acción? Afecta el mensaje de confirmación de
  FR-005.
- ¿Existe ya un concepto de "cuenta bancaria activa/inactiva" a nivel de columna de BD (el nombre de
  la relación `cuentasBancariasActivas` en `DataController::getClienteDetalle` lo sugiere), o el
  `destroy` debe ser un borrado físico? Condiciona si FR-001 es una baja lógica o un `DELETE` real.

---

## Proposed Changes

### Grupo A (US1) — Eliminar Cuenta Bancaria

#### [NEW] Ruta `clientes.cuenta-bancaria.destroy`

`routes/web.php` — junto a `clientes.cuenta-bancaria.store`/`.update` ya existentes:

```php
Route::delete('clientes/cuenta-bancaria/{id}', [ClientesController::class, 'destroyCuentaBancaria'])
    ->name('clientes.cuenta-bancaria.destroy');
```

#### [NEW/MODIFY] `app/Http/Controllers/ClientesController.php::destroyCuentaBancaria`

Controlador delgado: valida la restricción de negocio pendiente de definir (ver User Review
Required), delega en un método del trait/servicio correspondiente, responde `success`/`message`.

#### [MODIFY] `resources/js/Pages/Eneon/Dashboard/Components/ClientDetailPanel.tsx` (card de Cuentas Bancarias, ~L547-621)

Añadir botón "Eliminar" junto al de editar, con confirmación (mismo patrón que Documentos) y
llamada a la nueva ruta; actualizar la lista local sin recargar toda la ficha.

---

### Grupo B (US2) — Múltiples contactos + eliminar contacto

#### [MODIFY] `ClientDetailPanel.tsx` (~L788-807, botón "Asignar" condicionado a `contactos.length === 0`)

Quitar la condición: el botón "Añadir contacto" MUST estar siempre visible, no solo cuando la lista
está vacía. Renombrar a un texto que no implique sustitución ("Añadir contacto" en vez de "Asignar").

#### [MODIFY] `resources/js/Pages/Eneon/Dashboard/Editar/CrearContactoModal.tsx`

Confirmar que no existe ninguna suposición interna de "reemplaza al contacto actual"; ajustar si la
hay.

#### [NEW] Botón + wiring de eliminar contacto en `ClientDetailPanel.tsx`

Usar `contactos.detalle-clientes.destroy` (desvincular, ver User Review Required) con confirmación
explícita, actualizando la lista local del bloque de Contactos tras la respuesta.

---

### Grupo C (US3) — Ciclo de vida completo de Punto de Suministro

#### [MODIFY] `ClientDetailPanel.tsx` (card de Puntos de Suministro)

Añadir acciones "Activar"/"Suspender"/"Eliminar" por punto de suministro (menú contextual o botones
según estado actual), reutilizando las rutas ya existentes en el backend:

```php
// ya existen en routes/web.php — solo se exponen en el frontend:
puntos-suministro.activar
puntos-suministro.suspender
puntos-suministro.destroy
```

Cada acción MUST pedir confirmación explícita (activar puede no necesitarla si se considera
reversible de bajo riesgo — a decidir en implementación; suspender/eliminar sí la requieren
siempre). El cambio de estado se refleja de inmediato en la tarjeta sin recargar toda la ficha.

---

### Grupo D (US4) — Guardado consistente

#### [NEW] `resources/js/Pages/Eneon/Dashboard/services/dashboardFeedback.ts` (o ubicación equivalente ya usada en el proyecto)

Funciones `notifySuccess(mensaje)` / `notifyError(mensaje)` que envuelven el mecanismo de
notificación definido en Open Questions, para que los 4 bloques las invoquen tras cada operación de
guardado/eliminación, en vez de cada uno construir su propio mensaje ad-hoc.

#### [MODIFY] Los 4 flujos de guardado dentro de `ClientDetailPanel.tsx` (Cuentas, Contactos, Documentos) y el callback `onUpdated`/`onSaved` de los modales de Punto de Suministro

Sustituir los mensajes de éxito/error ad-hoc actuales por llamadas a `notifySuccess`/`notifyError`.

---

### Grupo E (US5) — Estado de carga/vacío/error por tarjeta

#### [NEW] `resources/js/Pages/Eneon/Dashboard/Components/CardSectionStatus.tsx`

Componente reutilizable que recibe `{ isLoading, isEmpty, error, emptyAction, onRetry, children }`
y renderiza: skeleton mientras `isLoading`, mensaje de "sin datos" + botón de acción cuando
`isEmpty`, mensaje de error + botón "Reintentar" cuando `error`, o `children` en cualquier otro
caso.

#### [MODIFY] `ClientDetailPanel.tsx` — envolver cada una de las 5 cards (Datos del cliente, Puntos de
Suministro, Contactos, Cuentas Bancarias, Documentos) con `CardSectionStatus`, sustituyendo el
`loading.show()/hide()` global como única señal.

#### [MODIFY] `resources/js/Pages/Eneon/Dashboard/hooks/useClienteDashboardData.ts`

Exponer un estado de error explícito por la carga inicial de la ficha y una función `retry()`,
consumidos por `CardSectionStatus` en el nivel de página (FR-011).

---

## Orden de ejecución recomendado

| Orden | Grupo | User Story | Prioridad | Riesgo |
|---|---|---|---|---|
| 1 | E — Componente de estado de tarjeta | US5 | P2 | 🟢 Bajo (aditivo, sin tocar lógica de negocio) |
| 2 | D — Función de notificación compartida | US4 | P2 | 🟢 Bajo (aditivo) |
| 3 | A — Eliminar Cuenta Bancaria | US1 | P1 | 🟡 Medio (nueva ruta + regla de negocio pendiente de definir) |
| 4 | B — Múltiples contactos + eliminar | US2 | P1 | 🟡 Medio (decisión de semántica desvincular vs borrar) |
| 5 | C — Ciclo de vida Punto de Suministro | US3 | P1 | 🟢 Bajo (rutas backend ya existen, solo exponer en UI) |

Se recomienda E y D primero porque son aditivos y de bajo riesgo, y porque A/B/C los consumen
(cada eliminación nueva usa `notifyError`/`notifySuccess` y actualiza su tarjeta dentro del nuevo
`CardSectionStatus`), evitando tener que retocar A/B/C una segunda vez.

---

## Verification Plan

### Automated Tests

No hay tests automatizados de frontend configurados en el proyecto. Si se desea cobertura backend
para las nuevas rutas de eliminación (`destroyCuentaBancaria`, wiring de `puntos-suministro.destroy`
desde este flujo), añadir un test Pest/PHPUnit por endpoint verificando el caso de éxito y el caso
de rechazo por restricción de negocio (una vez definida en Open Questions). `./vendor/bin/pint`
antes de cerrar cada grupo.

### Manual Verification

1. **Grupo A:** eliminar una cuenta bancaria de un cliente con 2+ cuentas → desaparece solo esa,
   confirmación previa mostrada, mensaje de éxito con el mismo estilo que el resto de la ficha.
2. **Grupo B:** en un cliente con 1 contacto, añadir un segundo → ambos listados; eliminar uno →
   solo ese desaparece, el otro permanece.
3. **Grupo C:** suspender un punto de suministro activo → estado visible cambia sin recargar;
   reactivarlo → vuelve a activo; eliminar uno sin referencias activas → desaparece de la lista.
4. **Grupo D:** provocar un error de guardado en cada uno de los 4 bloques (ej. dejar un campo
   obligatorio vacío) → comparar que el aviso de error se ve y comporta igual en los 4.
5. **Grupo E:** abrir la ficha de un cliente sin ninguna cuenta bancaria registrada → la tarjeta
   muestra el estado "sin datos" con acción de añadir, distinto visualmente del estado de carga
   inicial. Forzar un fallo de red en la carga de la ficha → mensaje de error con opción de
   reintentar, sin recargar la página completa.
