Sprint 2 Etapas 1-2: modelo recursos utilizationMinutes+units, Dexie v3, ActivityPanel wow

This commit is contained in:
2026-05-19 21:26:34 -03:00
parent 49364f4b18
commit a01380240a
23 changed files with 292265 additions and 244 deletions

311
sprints/sprint-2/BRIEF.md Normal file
View File

@@ -0,0 +1,311 @@
# Sprint 2 — Brief
> **Propósito:** completar y elevar el modelo de recursos con costo por hora (ya parcialmente implementado), con foco en la experiencia "wow" en el workspace y en la visibilidad de recursos en el reporte y el PDF. Complementado con mejoras de UX para uso en talleres y pre-venta.
>
> **Usuario objetivo:** equipo de InQuality. Uso interno en talleres de análisis de procesos y pre-venta de automatización. Ocasionalmente presentado a clientes directamente.
>
> **Sprint anterior:** Sprint 1.5 — identidad visual completa, PDFs de clase profesional, badge ⚡ en canvas, ROI con 5 KPIs. Ver cierre en `methodology/CASE_STUDIES/`.
---
## 0. Reglas no negociables (heredadas)
- **Marca:** "InQ" siempre I-n-Q. "InQ ROI" nombre del producto.
- **Paleta:** naranja `#F59845`, gradiente `#F59845 → #ED3E8F`. No usar morado CONCILIA.
- **Logo:** usar PNGs de `assets/` en lugar de texto "InQ" / "InQuality" donde corresponda. Logo principal: `MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png`.
- **Footer PDFs:** "Powered by InQuality" — obligatorio en los tres PDFs.
- **Identidad visual:** estable hasta Sprint 4+. No reabrir paleta, tipografía ni sistema de componentes.
- **Validación visual:** tests verdes ≠ aprobación final. Todo artefacto visual requiere OK del director antes de cerrar etapa.
- **Iniciativa agéntica:** clasificar cambios por nivel (1/2/3). Ver `methodology/PATTERNS.md` C.6.
Para referencia completa del manual de marca, tokens CSS y logo, ver `assets/InQ manual de marca- 3.0.pdf` y `sprints/sprint-1-5/BRIEF.md` sección 1.
---
## 1. Contexto estratégico
### 1.1 Rebalanceo web/PDF
| Componente | Inversión esperada |
|---|---|
| Web app (workspace + reporte + features) | 6070 % |
| PDF (refinamientos, nuevas secciones) | 1525 % |
| Infraestructura / deuda técnica | 1015 % |
| Documentación / metodología | 510 % |
### 1.2 Criterio de wow (obligatorio)
Cada etapa con feature nueva incluye:
- **Versión mínima:** cumple la funcionalidad.
- **Versión con wow:** mismo objetivo + 1-2 detalles de experiencia que generan impacto positivo.
Por defecto: versión con wow si el costo extra < 40% del tiempo de la mínima.
---
## 2. Estado real del modelo de recursos (pre-Sprint 2)
**El modelo de recursos existe parcialmente desde el MVP.** Sprint 2 no construye desde cero.
### Lo que ya existe en el código
| Componente | Estado |
|---|---|
| `Resource` en `types.ts` | ✅ `{ id, processId, name, type, costPerHour }` |
| `ActivityResourceAssignment` en `Activity` | ✅ `{ resourceId, utilizationPercent: number (01) }` |
| Tabla `resources` en Dexie | ✅ Existe desde v1 |
| `ResourcesPanel.tsx` | ✅ CRUD completo de recursos por proceso |
| `ActivityPanel.tsx` asignación | ✅ Slider de % de utilización |
| Motor de simulación | ✅ Calcula `cost = costPerHour × (execTimeMin/60) × utilizationPercent` |
| `CostByResourceChart.tsx` en reporte | ✅ Muestra totales por recurso (o "Sin recursos") |
| Desglose por actividad en reporte | ❌ No existe |
| PDF con recursos | ❌ No existe |
### Problema del modelo actual
El modelo `utilizationPercent` no es intuitivo para consultores: "¿qué % de la actividad ocupa este recurso?" es menos claro que "¿cuántos minutos dedica este recurso a cada ejecución?". En un taller con cliente, la pregunta inevitable es "¿cómo llegaste a ese costo?" — la respuesta con minutos es verificable, con porcentajes no.
---
## 3. Decisiones arquitectónicas del sprint (validadas por el director)
| Decisión | Resolución |
|---|---|
| Scope de recursos | Por proceso. Catálogo global en Sprint 3 con feedback real. |
| Costo fijo convive con recursos | Sí. `costoTotal = directCostFixed + costoRecursos` |
| Recursos en escenario automatizado | Solo AS-IS en Sprint 2. TO-BE sigue con `automatedCostFixed`. |
| Modelo de utilización | Migrar `utilizationPercent``utilizationMinutes + units` |
| Versión Dexie | Incrementar a v3. Migración preserva datos existentes. |
### 3.1 Modelo migrado — `ActivityResourceAssignment`
```typescript
// Antes (v2)
interface ActivityResourceAssignment {
resourceId: UUID
utilizationPercent: number // 01, fracción del tiempo de la actividad
}
// Después (v3) — Sprint 2
interface ActivityResourceAssignment {
resourceId: UUID
utilizationMinutes: number // minutos del recurso por ejecución (independiente del tiempo de la actividad)
units: number // cantidad de unidades del recurso (default 1)
}
```
### 3.2 Fórmula de costo actualizada
```
costoRecurso = resource.costPerHour × assignment.utilizationMinutes / 60 × assignment.units
costoTotalActividad = directCostFixed + SUM(costoRecurso por cada assignment)
```
### 3.3 Migración v2 → v3
La migración convierte datos existentes con la heurística:
```
utilizationMinutes = Math.round(activity.executionTimeMinutes × assignment.utilizationPercent)
units = 1
```
Para asignaciones sin ejecutionTimeMinutes disponible, `utilizationMinutes = 0` (conservador, evita sobrestimación).
### 3.4 Lo que NO cambia en Sprint 2
- `Resource` type (solo `utilizationMinutes` cambia en `ActivityResourceAssignment`, no en `Resource`)
- Motor de simulación: recibe costo ya calculado o adapta la fórmula localmente
- Escenario automatizado: sin cambios
- ROI: opera sobre resultado de simulación, sin cambio
---
## 4. Features del sprint
### Feature A — Migración del modelo de recursos (Etapa 1)
**Peso estimado:** 15% del sprint.
- Actualizar `ActivityResourceAssignment` en `types.ts` y `schemas.ts`
- Actualizar fórmula en `simulation.ts`
- Dexie v3 con función de upgrade que migra `utilizationPercent → utilizationMinutes + units`
- Test de migración: procesos con asignaciones previas se convierten correctamente
- Tests unitarios de dominio: edge cases del nuevo modelo
**Criterio de aceptación:**
- [ ] Proceso sin recursos: comportamiento idéntico al actual
- [ ] Proceso con recursos: costo calculado correctamente con nueva fórmula
- [ ] Migración v2→v3: datos preservados, sin pérdida
- [ ] Tests de dominio verdes
---
### Feature B — ActivityPanel "wow" (Etapa 2)
**Peso estimado:** 20% del sprint.
**Versión mínima:**
- Reemplazar slider de % por inputs de `utilizationMinutes` (número) y `units` (número entero ≥ 1)
- Mostrar costo calculado por recurso debajo del input
**Versión con wow (recomendada):**
Todo lo anterior más:
- Cálculo en tiempo real: el costo se actualiza al tipear (debounce de 300ms)
- Subtotal de recursos visible en el panel antes de guardar
- Helper contextual bajo el total: _"Esta actividad consume el equivalente a **2.3 días** de trabajo del Analista Senior al mes"_ (calculado con `annualFrequency / 12 × utilizationMinutes / 60 / 8`)
- El campo `directCostFixed` con label explícito: "Costo fijo (insumos, licencias, etc.)" para diferenciar visualmente de los recursos
**Criterio de aceptación:**
- [ ] Inputs reemplazan slider de %
- [ ] Costo por recurso visible antes de guardar
- [ ] Helper contextual aparece cuando hay `annualFrequency > 0` y recursos asignados
- [ ] Validación visual del director aprobada
---
### Feature C — Reporte web: desglose de recursos (Etapa 3)
**Peso estimado:** 20% del sprint.
#### C.1 Desglose por actividad en ActivitiesTable
**Versión mínima:** columna "Recursos" en la tabla que muestra el costo total de recursos de la actividad.
**Versión con wow:** filas expandibles que muestran el desglose: fijo + recurso 1 + recurso 2 + subtotal. El expand/collapse usa animación consistente con el sistema de diseño.
#### C.2 CumulativeSavingsChart — arrancar en `-inversión` (TECH_DEBT)
El gráfico web parte de `(Año 0, 0)`. El PDF ya lo hace correcto (`(Año 0, -inversión)`). Inconsistencia que notarán analistas en pre-venta. Cambiar punto inicial en `CumulativeSavingsChart.tsx`.
#### C.3 Payback label superpuesto (TECH_DEBT — trivial)
Ajustar offset del label "Payback · X meses" para que no colisione con eje Y. ~15 min.
#### C.4 Badge ⚡ — evaluar tamaño (TECH_DEBT)
Evaluar 16×16px o 18×18px. Si no genera colisiones en nodos chicos: implementar y reportar. Si genera problemas: mantener 14×14px y documentar.
**Criterio de aceptación:**
- [ ] Tabla muestra desglose de recursos por actividad
- [ ] CumulativeSavingsChart arranca en `-automationInvestment`
- [ ] Payback label no colisiona con eje Y
- [ ] Validación visual del director aprobada
---
### Feature D — UX rápidos (Etapa 4)
**Peso estimado:** 10% del sprint.
#### D.1 Edición inline de nombre de proceso y cliente
**Problema:** requiere ir a configuración global. En un taller interrumpe el flujo.
**Versión mínima:** campo editable inline en el header del workspace (click para editar, Enter/blur para guardar).
**Versión con wow:** animación sutil texto → input → texto, guardado con checkmark naranja breve que desaparece en 1.5s.
**Criterio de aceptación:**
- [ ] Se puede editar nombre y cliente sin ir a configuración
- [ ] Los cambios persisten en IndexedDB
- [ ] Validación visual del director aprobada
---
### Feature E — PDF: recursos + verificación de truncamiento (Etapa 5)
**Peso estimado:** 15% del sprint.
#### E.1 Tabla de recursos en PDF (procesos Actual/Automatizado)
Sección condicional al final del PDF (solo si hay actividades con recursos asignados):
- Tabla: Actividad | Recurso | Tipo | Costo/hora | Minutos | Unidades | Costo
- Subtotal por actividad
- Si ninguna actividad tiene recursos: la sección no aparece
**Versión mínima:** tabla simple.
**Versión con wow:** chip de color por tipo de recurso (rol = naranja, sistema = azul slate, persona = verde, equipamiento = gris, insumo = amarillo).
#### E.2 Verificación de truncamiento (TECH_DEBT)
Test E2E que valide ancho de cada línea en los tres PDFs. Si pasa → cerrar ítem. Si hay truncamiento → aplicar `doc.splitTextToSize()`.
**Criterio de aceptación:**
- [ ] Sección de recursos aparece cuando hay datos, no aparece cuando no hay
- [ ] Test E2E confirma ausencia de truncamiento en los tres PDFs
- [ ] Validación visual del director aprobada
---
## 5. Estructura de etapas
| Etapa | Contenido | Peso |
|---|---|---|
| 1 | Migración modelo: `utilizationPercent``utilizationMinutes + units`. Dexie v3. Tests unitarios. | 15% |
| 2 | ActivityPanel "wow": inputs minutos/unidades, cálculo tiempo real, helper contextual | 20% |
| 3 | Reporte web: desglose recursos por actividad (expandible), CumulativeSavingsChart -inversión, payback fix, badge | 20% |
| 4 | UX rápidos: edición inline proceso/cliente | 10% |
| 5 | PDF: tabla recursos + verificación truncamiento | 15% |
| 6 | E2E tests completos + validación visual director | 10% |
| *(buffer)* | *(imprevistos o profundizar wow)* | 10% |
**Regla operativa:** ninguna etapa arranca sin su `ETAPA_X_PROMPT.md` aprobado por el director.
---
## 6. Lo que explícitamente NO entra en Sprint 2
- Catálogo global de recursos compartido entre procesos → Sprint 3
- Recursos en escenario automatizado (TO-BE) → Sprint 3 con Modelo B
- Múltiples simulaciones / historial de escenarios → Sprint 3
- Distribuciones de probabilidad → Fase 2
- Export Excel → Backlog sin driver urgente
- Modo oscuro, i18n, PWA → Fase 2
- Login, multi-tenant, colaboración → Fase 2
---
## 7. Deuda técnica que se cierra en este sprint
| Ítem en TECH_DEBT.md | Etapa |
|---|---|
| CumulativeSavingsChart arranca en `-inversión` (web) | 3 |
| Payback label superpuesto con eje Y | 3 |
| Badge ⚡ tamaño (evaluar 16-18px) | 3 |
| Verificación truncamiento notas metodológicas PDF | 5 |
---
## 8. Definition of Done — Sprint 2
- [ ] Features AE: todos los criterios de aceptación cumplen
- [ ] Migración Dexie v2→v3: test automatizado confirma preservación de datos
- [ ] Tests unitarios: fórmula nueva, edge cases (sin recursos, units > 1, solo fijo, mixto)
- [ ] E2E: flujo completo asignar recurso → simular → verificar costo en reporte → exportar PDF
- [ ] Validación visual: reporte web con desglose, PDF con tabla de recursos, chart con -inversión
- [ ] TECH_DEBT.md actualizado: ítems cerrados documentados
- [ ] OBSERVATIONS.md limpio al cerrar
- [ ] STRATEGIC_DIRECTION.md actualizado con reflexiones post-sprint
- [ ] Ratio web/PDF respetado (verificar con conteo de etapas)
---
## 9. Riesgos
| Riesgo | Probabilidad | Mitigación |
|---|---|---|
| La migración de `utilizationPercent``utilizationMinutes` produce valores incorrectos en procesos existentes | Media | Test de migración obligatorio antes de cualquier otra etapa. La heurística es conservadora (redondeo hacia abajo). |
| El helper contextual en ActivityPanel requiere acceso a `annualFrequency` del proceso | Baja | `useReportData` o contexto del workspace ya tiene el proceso. Verificar en Etapa 2. |
| La tabla de recursos en PDF excede el espacio disponible en página 4 | Media | Diseñar como página separada si supera el espacio. Sección completamente condicional. |
---
## 10. Referencias
- `CLAUDE.md` (simulador-web) — reglas de implementación
- `STRATEGIC_DIRECTION.md` — decisiones estratégicas post-Sprint 1.5
- `BACKLOG.md` — ítems "Recursos", "Edición inline"
- `TECH_DEBT.md` — ítems CumulativeSavingsChart, Payback, Badge, Truncamiento
- `methodology/PATTERNS.md` — C.6, C.1
- `methodology/ANTI_PATTERNS.md` — AP.8
- `sprints/sprint-1-5/BRIEF.md` — manual de marca completo y tokens CSS
- `assets/InQ manual de marca- 3.0.pdf` — manual de marca oficial

View File

@@ -0,0 +1,259 @@
# Sprint 2 — Etapa 1: Migración del modelo de recursos
> **Tipo:** dominio puro + persistencia. Sin UI. Sin cambios visuales.
> **Objetivo:** migrar `ActivityResourceAssignment.utilizationPercent` → `utilizationMinutes + units`, actualizar la fórmula de simulación, incrementar Dexie a v3, y garantizar que los datos existentes se preservan correctamente.
> **Resultado esperado:** tests verdes + reporte de archivos modificados. La UI en Etapas 23.
---
## Contexto imprescindible
### Estado actual del modelo (lo que vas a encontrar en el código)
```typescript
// src/domain/types.ts — HOY
interface ActivityResourceAssignment {
resourceId: UUID
utilizationPercent: number // 01, fracción del tiempo de la actividad
}
// Fórmula actual en src/domain/simulation.ts
const hoursUsed = (executionTimeMinutes / 60) * assignment.utilizationPercent
const cost = resource.costPerHour * hoursUsed * execProb
```
```typescript
// src/domain/schemas.ts — HOY
export const ActivityResourceAssignmentSchema = z.object({
resourceId: z.string().uuid(),
utilizationPercent: z.number().min(0).max(1),
})
```
```typescript
// src/persistence/db.ts — HOY
// Dexie versión 2 (v1 original + v2 de Sprint 1)
// La tabla 'resources' ya existe desde v1.
// ActivityResourceAssignment está embebido en Activity (no es tabla propia).
```
### Lo que NO existe todavía (no confundir con lo anterior)
- No existe tabla `resourceAssignments` separada. Los assignments están embebidos en el array `Activity.assignedResources`.
- No existe campo `utilizationMinutes`. Solo existe `utilizationPercent`.
- No existe campo `units`. Hay que agregarlo.
---
## Cambios requeridos
### 1. `src/domain/types.ts`
Reemplazar `ActivityResourceAssignment`:
```typescript
// ANTES
export interface ActivityResourceAssignment {
resourceId: UUID
utilizationPercent: number
}
// DESPUÉS
export interface ActivityResourceAssignment {
resourceId: UUID
utilizationMinutes: number // minutos que este recurso dedica por ejecución (independiente del tiempo de la actividad)
units: number // cantidad de unidades del recurso (ej: 2 impresoras). Default: 1.
}
```
El tipo `Resource` NO cambia. Solo cambia `ActivityResourceAssignment`.
### 2. `src/domain/schemas.ts`
Reemplazar `ActivityResourceAssignmentSchema`:
```typescript
// ANTES
export const ActivityResourceAssignmentSchema = z.object({
resourceId: z.string().uuid(),
utilizationPercent: z.number().min(0).max(1),
})
// DESPUÉS
export const ActivityResourceAssignmentSchema = z.object({
resourceId: z.string().uuid(),
utilizationMinutes: z.number().min(0, 'Los minutos no pueden ser negativos'),
units: z.number().int().min(1, 'Las unidades deben ser al menos 1').default(1),
})
```
### 3. `src/domain/simulation.ts`
Actualizar la fórmula de costo de recursos. Buscar el bloque que calcula `hoursUsed` y reemplazarlo:
```typescript
// ANTES
const hoursUsed = (executionTimeMinutes / 60) * assignment.utilizationPercent
const cost = resource.costPerHour * hoursUsed * execProb
// DESPUÉS
const hoursUsed = (assignment.utilizationMinutes / 60) * assignment.units
const cost = resource.costPerHour * hoursUsed * execProb
```
El `execProb` se mantiene igual — sigue siendo la probabilidad de ejecución del nodo en el grafo BPMN. El costo esperado del recurso por ejecución del proceso es `costPerHour × utilizationMinutes/60 × units × execProb`.
También actualizar la línea de acumulación de minutos por recurso (buscar `resourceTotals`):
```typescript
// ANTES
minutes: prev.minutes + activity.executionTimeMinutes * assignment.utilizationPercent * execProb,
// DESPUÉS
minutes: prev.minutes + assignment.utilizationMinutes * assignment.units * execProb,
```
### 4. `src/persistence/db.ts`
Agregar versión 3 con función de upgrade. La migración convierte los assignments embebidos en Activity:
```typescript
this.version(3)
.stores({
// Los stores no cambian — ActivityResourceAssignment está embebido en Activity, no es tabla propia
processes: 'id, name, updatedAt',
activities: 'id, processId, bpmnElementId',
gateways: 'id, processId, bpmnElementId',
resources: 'id, processId, name',
simulations: 'id, processId, executedAt',
})
.upgrade(async (tx) => {
// Migrar assignedResources de cada actividad: utilizationPercent → utilizationMinutes + units
await tx.table('activities').toCollection().modify((act) => {
if (!Array.isArray(act.assignedResources)) {
act.assignedResources = []
return
}
act.assignedResources = act.assignedResources.map((assignment: Record<string, unknown>) => {
if ('utilizationPercent' in assignment) {
// Heurística de conversión: minutos = round(executionTimeMinutes × utilizationPercent)
// Si executionTimeMinutes no está disponible o es 0, usar 0 (conservador)
const execMinutes = typeof act.executionTimeMinutes === 'number' ? act.executionTimeMinutes : 0
const utilizationMinutes = Math.round(execMinutes * (assignment.utilizationPercent as number))
return {
resourceId: assignment.resourceId,
utilizationMinutes,
units: 1,
}
}
// Ya está en el nuevo formato (por si se corre la migración dos veces)
if ('utilizationMinutes' in assignment) {
return {
resourceId: assignment.resourceId,
utilizationMinutes: assignment.utilizationMinutes ?? 0,
units: assignment.units ?? 1,
}
}
// Fallback defensivo
return { resourceId: assignment.resourceId, utilizationMinutes: 0, units: 1 }
})
})
})
```
**Importante:** no eliminar las versiones 1 y 2 anteriores. Dexie requiere el historial completo de versiones.
### 5. Puntos de inicialización en `src/features/import/ImportPage.tsx`
Buscar donde se inicializa una nueva actividad (al importar BPMN). Hay una línea similar a:
```typescript
assignedResources: [],
```
Esto está bien — un array vacío es válido con el nuevo modelo. No requiere cambio, pero verificar que no haya ningún lugar que inicialice con `utilizationPercent`.
**Grep de verificación:** buscar en todo `src/` cualquier referencia a `utilizationPercent`. Al terminar la etapa, esta búsqueda debe devolver cero resultados.
---
## Tests requeridos
### Tests unitarios — `tests/domain/simulation.test.ts`
Agregar los siguientes casos al archivo existente (no borrar los tests actuales):
```
describe('recursos con modelo utilizationMinutes + units', () => {
test('actividad sin recursos: costo = directCostFixed')
test('actividad con 1 recurso: costo = fijo + costPerHour × utilizationMinutes/60 × units × execProb')
test('actividad con 2 recursos: costo = fijo + suma de costos de recursos')
test('units = 2: costo de recurso se duplica')
test('utilizationMinutes = 0: el recurso no agrega costo')
test('resource no encontrado en el mapa: se ignora sin lanzar error')
})
```
### Test de migración — `tests/integration/` (crear archivo nuevo `db-migration-v3.test.ts`)
```
test('migración v2→v3: actividad con utilizationPercent se convierte a utilizationMinutes correctamente')
test('migración v2→v3: actividad sin assignedResources mantiene array vacío')
test('migración v2→v3: actividad con assignedResources vacío no falla')
test('migración v2→v3: el costo calculado después de la migración es aproximadamente igual al anterior')
// El costo debe ser igual ± redondeo de Math.round()
```
---
## Verificaciones finales obligatorias
Antes de reportar la etapa como completa:
1. **Grep de limpieza:** `grep -r "utilizationPercent" src/` — debe devolver 0 resultados.
2. **Build limpio:** `npm run build` sin errores de TypeScript.
3. **Tests:** `npm run test` — todos los tests (existentes + nuevos) deben pasar.
4. **Smoke test manual:** cargar la app en dev (`npm run dev`), abrir un proceso existente con recursos, verificar que no crashea y que el workspace carga.
---
## Alcance estricto — lo que NO entra en esta etapa
- ❌ Cambios en `ActivityPanel.tsx` (UI del panel de actividad) — Etapa 2
- ❌ Cambios en `ResourcesPanel.tsx` — Etapa 2 si necesita ajustes
- ❌ Cambios en `ActivitiesTable.tsx` ni en el reporte — Etapa 3
- ❌ Cambios en PDFs — Etapa 5
- ❌ Edición inline de proceso/cliente — Etapa 4
- ❌ CumulativeSavingsChart, payback label, badge — Etapa 3
Si al hacer el grep de limpieza encontrás referencias a `utilizationPercent` en componentes UI, **no las corrijas en esta etapa**. Documentalas en el reporte para que Etapa 2 las tome.
---
## Reporte esperado al terminar
```
## Etapa 1 completada
### Archivos modificados
- src/domain/types.ts — ActivityResourceAssignment: utilizationPercent → utilizationMinutes + units
- src/domain/schemas.ts — ActivityResourceAssignmentSchema actualizado
- src/domain/simulation.ts — fórmula de costo de recursos actualizada
- src/persistence/db.ts — versión 3 con migración
### Archivos creados
- tests/integration/db-migration-v3.test.ts
### Tests
- X tests unitarios nuevos (todos verdes)
- X tests de migración (todos verdes)
- X tests previos (todos siguen verdes)
### Grep de limpieza
- "utilizationPercent" en src/: 0 resultados ✅
(o lista de archivos UI pendientes para Etapa 2)
### Build
- npm run build: ✅ sin errores TypeScript
### Notas
[cualquier hallazgo relevante para Etapas siguientes]
```

View File

@@ -0,0 +1,367 @@
# Sprint 2 — Etapa 2: ActivityPanel "wow"
> **Tipo:** UI — un solo componente. Sin cambios en dominio, persistencia ni reporte.
> **Objetivo:** reescribir la sección de recursos de `ActivityPanel.tsx`: reemplazar el slider roto por inputs numéricos, agregar campo de unidades, mostrar cálculo de costo en tiempo real y helper contextual.
> **Resultado esperado:** panel funcional y visualmente consistente con el sistema de diseño InQ ROI. Validación visual del director requerida antes del OK formal.
---
## Estado de partida — lo que hay que corregir
El archivo `src/features/workspace/ActivityPanel.tsx` quedó en estado intermedio después de Etapa 1. El bug está en líneas 193199:
```tsx
// ROTO — slider con divisor /100 del modelo anterior
<Slider
value={[assignment.utilizationMinutes]}
min={0}
max={100}
step={5}
onValueChange={([v]) => setUtilization(assignment.resourceId, v / 100)} // ← bug
/>
```
El divisor `/100` hace que cualquier posición del slider (0100) se divida antes de `Math.round()`, produciendo solo 0 o 1 minuto. El slider es inusable.
**Esta etapa reemplaza ese bloque completamente** con inputs numéricos. El componente conserva toda la lógica no-rota (guardar, cambios, automatización, etc.) — solo se rehace la sección de recursos asignados.
---
## Archivo a modificar
**Un único archivo:** `src/features/workspace/ActivityPanel.tsx`
Si durante la implementación identificás que otro archivo necesita cambio, **detenete y reportá el conflicto antes de tocarlo**. No procedas sin confirmación.
---
## Cambios requeridos en detalle
### 1. Eliminar `Slider` del import y del componente
Remover:
```tsx
import { Slider } from '@/components/ui/slider'
```
Asegurarse de que no quede ningún uso de `<Slider` en el archivo.
### 2. Agregar `currentProcess` al destructuring del store
```tsx
// ANTES
const { activities, resources, updateActivity } = useProcessStore()
// DESPUÉS
const { activities, resources, updateActivity, currentProcess } = useProcessStore()
```
`currentProcess` ya existe en el store (`src/store/process-store.ts` línea 6). No requiere ningún cambio en el store.
### 3. Actualizar la función `setUtilization` — renombrar y agregar `setUnits`
Reemplazar la función `setUtilization` actual por dos funciones separadas y claras:
```tsx
function setUtilizationMinutes(resourceId: string, minutes: number) {
if (!localActivity) return
handleChange(
'assignedResources',
localActivity.assignedResources.map((r) =>
r.resourceId === resourceId
? { ...r, utilizationMinutes: Math.max(0, Math.round(minutes)) }
: r
)
)
}
function setUnits(resourceId: string, units: number) {
if (!localActivity) return
handleChange(
'assignedResources',
localActivity.assignedResources.map((r) =>
r.resourceId === resourceId
? { ...r, units: Math.max(1, Math.round(units)) }
: r
)
)
}
```
### 4. Calcular el costo total de la actividad (en tiempo real, sin guardar)
Agregar estas constantes calculadas antes del `return`, usando los datos locales del estado:
```tsx
// Costo de recursos calculado en tiempo real (misma fórmula que simulation.ts)
const localResourceCost = localActivity.assignedResources.reduce((total, assignment) => {
const resource = resources.find((r) => r.id === assignment.resourceId)
if (!resource) return total
return total + (resource.costPerHour * assignment.utilizationMinutes / 60 * assignment.units)
}, 0)
const localTotalCost = localActivity.directCostFixed + localResourceCost
```
### 5. Reemplazar el bloque de recursos asignados completo
Reemplazar desde `{/* Recursos asignados */}` hasta el cierre del bloque `{resources.length === 0 && ...}` (líneas 159228) con:
```tsx
{/* Recursos asignados */}
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium block">Recursos asignados</Label>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-slate-400 cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-52 text-xs">
Cada recurso aporta costo según: costo/hora × minutos por ejecución × unidades
</TooltipContent>
</Tooltip>
</div>
{localActivity.assignedResources.length === 0 ? (
<p className="text-xs text-slate-400 py-1">Sin recursos asignados</p>
) : (
<div className="space-y-2">
{localActivity.assignedResources.map((assignment) => {
const resource = resources.find((r) => r.id === assignment.resourceId)
if (!resource) return null
// Costo de este recurso en tiempo real
const resourceCost = resource.costPerHour * assignment.utilizationMinutes / 60 * assignment.units
return (
<div key={assignment.resourceId} className="bg-slate-50 rounded-lg p-3 space-y-2.5">
{/* Header del recurso */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium text-slate-700">{resource.name}</p>
<p className="text-xs text-slate-400 font-mono">{formatCurrency(resource.costPerHour)}/h</p>
</div>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-slate-400 hover:text-destructive"
onClick={() => removeResource(assignment.resourceId)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
{/* Inputs: minutos y unidades */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs text-slate-500">Minutos / ejecución</Label>
<Input
type="number"
min={0}
step={1}
value={assignment.utilizationMinutes === 0 ? '' : assignment.utilizationMinutes}
onChange={(e) => setUtilizationMinutes(assignment.resourceId, parseFloat(e.target.value) || 0)}
className="font-mono h-7 text-xs"
placeholder="0"
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-slate-500">Unidades</Label>
<Input
type="number"
min={1}
step={1}
value={assignment.units}
onChange={(e) => setUnits(assignment.resourceId, parseFloat(e.target.value) || 1)}
className="font-mono h-7 text-xs"
/>
</div>
</div>
{/* Costo calculado en tiempo real */}
<div className="flex items-center justify-between pt-0.5">
<span className="text-xs text-slate-400">Costo por ejecución</span>
<span className="text-xs font-mono font-semibold text-slate-700">
{formatCurrency(resourceCost)}
</span>
</div>
</div>
)
})}
</div>
)}
{/* Selector para agregar recurso */}
{availableResources.length > 0 && (
<Select onValueChange={addResource}>
<SelectTrigger className="h-8 text-xs">
<div className="flex items-center gap-1.5 text-slate-500">
<PlusCircle className="h-3.5 w-3.5" />
<SelectValue placeholder="Agregar recurso…" />
</div>
</SelectTrigger>
<SelectContent>
{availableResources.map((r) => (
<SelectItem key={r.id} value={r.id} className="text-xs">
{r.name} {formatCurrency(r.costPerHour)}/h
</SelectItem>
))}
</SelectContent>
</Select>
)}
{resources.length === 0 && (
<p className="text-xs text-slate-400">
Creá recursos en la tab &quot;Recursos&quot; para asignarlos
</p>
)}
</div>
{/* ── Resumen de costos en tiempo real ─────────────────────────────── */}
{(localActivity.assignedResources.length > 0 || localActivity.directCostFixed > 0) && (
<div className="rounded-lg border border-slate-200 bg-white p-3 space-y-1.5">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Costo por ejecución</p>
{localActivity.directCostFixed > 0 && (
<div className="flex justify-between">
<span className="text-xs text-slate-500">Fijo (insumos, licencias)</span>
<span className="text-xs font-mono text-slate-600">{formatCurrency(localActivity.directCostFixed)}</span>
</div>
)}
{localActivity.assignedResources.map((assignment) => {
const resource = resources.find((r) => r.id === assignment.resourceId)
if (!resource) return null
const cost = resource.costPerHour * assignment.utilizationMinutes / 60 * assignment.units
if (cost === 0) return null
return (
<div key={assignment.resourceId} className="flex justify-between">
<span className="text-xs text-slate-500">{resource.name}</span>
<span className="text-xs font-mono text-slate-600">{formatCurrency(cost)}</span>
</div>
)
})}
<div className="flex justify-between border-t border-slate-100 pt-1.5 mt-1">
<span className="text-xs font-semibold text-slate-700">Total</span>
<span className="text-xs font-mono font-bold text-[#F59845]">{formatCurrency(localTotalCost)}</span>
</div>
</div>
)}
{/* ── Helper contextual ────────────────────────────────────────────── */}
{(() => {
const annualFrequency = currentProcess?.annualFrequency ?? 0
if (annualFrequency === 0 || localActivity.assignedResources.length === 0) return null
// Recurso con mayor costo como referencia del helper
const topAssignment = [...localActivity.assignedResources]
.map((a) => {
const r = resources.find((res) => res.id === a.resourceId)
return r ? { name: r.name, cost: r.costPerHour * a.utilizationMinutes / 60 * a.units, utilizationMinutes: a.utilizationMinutes } : null
})
.filter(Boolean)
.sort((a, b) => (b?.cost ?? 0) - (a?.cost ?? 0))[0]
if (!topAssignment || topAssignment.utilizationMinutes === 0) return null
const execsPerMonth = annualFrequency / 12
const minutesPerMonth = execsPerMonth * topAssignment.utilizationMinutes
const daysPerMonth = minutesPerMonth / 60 / 8
if (daysPerMonth < 0.1) return null
return (
<div className="flex items-start gap-2 rounded-lg bg-orange-50 border border-orange-100 px-3 py-2">
<Info className="h-3.5 w-3.5 text-[#F59845] shrink-0 mt-0.5" />
<p className="text-xs text-orange-700 leading-snug">
A {Math.round(execsPerMonth)} ejecuciones/mes, <strong>{topAssignment.name}</strong> dedica{' '}
<strong>{daysPerMonth < 1 ? `${Math.round(daysPerMonth * 8)}h` : `${daysPerMonth.toFixed(1)} días`}</strong>{' '}
al mes a esta actividad.
</p>
</div>
)
})()}
```
---
## Verificaciones obligatorias antes de reportar
### 1. Grep de limpieza — sin Slider
```bash
grep -n "Slider\|utilizationPercent" src/features/workspace/ActivityPanel.tsx
```
Debe devolver 0 resultados.
### 2. Verificación de scope con git diff — OBLIGATORIO
```bash
git diff --name-only HEAD
```
**El resultado debe contener únicamente:**
```
src/features/workspace/ActivityPanel.tsx
```
Si aparece cualquier otro archivo:
1. Revertilo: `git checkout HEAD -- <archivo>`
2. Listarlo bajo "Conflictos de scope" en el reporte
3. No incluirlo en el entregable de esta etapa
### 3. Build limpio
```bash
npm run build
```
Sin errores TypeScript.
### 4. Tests
```bash
npm run test
```
Todos los tests existentes deben seguir verdes. Esta etapa no agrega tests nuevos (es UI pura).
### 5. Smoke test manual
Abrir `npm run dev`, cargar un proceso, asignar un recurso a una actividad y verificar:
- [ ] Los inputs de minutos y unidades son editables
- [ ] El costo por ejecución se actualiza al tipear
- [ ] El resumen total aparece cuando hay datos
- [ ] El helper naranja aparece si `annualFrequency > 0` y hay recursos con minutos > 0
- [ ] El botón Guardar se activa al cambiar valores
- [ ] La sección de Automatización sigue funcionando igual
---
## Alcance estricto — lo que NO entra en esta etapa
-`ResourcesPanel.tsx` — no requiere cambios, CRUD de recursos funciona correctamente
- ❌ Ningún archivo de reporte (`src/features/report/`) — Etapa 3
- ❌ Ningún archivo de dominio (`src/domain/`) — Etapa 1 ya los cerró
- ❌ Ningún archivo de persistencia (`src/persistence/`) — no tocar
- ❌ Edición inline de proceso/cliente — Etapa 4
- ❌ PDFs — Etapa 5
---
## Reporte esperado al terminar
```
## Etapa 2 completada
### Archivos modificados
- src/features/workspace/ActivityPanel.tsx
### Cambios realizados
- Slider eliminado (import removido, sin referencias en el archivo)
- Inputs numéricos de utilizationMinutes y units implementados
- Cálculo en tiempo real: costo por recurso + resumen total
- Helper contextual con annualFrequency del proceso
- [cualquier variación respecto al diseño propuesto]
### Verificaciones
- grep Slider/utilizationPercent en ActivityPanel: 0 resultados ✅
- git diff --name-only HEAD: solo ActivityPanel.tsx ✅
- npm run build: ✅
- npm run test: X tests, todos verdes ✅
- Smoke test: [descripción breve de lo que se verificó manualmente]
### Notas para Etapas siguientes
[hallazgos relevantes para Etapas 3+]
```