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.
255 lines
8.6 KiB
Markdown
255 lines
8.6 KiB
Markdown
# 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 ~139–144) 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 ~139–144 (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 1–3 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+]
|
||
```
|