# 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 193–199:

```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 (0–100) 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 159–228) 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+]
```
