# 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 */}
<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)
