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>
This commit is contained in:
2026-05-27 11:49:43 -03:00
parent d2746f07d1
commit 67e259e6ed
57 changed files with 4372 additions and 82 deletions

View File

@@ -0,0 +1,445 @@
# 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)