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:
445
sprints/sprint-3/ETAPA_5_PROMPT.md
Normal file
445
sprints/sprint-3/ETAPA_5_PROMPT.md
Normal 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 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)
|
||||
Reference in New Issue
Block a user