Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Tipos: Resource.processId? eliminado (vestigial); ResourceType ya tenía los 5 valores correctos, sin cambios. resource-repo.ts: agregado getAllWithCreator() con JOIN a users!created_by (mismo patrón que Etapa 6) y tipo ResourceWithCreator. delete() ahora valida que no haya asignaciones activas en activity_resource_assignments antes de borrar — lanza error explicativo si las hay. Verifiqué ambos contra Supabase real: el JOIN funciona (FK reconocida por PostgREST) y el conteo de guarda también. Nuevo — src/features/resources/: resource-display.tsx — labels, colores/íconos por tipo (Lucide: User/Briefcase/Server/Wrench/Package) y ResourceTypeAvatar compartido ResourceFormSheet.tsx — Sheet reutilizable para crear/editar, usado tanto en el catálogo como en el panel del workspace ResourceCatalogPage.tsx — página /recursos: búsqueda, filtro por tipo, tabla con badge de tipo, costo, "creado por", editar/eliminar con confirmación Nuevo — src/components/ui/sheet.tsx: componente shadcn Sheet (no existía), construido sobre @radix-ui/react-dialog que ya estaba instalado — no agregué dependencias nuevas. ResourcesPanel.tsx rediseñado: ahora solo muestra recursos usados en el proceso actual (derivado de assignedResources), con conteo de actividades por recurso. CRUD completo se mudó a /recursos; el panel solo permite "Nuevo" (catálogo global) y tiene el link "Ver catálogo completo". Router: ruta /recursos registrada. ActivityPanel no se tocó, como indicaba el scope.
100 lines
4.2 KiB
TypeScript
100 lines
4.2 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import { runSimulation } from '@/domain/simulation'
|
||
import type { Activity, Resource, SimulationInput } from '@/domain/types'
|
||
|
||
// BPMN de un solo nodo — execProb = 1.0, sin gateways
|
||
const SINGLE_TASK_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
|
||
<process id="proc1" isExecutable="false">
|
||
<startEvent id="start"><outgoing>f1</outgoing></startEvent>
|
||
<task id="taskA" name="Tarea A"><incoming>f1</incoming><outgoing>f2</outgoing></task>
|
||
<endEvent id="end"><incoming>f2</incoming></endEvent>
|
||
<sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
|
||
<sequenceFlow id="f2" sourceRef="taskA" targetRef="end"/>
|
||
</process>
|
||
</definitions>`
|
||
|
||
const makeResource = (id: string, costPerHour: number): Resource => ({
|
||
id, name: `Recurso ${id}`, type: 'role', costPerHour,
|
||
})
|
||
|
||
const baseActivity = (overrides: Partial<Activity> = {}): Activity => ({
|
||
id: 'act-taskA', processId: 'proc1', bpmnElementId: 'taskA',
|
||
name: 'Tarea A', type: 'task',
|
||
directCostFixed: 0, executionTimeMinutes: 0,
|
||
assignedResources: [],
|
||
automatable: true,
|
||
automatedCostFixed: 0, automatedTimeMinutes: 0,
|
||
automatedAssignedResources: [],
|
||
...overrides,
|
||
})
|
||
|
||
const baseInput = (activity: Activity, resources: Resource[] = []): SimulationInput => ({
|
||
processXml: SINGLE_TASK_BPMN,
|
||
activities: new Map([['taskA', activity]]),
|
||
gateways: new Map(),
|
||
resources: new Map(resources.map((r) => [r.id, r])),
|
||
globalSettings: { currency: 'USD', overheadPercentage: 0 },
|
||
scenario: 'automated',
|
||
})
|
||
|
||
describe('Sprint 3 — automatedAssignedResources en motor de simulación', () => {
|
||
|
||
it('Caso 1: solo costo fijo, sin recursos TO-BE — resourceCostBreakdown vacío', () => {
|
||
const act = baseActivity({ automatedCostFixed: 100, automatedTimeMinutes: 60, automatedAssignedResources: [] })
|
||
const result = runSimulation(baseInput(act))
|
||
|
||
const actResult = result.perActivity[0]
|
||
expect(actResult.expectedDirectCost).toBeCloseTo(100)
|
||
expect(actResult.resourceCostBreakdown).toHaveLength(0)
|
||
})
|
||
|
||
it('Caso 2: solo recursos TO-BE — costo = costPerHour × (min/60) × units × execProb', () => {
|
||
const r1 = makeResource('r1', 30)
|
||
const act = baseActivity({
|
||
automatedCostFixed: 0, automatedTimeMinutes: 30,
|
||
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 2 }],
|
||
})
|
||
const result = runSimulation(baseInput(act, [r1]))
|
||
|
||
// 30 $/h × (60min/60) × 2 units × 1.0 execProb = 60
|
||
const actResult = result.perActivity[0]
|
||
expect(actResult.expectedDirectCost).toBeCloseTo(60)
|
||
expect(actResult.resourceCostBreakdown).toHaveLength(1)
|
||
expect(actResult.resourceCostBreakdown[0].resourceId).toBe('r1')
|
||
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(60)
|
||
})
|
||
|
||
it('Caso 3: mixto — costo fijo + recurso TO-BE se suman', () => {
|
||
const r1 = makeResource('r1', 60)
|
||
const act = baseActivity({
|
||
automatedCostFixed: 50, automatedTimeMinutes: 30,
|
||
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }],
|
||
})
|
||
const result = runSimulation(baseInput(act, [r1]))
|
||
|
||
// fijo=50 + 60*(30/60)*1=30 → total=80
|
||
const actResult = result.perActivity[0]
|
||
expect(actResult.expectedDirectCost).toBeCloseTo(80)
|
||
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(30)
|
||
})
|
||
|
||
it('Caso 4: actividad NO automatable — usa assignedResources, ignora automatedAssignedResources', () => {
|
||
const r1 = makeResource('r1', 60)
|
||
const r2 = makeResource('r2', 100)
|
||
const act = baseActivity({
|
||
automatable: false,
|
||
directCostFixed: 0,
|
||
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }],
|
||
automatedAssignedResources: [{ resourceId: 'r2', utilizationMinutes: 60, units: 1 }],
|
||
})
|
||
const result = runSimulation(baseInput(act, [r1, r2]))
|
||
|
||
// escenario automated pero automatable=false → usa r1 (assignedResources), no r2
|
||
const actResult = result.perActivity[0]
|
||
expect(actResult.resourceCostBreakdown).toHaveLength(1)
|
||
expect(actResult.resourceCostBreakdown[0].resourceId).toBe('r1')
|
||
expect(actResult.resourceCostBreakdown[0].cost).toBeCloseTo(60)
|
||
})
|
||
})
|