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

255 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```tsx
<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
```tsx
// 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:
```tsx
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:
```tsx
// 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`):
```tsx
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:
```tsx
<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
```bash
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
```bash
npm run build
```
Sin errores TypeScript.
### 3. Tests
```bash
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+]
```