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

View File

@@ -79,7 +79,7 @@ describe('runSimulation', () => {
name: string,
directCost: number,
minutes: number,
resources: Array<{ resourceId: string; utilizationPercent: number }> = []
resources: Array<{ resourceId: string; utilizationMinutes: number; units?: number }> = []
): Activity => ({
id: `act-${bpmnElementId}`,
processId: 'proc1',
@@ -88,7 +88,7 @@ describe('runSimulation', () => {
type: 'task',
directCostFixed: directCost,
executionTimeMinutes: minutes,
assignedResources: resources,
assignedResources: resources.map((r) => ({ ...r, units: r.units ?? 1 })),
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
@@ -136,7 +136,7 @@ describe('runSimulation', () => {
it('incluye costo de recursos asignados', () => {
const resource = makeResource('res1', 'Analista', 60) // $60/hora
const activityA = makeActivity('taskA', 'Tarea A', 0, 60, [
{ resourceId: 'res1', utilizationPercent: 1.0 }, // 1 hora completa
{ resourceId: 'res1', utilizationMinutes: 60, units: 1 }, // 1 hora completa
])
const input: SimulationInput = {
@@ -390,3 +390,108 @@ describe('runSimulation — scenario automated', () => {
}
})
})
// ─── Sprint 2: modelo utilizationMinutes + units ──────────────────────────────
describe('runSimulation — recursos con modelo utilizationMinutes + units', () => {
const makeRes = (id: string, costPerHour: number): Resource => ({
id, processId: 'proc1', name: `Recurso ${id}`, type: 'role', costPerHour,
})
const makeAct = (
bpmnElementId: string,
directCost: number,
minutes: number,
resources: import('@/domain/types').ActivityResourceAssignment[] = []
): Activity => ({
id: `act-${bpmnElementId}`, processId: 'proc1', bpmnElementId,
name: bpmnElementId, type: 'task',
directCostFixed: directCost, executionTimeMinutes: minutes,
assignedResources: resources,
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
})
it('actividad sin recursos: costo = directCostFixed', () => {
const act = makeAct('t1', 200, 30)
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', { ...act, bpmnElementId: 'taskA' }]]),
gateways: new Map(), resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})
expect(result.totalDirectCost).toBe(200)
})
it('actividad con 1 recurso: costo = fijo + costPerHour × utilizationMinutes/60 × units × execProb', () => {
// Analista: $120/h, 30 min, 1 unidad → $60. Fijo: $50. Total: $110
const act = makeAct('taskA', 50, 60, [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }])
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act]]),
gateways: new Map(),
resources: new Map([['r1', makeRes('r1', 120)]]),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})
const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')
expect(actResult?.expectedDirectCost).toBeCloseTo(110)
})
it('actividad con 2 recursos: costo = fijo + suma de costos de recursos', () => {
// r1: $60/h × 30min/60 × 1 = $30. r2: $40/h × 60min/60 × 1 = $40. Fijo: $20. Total: $90
const act = makeAct('taskA', 20, 60, [
{ resourceId: 'r1', utilizationMinutes: 30, units: 1 },
{ resourceId: 'r2', utilizationMinutes: 60, units: 1 },
])
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act]]),
gateways: new Map(),
resources: new Map([['r1', makeRes('r1', 60)], ['r2', makeRes('r2', 40)]]),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})
const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')
expect(actResult?.expectedDirectCost).toBeCloseTo(90)
})
it('units = 2: costo de recurso se duplica', () => {
// $60/h × 30min/60 × 2 unidades = $60. Fijo: $0. Total: $60
const act = makeAct('taskA', 0, 30, [{ resourceId: 'r1', utilizationMinutes: 30, units: 2 }])
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act]]),
gateways: new Map(),
resources: new Map([['r1', makeRes('r1', 60)]]),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})
expect(result.totalDirectCost).toBeCloseTo(60)
})
it('utilizationMinutes = 0: el recurso no agrega costo', () => {
const act = makeAct('taskA', 100, 60, [{ resourceId: 'r1', utilizationMinutes: 0, units: 1 }])
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act]]),
gateways: new Map(),
resources: new Map([['r1', makeRes('r1', 200)]]),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})
expect(result.totalDirectCost).toBe(100)
})
it('resource no encontrado en el mapa: se ignora sin lanzar error', () => {
const act = makeAct('taskA', 100, 60, [{ resourceId: 'no-existe', utilizationMinutes: 30, units: 1 }])
expect(() => runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act]]),
gateways: new Map(), resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})).not.toThrow()
const result = runSimulation({
processXml: LINEAR_BPMN,
activities: new Map([['taskA', act]]),
gateways: new Map(), resources: new Map(),
globalSettings: { currency: 'USD', overheadPercentage: 0 },
})
expect(result.totalDirectCost).toBe(100)
})
})