Files
inq-roi-simulador-web/sprints/sprint-2/ETAPA_4_PROMPT.md
Marcos Benítez 1ff2724735 feat(sprint-2): recursos por actividad — modelo, UX y PDF
Etapas 3-6 del Sprint 2. Resumen de cambios:

**Reporte web (Etapa 3)**
- ActivitiesTable: filas expandibles con desglose fijo + recursos por actividad
- CumulativeSavingsChart: fix payback label superpuesto con eje Y
- BpmnCanvas: badge  14→20px
- TECH_DEBT cerrado: chart -inversión (ya estaba), payback label, badge

**Workspace (Etapa 4)**
- WorkspacePage: edición inline de nombre de proceso y cliente
  (click → input → Enter/blur guarda en IndexedDB, checkmark naranja 1.5s)

**PDF (Etapas 5-6)**
- pdf-sections: drawResourcesSection — tabla condicional de recursos por actividad
  con chips de tipo (rol/persona/sistema/equipamiento/insumo)
- pdf-export: página de recursos (pág. 4 cuando hay datos, metodología pasa a pág. 5)
- Fix truncamiento: splitTextToSize en resourceName y top3 ROI
- Fix texto: "ejecución" con tilde, header "Esp. (USD)" sin truncar
- TECH_DEBT cerrado: truncamiento notas metodológicas

**Tests**
- 7 tests nuevos: drawResourcesSection (4) + comportamiento condicional PDF (3)
- Total: 541 → 548 tests verdes

BREAKING: exportScenarioPdf ahora recibe activities[] como parámetro adicional.
2026-05-20 08:24:32 -03:00

8.6 KiB
Raw Permalink Blame History

Sprint 2 — Etapa 4: Edición inline de nombre de proceso y cliente

Tipo: UI — un solo componente. Sin cambios en dominio, persistencia ni reporte. Objetivo: hacer editable el nombre del proceso y el cliente directamente desde el header del workspace. Hoy son campos estáticos (<p>); deben convertirse en inputs inline que guardan en IndexedDB al confirmar. Resultado esperado: click sobre nombre o cliente → input inline → Enter o blur guarda → checkmark naranja breve confirma. Validación visual del director requerida antes del OK formal.


Archivo a modificar

Un único archivo: src/features/workspace/WorkspacePage.tsx

Si durante la implementación identificás que otro archivo necesita cambio, detenete y reportá el conflicto antes de tocarlo. No procedas sin confirmación.


Contexto: estado actual del header

El header del workspace (líneas ~139144) tiene hoy:

<div className="flex-1 min-w-0">
  <p className="text-sm font-semibold text-slate-800 truncate">{currentProcess.name}</p>
  {currentProcess.clientName && (
    <p className="text-xs text-slate-400 truncate">{currentProcess.clientName}</p>
  )}
</div>

Ambos son texto estático. La función updateProcess ya existe en el store (useProcessStore) y persiste en IndexedDB — no requiere ningún cambio fuera de este archivo.


Cambios requeridos

1. Agregar updateProcess al destructuring del store

// ANTES (línea ~26)
const { currentProcess, activities, isLoading, error, loadProcess, gateways } = useProcessStore()

// DESPUÉS
const { currentProcess, activities, isLoading, error, loadProcess, gateways, updateProcess } = useProcessStore()

2. Agregar iconos al import de lucide-react

Agregar Check y Pencil al import existente:

import {
  Loader2, BarChart3, Play, AlertTriangle,
  PanelRightClose, PanelRightOpen, Home, GitMerge, MousePointer2,
  Check, Pencil
} from 'lucide-react'

3. Agregar estado local para edición inline

Insertar antes del return, junto a los otros estados del componente:

// Estado para edición inline del header
const [editingField, setEditingField] = useState<'name' | 'client' | null>(null)
const [editValue, setEditValue] = useState('')
const [savedField, setSavedField] = useState<'name' | 'client' | null>(null)
const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

useRef ya está importado desde React — no requiere cambio en imports de React.

4. Agregar función saveField

Insertar junto a las otras funciones del componente (cerca de handleSimulate):

async function saveField(field: 'name' | 'client') {
  if (field === 'name') {
    const trimmed = editValue.trim()
    if (trimmed && trimmed !== currentProcess?.name) {
      await updateProcess({ name: trimmed })
    }
  } else {
    const trimmed = editValue.trim()
    if (trimmed !== (currentProcess?.clientName ?? '')) {
      await updateProcess({ clientName: trimmed })
    }
  }
  setEditingField(null)
  // Wow: checkmark naranja breve
  setSavedField(field)
  if (savedTimerRef.current) clearTimeout(savedTimerRef.current)
  savedTimerRef.current = setTimeout(() => setSavedField(null), 1500)
}

5. Reemplazar el bloque estático del header

Reemplazar las líneas ~139144 (el <div className="flex-1 min-w-0"> completo) con:

<div className="flex-1 min-w-0">

  {/* Nombre del proceso — editable inline */}
  {editingField === 'name' ? (
    <input
      autoFocus
      className="text-sm font-semibold text-slate-800 bg-transparent border-b border-[#F59845] outline-none w-full"
      value={editValue}
      onChange={(e) => setEditValue(e.target.value)}
      onBlur={() => saveField('name')}
      onKeyDown={(e) => {
        if (e.key === 'Enter') { e.preventDefault(); saveField('name') }
        if (e.key === 'Escape') setEditingField(null)
      }}
    />
  ) : (
    <div
      className="flex items-center gap-1 group cursor-text"
      onClick={() => { setEditValue(currentProcess.name); setEditingField('name') }}
    >
      <p className="text-sm font-semibold text-slate-800 truncate">{currentProcess.name}</p>
      {savedField === 'name'
        ? <Check className="h-3.5 w-3.5 text-[#F59845] shrink-0" />
        : <Pencil className="h-3 w-3 text-slate-300 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
      }
    </div>
  )}

  {/* Cliente — editable inline */}
  {editingField === 'client' ? (
    <input
      autoFocus
      className="text-xs text-slate-400 bg-transparent border-b border-[#F59845] outline-none w-full"
      value={editValue}
      onChange={(e) => setEditValue(e.target.value)}
      onBlur={() => saveField('client')}
      onKeyDown={(e) => {
        if (e.key === 'Enter') { e.preventDefault(); saveField('client') }
        if (e.key === 'Escape') setEditingField(null)
      }}
      placeholder="Agregar cliente…"
    />
  ) : (
    <div
      className="flex items-center gap-1 group cursor-text"
      onClick={() => { setEditValue(currentProcess.clientName ?? ''); setEditingField('client') }}
    >
      {currentProcess.clientName
        ? <p className="text-xs text-slate-400 truncate">{currentProcess.clientName}</p>
        : <p className="text-xs text-slate-300 truncate italic">Agregar cliente</p>
      }
      {savedField === 'client'
        ? <Check className="h-3 w-3 text-[#F59845] shrink-0" />
        : <Pencil className="h-2.5 w-2.5 text-slate-300 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
      }
    </div>
  )}

</div>

Comportamientos esperados

  • Click sobre el nombre → input inline con el valor actual, borde inferior naranja
  • Enter o blur → guarda, vuelve a modo lectura, aparece ✓ naranja 1.5 segundos
  • Escape → cancela sin guardar, vuelve al valor anterior
  • Nombre vacío al guardar → no guarda (el if (trimmed && ...) lo previene), vuelve al valor anterior
  • Cliente vacío al guardar → guarda string vacío (limpiar el cliente es una acción válida)
  • Sin cliente asignado → muestra "Agregar cliente…" en gris itálico; al hacer click se abre el input vacío
  • El ícono ✏️ es invisible hasta hacer hover sobre la fila — indica que es editable sin ocupar espacio visualmente

Verificaciones obligatorias antes de reportar

1. Verificación de scope con git diff — OBLIGATORIO

git diff --name-only HEAD

El resultado debe contener únicamente:

src/features/workspace/WorkspacePage.tsx

Si aparece cualquier archivo no listado:

  1. Revertilo: git checkout HEAD -- <archivo>
  2. Listarlo bajo "Conflictos de scope" en el reporte
  3. No incluirlo en el entregable

2. Build limpio

npm run build

Sin errores TypeScript.

3. Tests

npm run test

Todos los tests existentes deben seguir verdes. Esta etapa no agrega tests nuevos (UI pura, sin lógica de dominio nueva).

4. Smoke test manual

  • Click sobre el nombre del proceso → se abre input con valor actual
  • Tipear un nombre nuevo → Enter → se cierra, aparece ✓ naranja breve, el nombre cambió
  • Recargar la página → el nombre nuevo persiste (IndexedDB)
  • Click sobre el cliente → input editable
  • Borrar el cliente completamente → guardar → el campo queda vacío mostrando "Agregar cliente…"
  • Escape cancela sin guardar

Alcance estricto — lo que NO entra en esta etapa

  • GlobalSettingsPanel.tsx — no tocar (edición desde settings sigue funcionando igual)
  • Ningún archivo de reporte (src/features/report/) — Etapa 3 ya cerrada
  • Ningún archivo de dominio (src/domain/) — Etapas 13 ya cerraron dominio
  • Ningún archivo de persistencia (src/persistence/) — updateProcess del store es suficiente
  • PDFs — Etapa 5
  • Edición de campos adicionales del proceso (frecuencia anual, overhead) — BACKLOG

Reporte esperado al terminar

## Etapa 4 completada

### Archivos modificados
- src/features/workspace/WorkspacePage.tsx

### Cambios realizados
- Edición inline de nombre de proceso: click → input → Enter/blur guarda
- Edición inline de cliente: click → input → Enter/blur guarda
- Checkmark naranja (#F59845) confirmatorio con timeout 1.5s
- Ícono ✏️ visible al hover, oculto en reposo
- Escape cancela sin guardar en ambos campos

### Verificaciones
- git diff --name-only HEAD: solo WorkspacePage.tsx ✅
- npm run build: ✅
- npm run test: X/X tests verdes ✅
- Smoke test: [descripción breve]

### Conflictos de scope
[ninguno / lista de archivos revertidos y razón]

### Notas para Etapas siguientes
[hallazgos relevantes para Etapas 5+]