# 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 1–4 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 */}

Recursos TO-BE

{/* Dropdown para agregar recurso */} {availableAutomatedResources.length > 0 && ( )}
{/* Lista de recursos asignados al escenario automatizado */} {(localActivity.automatedAssignedResources ?? []).length === 0 ? (

Sin recursos TO-BE — solo costo fijo automatizado.

) : (
{(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 (
{resource.name}
{formatCurrency(cost, currency)}
{/* Utilización y unidades */}

Min. por ejecución

updateAutomatedAssignment( assignment.resourceId, 'utilizationMinutes', Math.max(0, parseFloat(e.target.value) || 0) ) } className="h-6 text-xs font-mono" />

Unidades

updateAutomatedAssignment( assignment.resourceId, 'units', Math.max(1, parseInt(e.target.value) || 1) ) } className="h-6 text-xs font-mono" />
) })}
)}
``` **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> // ...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] && ( <>

AS-IS

{/* Lista del pairedBreakdown (recursos AS-IS) */} {pairedBreakdown[act.activityId].map((r) => (
{r.resourceName} {formatCurrency(r.cost, currency)}
))}

TO-BE

{act.resourceCostBreakdown.length === 0 ?

Sin recursos TO-BE

: act.resourceCostBreakdown.map((r) => (
{r.resourceName} {formatCurrency(r.cost, currency)}
)) } )} ``` 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 ``` --- ## ❌ 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)