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:
2026-05-27 11:49:43 -03:00
parent d2746f07d1
commit 67e259e6ed
57 changed files with 4372 additions and 82 deletions

View File

@@ -14,7 +14,7 @@ function makeProcess(extras: Partial<Process> = {}): Process {
id: 'p1', name: 'Proceso de Crédito', clientName: 'Banco XYZ',
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: 0, updatedAt: 0, ...extras,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ...extras,
}
}
@@ -66,11 +66,13 @@ const mockActivities: Activity[] = [
id: 'a1', processId: 'p1', bpmnElementId: 'task_recv', name: 'Recibir solicitud', type: 'task',
directCostFixed: 200, executionTimeMinutes: 60, assignedResources: [],
automatable: true, automatedCostFixed: 20, automatedTimeMinutes: 5,
automatedAssignedResources: [],
},
{
id: 'a2', processId: 'p1', bpmnElementId: 'task_valid', name: 'Validar datos', type: 'task',
directCostFixed: 100, executionTimeMinutes: 30, assignedResources: [],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [],
},
]

View File

@@ -9,7 +9,7 @@ function makeProcess(currency: string, extras: Partial<Process> = {}): Process {
id: 'p1', name: 'Proceso de Ventas', clientName: 'Empresa ABC',
bpmnXml: '', currency, overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, ...extras,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ...extras,
}
}

View File

@@ -26,7 +26,7 @@ function buildInput(xml: string, directCost: number): SimulationInput {
actEls.map((el) => [el.bpmnElementId, {
id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, name: el.name,
type: el.type, directCostFixed: directCost, executionTimeMinutes: 30, assignedResources: [],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0, automatedAssignedResources: [],
}])
)
@@ -66,6 +66,7 @@ describe('Medición de tamaños de CSV — 3 sample BPMNs', () => {
bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const simulation: Simulation = {

View File

@@ -51,6 +51,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const perActivityActual = [
@@ -101,11 +102,13 @@ const mockActivities: Activity[] = [
id: 'a1', processId: 'p1', bpmnElementId: 'task_recv', name: 'Recibir solicitud', type: 'task',
directCostFixed: 200, executionTimeMinutes: 60, assignedResources: [],
automatable: true, automatedCostFixed: 20, automatedTimeMinutes: 5,
automatedAssignedResources: [],
},
{
id: 'a2', processId: 'p1', bpmnElementId: 'task_valid', name: 'Validar datos', type: 'task',
directCostFixed: 100, executionTimeMinutes: 30, assignedResources: [],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [],
},
]

View File

@@ -43,6 +43,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
// Simulación SIN recursos en ninguna actividad
@@ -72,6 +73,7 @@ const mockActivity: Activity = {
directCostFixed: 200, executionTimeMinutes: 60,
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [],
}
const mockSimWithResources: Simulation = {

View File

@@ -54,6 +54,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const mockSimulation: Simulation = {

View File

@@ -59,6 +59,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 50000,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const mockSimulation: Simulation = {

View File

@@ -30,6 +30,7 @@ const mockActivity: Activity = {
directCostFixed: 600, executionTimeMinutes: 60,
assignedResources: [{ resourceId: 'r1', utilizationMinutes: 60, units: 1 }],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatedAssignedResources: [{ resourceId: 'r1', utilizationMinutes: 5, units: 3 }],
}
const activityWithResources: ActivitySimResult = {
@@ -122,3 +123,43 @@ describe('drawResourcesSection', () => {
expect(autoTableMock).toHaveBeenCalledTimes(1)
})
})
// ─── Escenario automatizado usa automatedAssignedResources ────────────────────
describe('drawResourcesSection — escenario automatizado', () => {
let autoTableMock: ReturnType<typeof vi.fn>
beforeEach(() => {
autoTableMock = vi.fn()
})
it('escenario automatizado: pasa utilizationMinutes y units de automatedAssignedResources', async () => {
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
drawResourcesSection(
mockDoc, autoTableMock,
{ perActivity: [activityWithResources] },
[mockResource], [mockActivity], 'USD', 40,
'automatizado'
)
const body = (autoTableMock.mock.calls[0][1] as any).body as any[][]
const row = body[0]
// Min. (índice 4) y Uds. (índice 5) deben venir de automatedAssignedResources: { utilMin: 5, units: 3 }
expect(row[4].content).toBe('5')
expect(row[5].content).toBe('3')
})
it('escenario actual: pasa utilizationMinutes y units de assignedResources (sin regresión)', async () => {
const { drawResourcesSection } = await import('@/lib/export/pdf-sections')
drawResourcesSection(
mockDoc, autoTableMock,
{ perActivity: [activityWithResources] },
[mockResource], [mockActivity], 'USD', 40,
'actual'
)
const body = (autoTableMock.mock.calls[0][1] as any).body as any[][]
const row = body[0]
// assignedResources: { utilMin: 60, units: 1 }
expect(row[4].content).toBe('60')
expect(row[5].content).toBe('1')
})
})

View File

@@ -56,6 +56,7 @@ const mockProcess: Process = {
overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const mockSimulation: Simulation = {

View File

@@ -52,6 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const mockRoi: RoiResult = {
@@ -103,10 +104,10 @@ const mockSimulation: Simulation = {
const mockActivities: Activity[] = [
{ id: 'a1', processId: 'p1', bpmnElementId: 't1', name: 'Tarea A', type: 'task',
automatable: true, directCostFixed: 400, executionTimeMinutes: 30,
automatedCostFixed: 40, automatedTimeMinutes: 5, assignedResources: [] },
automatedCostFixed: 40, automatedTimeMinutes: 5, assignedResources: [], automatedAssignedResources: [] },
{ id: 'a2', processId: 'p1', bpmnElementId: 't2', name: 'Tarea B', type: 'task',
automatable: false, directCostFixed: 200, executionTimeMinutes: 20,
automatedCostFixed: 0, automatedTimeMinutes: 0, assignedResources: [] },
automatedCostFixed: 0, automatedTimeMinutes: 0, assignedResources: [], automatedAssignedResources: [] },
]
describe('PDF ROI — Etapa 7 páginas 2-5', () => {

View File

@@ -52,6 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [],
}
const mockSimulation: Simulation = {