Files
inq-roi-simulador-web/sprints/sprint-3/ETAPA_5_PROMPT.md
Marcos Benítez 67e259e6ed feat(sprint-3): biblioteca, recursos TO-BE, ROI mejorado e identidad de marca
Sprint 3 completo — 7 etapas:

- Etapa 1-2: biblioteca de procesos con agrupación por cliente (Dexie v4,
  LibraryPage, library-store, groupId/tags en Process)
- Etapa 3: modal de configuración global por proceso (horizonte, frecuencia,
  inversión, moneda) con validación y persistencia
- Etapa 4: panel de recursos rediseñado con soporte de unidades y minutos
  de utilización (utilizationMinutes + units)
- Etapa 5: recursos en escenario automatizado — motor de simulación,
  UI de edición en ActivityPanel, comparación AS-IS/TO-BE en reporte
- Etapa 6: PDF recursos automatizados, automatedAssignedResources requerido,
  561 tests pasando
- Etapa 7: identidad de marca — AppHeader gradiente naranja/magenta con logo
  InQuality en 5 páginas, favicon con logo real, logo/marca navegan a home

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:49:43 -03:00

446 lines
17 KiB
Markdown
Raw Permalink 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 3 — Etapa 5: Recursos en escenario automatizado
## Contexto
Sos Claude Code trabajando en **InQ ROI**. Las reglas de implementación están en `simulador-web/CLAUDE.md`. El brief del sprint está en `sprints/sprint-3/BRIEF.md`.
**Estado al iniciar esta etapa:**
- Etapas 14 completadas. Dexie v4 activo. Biblioteca operativa con grupos, tags, búsqueda. Indicador stale funcionando (persiste en recarga). Botón fit-viewport en BpmnCanvas.
- `Activity.automatedAssignedResources?: ActivityResourceAssignment[]` existe en `types.ts` como campo opcional desde Etapa 1. La migración v4 ya popula las actividades existentes con `[]`.
- `simulation.ts` actualmente usa `activity.assignedResources` en ambos escenarios. La rama `useAutomated` solo cambia `directCostFixed` y `executionTimeMinutes`, pero **no cambia los recursos** — bug intencional pendiente de esta etapa.
- `ActivityPanel.tsx` tiene la sección "Automatización" con toggle, `automatedCostFixed` e `automatedTimeMinutes`. Los recursos automatizados no tienen UI todavía.
- `ActivitiesTable.tsx` recibe `perActivity: ActivitySimResult[]` y muestra `resourceCostBreakdown` en filas expandibles.
**Esta etapa conecta las tres capas: motor de simulación, UI de edición, y visualización en reporte.**
---
## Objetivo
**E.1 — Motor de simulación**
Cuando `useAutomated = true`, usar `activity.automatedAssignedResources ?? []` en lugar de `activity.assignedResources` para el cálculo de costos de recursos.
**E.2 — UI en ActivityPanel**
Agregar subsección "Recursos TO-BE" dentro de la sección Automatización del ActivityPanel (visible cuando `automatable = true`). Mismo patrón de asignación que los recursos AS-IS.
**E.3 — Reporte web (versión mínima)**
Sin cambios necesarios: cuando el motor usa `automatedAssignedResources`, el `resourceCostBreakdown` de `SimulationResult` ya refleja correctamente los recursos del escenario automatizado. Las filas expandibles existentes en `ActivitiesTable` muestran el breakdown del escenario que corresponde a cada tab.
**E.3 wow — Comparación lado a lado**
En el tab "Automatizado", las filas expandibles de actividades automatizables muestran AMBOS breakdowns: "Recursos AS-IS" y "Recursos TO-BE", para facilitar la comparación.
---
## Arquitectura — leer antes de implementar
### `automatedAssignedResources` sigue siendo opcional en types.ts
**No cambiar `types.ts` en esta etapa.** El campo permanece como `automatedAssignedResources?: ActivityResourceAssignment[]`. La promoción a requerido + actualización de tests va en Etapa 6 para hacer el cambio en un solo paso limpio.
Consecuencia: en todo el código nuevo usar `activity.automatedAssignedResources ?? []` para manejar el caso de actividades antiguas que podrían no tener el campo.
### Dependencias autorizadas
`ActivityPanel` ya importa de `useProcessStore` para leer `resources`. No necesita importar nuevas stores para esta feature.
---
## Cambios autorizados y arquitectura
### `src/domain/simulation.ts`
**Cambio mínimo: usar los recursos correctos según el escenario.**
Localizar el bloque de la línea ~207:
```typescript
// ACTUAL (usa siempre assignedResources):
for (const assignment of activity.assignedResources) {
```
Reemplazar por:
```typescript
// CORRECTO: seleccionar el array según el escenario
const resourceAssignments = useAutomated
? (activity.automatedAssignedResources ?? [])
: activity.assignedResources
for (const assignment of resourceAssignments) {
```
Este es el único cambio en `simulation.ts`. No tocar nada más en este archivo.
### `src/features/import/ImportPage.tsx`
Agregar `automatedAssignedResources: []` a la construcción de actividades (~línea 89):
```typescript
const activities: Activity[] = activityElements.map((el) => ({
id: uuidv4(),
processId,
bpmnElementId: el.bpmnElementId,
name: el.name,
type: el.type,
directCostFixed: 0,
executionTimeMinutes: 0,
assignedResources: [],
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
automatedAssignedResources: [], // 🆕
}))
```
**Clasificación de iniciativa:** Nivel 1 — compatibilidad preventiva, no hay cambio visible.
### `src/features/workspace/ActivityPanel.tsx`
#### 1 — Inicialización del estado local
Actualizar el `setLocalActivity` inicial para incluir `automatedAssignedResources`:
```typescript
setLocalActivity(activity ? {
...activity,
assignedResources: [...activity.assignedResources],
automatedAssignedResources: [...(activity.automatedAssignedResources ?? [])],
} : null)
```
#### 2 — Handlers de recursos automatizados
Agregar tres handlers paralelos a los de AS-IS (cerca de `addResource`, `removeResource`, `updateResourceAssignment`):
```typescript
const addAutomatedResource = (resourceId: string) => {
if (!localActivity) return
const already = (localActivity.automatedAssignedResources ?? []).some(
(r) => r.resourceId === resourceId
)
if (already) return
handleChange('automatedAssignedResources', [
...(localActivity.automatedAssignedResources ?? []),
{ resourceId, utilizationMinutes: 60, units: 1 },
])
}
const removeAutomatedResource = (resourceId: string) => {
if (!localActivity) return
handleChange(
'automatedAssignedResources',
(localActivity.automatedAssignedResources ?? []).filter((r) => r.resourceId !== resourceId)
)
}
const updateAutomatedAssignment = (
resourceId: string,
field: 'utilizationMinutes' | 'units',
value: number
) => {
if (!localActivity) return
handleChange(
'automatedAssignedResources',
(localActivity.automatedAssignedResources ?? []).map((r) =>
r.resourceId === resourceId ? { ...r, [field]: value } : r
)
)
}
```
#### 3 — Dropdown de recursos disponibles para TO-BE
```typescript
const availableAutomatedResources = resources.filter(
(r) => !(localActivity.automatedAssignedResources ?? []).some((ar) => ar.resourceId === r.id)
)
```
#### 4 — Costo total de recursos TO-BE (para mostrar en UI)
```typescript
const localAutomatedResourceCost = (localActivity.automatedAssignedResources ?? []).reduce(
(total, assignment) => {
const r = resources.find((res) => res.id === assignment.resourceId)
if (!r) return total
return total + r.costPerHour * (assignment.utilizationMinutes / 60) * assignment.units
},
0
)
```
#### 5 — UI dentro del contenedor de Automatización
**Cambio requerido previo:** el contenedor animado actual tiene `max-h-80` (320px), insuficiente para el nuevo contenido. Cambiar a `max-h-[640px]`.
Dentro del contenedor animado (después de los inputs `automatedCostFixed` y `automatedTimeMinutes`), agregar:
```tsx
{/* Separator + Recursos TO-BE */}
<Separator />
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
Recursos TO-BE
</p>
{/* Dropdown para agregar recurso */}
{availableAutomatedResources.length > 0 && (
<Select onValueChange={addAutomatedResource}>
<SelectTrigger className="h-7 text-xs w-[120px]">
<SelectValue placeholder="+ Agregar" />
</SelectTrigger>
<SelectContent>
{availableAutomatedResources.map((r) => (
<SelectItem key={r.id} value={r.id} className="text-xs">
{r.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{/* Lista de recursos asignados al escenario automatizado */}
{(localActivity.automatedAssignedResources ?? []).length === 0 ? (
<p className="text-xs text-slate-400 italic">
Sin recursos TO-BE solo costo fijo automatizado.
</p>
) : (
<div className="space-y-2">
{(localActivity.automatedAssignedResources ?? []).map((assignment) => {
const resource = resources.find((r) => r.id === assignment.resourceId)
if (!resource) return null
const cost = resource.costPerHour * (assignment.utilizationMinutes / 60) * assignment.units
return (
<div key={assignment.resourceId} className="rounded-lg border border-border bg-card p-2 space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs font-medium truncate max-w-[120px]">{resource.name}</span>
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-slate-500">
{formatCurrency(cost, currency)}
</span>
<button
type="button"
onClick={() => removeAutomatedResource(assignment.resourceId)}
className="text-slate-400 hover:text-red-500 transition-colors"
aria-label={`Quitar ${resource.name}`}
>
<X className="h-3.5 w-3.5" />
</button>
</div>
</div>
{/* Utilización y unidades */}
<div className="flex items-center gap-2">
<div className="flex-1 space-y-0.5">
<p className="text-[10px] text-slate-400">Min. por ejecución</p>
<Input
type="number"
min={0}
step={1}
value={assignment.utilizationMinutes || ''}
onChange={(e) =>
updateAutomatedAssignment(
assignment.resourceId,
'utilizationMinutes',
Math.max(0, parseFloat(e.target.value) || 0)
)
}
className="h-6 text-xs font-mono"
/>
</div>
<div className="w-16 space-y-0.5">
<p className="text-[10px] text-slate-400">Unidades</p>
<Input
type="number"
min={1}
step={1}
value={assignment.units || ''}
onChange={(e) =>
updateAutomatedAssignment(
assignment.resourceId,
'units',
Math.max(1, parseInt(e.target.value) || 1)
)
}
className="h-6 text-xs font-mono"
/>
</div>
</div>
</div>
)
})}
</div>
)}
</div>
```
**Nota:** Si `resources` (el catálogo del proceso) está vacío, el dropdown no aparece y la sección muestra solo el empty state. Esto es correcto — el usuario primero carga recursos en el panel "Recursos" y luego los asigna aquí.
#### 6 — Actualizar el helper ámbar
El helper ámbar existente muestra cuando `automatedCostFixed === 0 && automatedTimeMinutes === 0`. Ahora también debe considerar recursos:
```typescript
const showHelper = localActivity.automatable
&& localActivity.automatedCostFixed === 0
&& localActivity.automatedTimeMinutes === 0
&& (localActivity.automatedAssignedResources ?? []).length === 0
```
---
## Versión con wow — Comparación lado a lado en reporte
**Implementar si el costo extra es < 40% del mínimo.**
El objetivo: en el tab "Automatizado" del reporte, las filas expandibles de actividades automatizables muestran el desglose de recursos de AMBOS escenarios.
### `src/features/report/ActivitiesTable.tsx`
Agregar prop opcional:
```typescript
interface ActivitiesTableProps {
perActivity: ActivitySimResult[]
pairedBreakdown?: Record<string, Array<{ resourceId: string; resourceName: string; cost: number }>>
// ...props existentes sin cambio...
}
```
En la fila expandida, si `pairedBreakdown` tiene data para esta actividad Y la actividad tiene su propio breakdown, mostrar dos secciones:
```tsx
{/* En la fila expandida — cuando hay pairedBreakdown para esta actividad */}
{pairedBreakdown?.[act.activityId] && (
<>
<p className="text-[10px] font-semibold text-slate-400 uppercase tracking-wide mt-1">AS-IS</p>
{/* Lista del pairedBreakdown (recursos AS-IS) */}
{pairedBreakdown[act.activityId].map((r) => (
<div key={r.resourceId} className="flex justify-between text-xs">
<span className="text-slate-600">{r.resourceName}</span>
<span className="font-mono">{formatCurrency(r.cost, currency)}</span>
</div>
))}
<p className="text-[10px] font-semibold text-slate-400 uppercase tracking-wide mt-2">TO-BE</p>
{act.resourceCostBreakdown.length === 0
? <p className="text-xs text-slate-400 italic">Sin recursos TO-BE</p>
: act.resourceCostBreakdown.map((r) => (
<div key={r.resourceId} className="flex justify-between text-xs">
<span className="text-slate-600">{r.resourceName}</span>
<span className="font-mono">{formatCurrency(r.cost, currency)}</span>
</div>
))
}
</>
)}
```
Si no hay `pairedBreakdown`, el comportamiento existente se mantiene sin cambios.
### `src/features/report/ReportPage.tsx`
Agregar el cálculo del breakdown pareado:
```typescript
const actualBreakdownById = useMemo(() => {
if (!simulation?.result) return undefined
return Object.fromEntries(
simulation.result.perActivity.map((a) => [a.activityId, a.resourceCostBreakdown])
)
}, [simulation?.result])
```
En el `ActivitiesTable` del tab "Automatizado", pasar el prop:
```tsx
<ActivitiesTable
perActivity={resultAutomated.perActivity}
pairedBreakdown={actualBreakdownById}
{/* ...props existentes... */}
/>
```
---
## ❌ Lo que NO entra en esta etapa
- `src/domain/types.ts` — no modificar. El campo `automatedAssignedResources?` permanece opcional hasta Etapa 6.
- Tests nuevos — van en Etapa 6 junto con la promoción del campo a requerido.
- `src/store/process-store.ts` — no tocar.
- `src/store/simulation-store.ts` — no tocar.
- `src/persistence/db.ts` — no tocar (migración ya hecha).
- PDF — va en Etapa 6.
- `ActivityPanel.tsx` sección AS-IS de recursos — no modificar la sección existente, solo agregar la sección TO-BE.
- `ResourcesPanel.tsx` — no tocar.
- `GatewayPanel.tsx` — no tocar.
---
## Verificación antes de entregar
```bash
# 1. Build limpio
npm run build
# 2. Tests (todos los previos deben seguir verdes)
npm run test
# 3. Control de scope
git diff --name-only HEAD
```
El diff debe mostrar únicamente:
- `src/domain/simulation.ts`
- `src/features/workspace/ActivityPanel.tsx`
- `src/features/import/ImportPage.tsx`
- `src/features/report/ActivitiesTable.tsx` (solo si se implementó wow)
- `src/features/report/ReportPage.tsx` (solo si se implementó wow)
Si aparece `src/domain/types.ts`, `src/store/`, `src/persistence/`, o cualquier panel que no sea ActivityPanel: **revertir y reportar**.
### Verificación funcional mandatoria
Antes de reportar, verificar manualmente:
1. **Recursos AS-IS no contaminan TO-BE:** asignar recursos en AS-IS → simular → el tab Automatizado NO debe incluir esos recursos si `automatedAssignedResources` está vacío.
2. **Recursos TO-BE correctos:** asignar un recurso en TO-BE (en ActivityPanel) → simular → el tab Automatizado MUESTRA ese recurso en el breakdown de la actividad.
3. **Mixto funciona:** actividad con `automatedCostFixed = 500` + un recurso TO-BE → el costo automatizado total = 500 + costo del recurso.
4. **Actividad no automatizable:** recursos AS-IS se usan en ambos escenarios (comportamiento anterior sin cambio).
5. **Sin recursos TO-BE:** proceso sin recursos asignados en TO-BE → el motor no crashea, muestra 0 en recursos del tab Automatizado.
---
## Criterios de aceptación
- [ ] Motor de simulación usa `automatedAssignedResources` al calcular escenario automatizado.
- [ ] Actividades no automatizables: comportamiento sin cambio (siguen usando `assignedResources`).
- [ ] Sección "Recursos TO-BE" visible en ActivityPanel cuando `automatable = true`.
- [ ] Asignar recurso TO-BE → Guardar → recargar página → recurso persiste.
- [ ] Dropdown de recursos TO-BE excluye los ya asignados.
- [ ] Costo TO-BE en reporte refleja recursos TO-BE + `automatedCostFixed`.
- [ ] `ImportPage` crea actividades con `automatedAssignedResources: []`.
- [ ] Build sin errores TypeScript.
- [ ] Todos los tests previos siguen verdes.
- [ ] Validación visual del director aprobada.
---
## Reporte esperado al terminar
1. Resultado de `git diff --name-only HEAD`.
2. Resultado de `npm run build` y `npm run test`.
3. Confirmación de los 5 casos de verificación funcional.
4. Si se implementó comparación lado a lado (wow): declararlo.
5. Cualquier decisión de iniciativa (Nivel 1/2/3).
6. Si apareció algún archivo fuera del scope: declararlo con justificación.
---
## Referencias
- `sprints/sprint-3/BRIEF.md` — sección 4, Feature E y sección 3.3 (fórmula de costo automatizado)
- `src/domain/simulation.ts` — línea ~207, loop de `assignedResources`
- `src/features/workspace/ActivityPanel.tsx` — sección Automatización (~línea 352), handlers de recursos AS-IS como referencia de patrón
- `src/features/report/ActivitiesTable.tsx` — prop interface y filas expandibles
- `src/features/report/ReportPage.tsx` — tabs actual/automatizado, uso de `simulation.result` y `simulation.resultAutomated`
- `methodology/ANTI_PATTERNS.md` — AP.8 (adelanto silencioso), AP.9 (restricciones contradictorias)
- `methodology/PATTERNS.md` — C.7 (git diff como control preventivo)