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

@@ -32,6 +32,7 @@ function makeActivity(overrides: Partial<Activity> & { id: string; processId: st
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
automatedAssignedResources: [],
}
}

View File

@@ -16,7 +16,7 @@ import type { Activity, Process } from '@/domain/types'
// ─── Fixtures de registros "viejos" (sin campos Sprint 1) ─────────────────────
function makeLegacyActivity(id: string, processId: string): Omit<Activity, 'automatable' | 'automatedCostFixed' | 'automatedTimeMinutes'> {
function makeLegacyActivity(id: string, processId: string): Omit<Activity, 'automatable' | 'automatedCostFixed' | 'automatedTimeMinutes' | 'automatedAssignedResources'> {
return {
id, processId,
bpmnElementId: `task_${id}`,
@@ -28,7 +28,7 @@ function makeLegacyActivity(id: string, processId: string): Omit<Activity, 'auto
}
}
function makeLegacyProcess(id: string): Omit<Process, 'annualFrequency' | 'analysisHorizonYears' | 'automationInvestment'> {
function makeLegacyProcess(id: string): Omit<Process, 'annualFrequency' | 'analysisHorizonYears' | 'automationInvestment' | 'groupId' | 'tags'> {
return {
id, name: 'Proceso Legacy', clientName: 'Cliente Viejo',
bpmnXml: '<definitions/>', currency: 'USD', overheadPercentage: 0.15,
@@ -59,6 +59,7 @@ describe('Migración Dexie v2 — Activity: defaults de campos Sprint 1', () =>
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
automatedAssignedResources: [],
}
await activityRepo.save(act)
const retrieved = await activityRepo.getByProcess('p1')
@@ -73,6 +74,7 @@ describe('Migración Dexie v2 — Activity: defaults de campos Sprint 1', () =>
automatable: true,
automatedCostFixed: 12.5,
automatedTimeMinutes: 3,
automatedAssignedResources: [],
}
await activityRepo.save(act)
const [retrieved] = await activityRepo.getByProcess('p1')
@@ -87,6 +89,7 @@ describe('Migración Dexie v2 — Activity: defaults de campos Sprint 1', () =>
automatable: true,
automatedCostFixed: 5,
automatedTimeMinutes: 10,
automatedAssignedResources: [],
}
await activityRepo.save(act)
// Actualizar solo directCostFixed — los campos de automatización no deben cambiar
@@ -105,6 +108,7 @@ describe('Migración Dexie v2 — Process: defaults de campos Sprint 1', () => {
annualFrequency: 1000,
analysisHorizonYears: 3,
automationInvestment: 0,
groupId: null, tags: [],
}
await processRepo.save(proc)
const retrieved = await processRepo.getById('proc-1')
@@ -119,6 +123,7 @@ describe('Migración Dexie v2 — Process: defaults de campos Sprint 1', () => {
annualFrequency: 5000,
analysisHorizonYears: 5,
automationInvestment: 75_000,
groupId: null, tags: [],
}
await processRepo.save(proc)
const retrieved = await processRepo.getById('proc-2')
@@ -133,6 +138,7 @@ describe('Migración Dexie v2 — Process: defaults de campos Sprint 1', () => {
annualFrequency: 2000,
analysisHorizonYears: 4,
automationInvestment: 30_000,
groupId: null, tags: [],
}
await processRepo.save(proc)
// Actualizar solo el nombre — los campos de volumetría no deben cambiar

View File

@@ -85,7 +85,7 @@ describe('motor / propagación con GatewayConfig correcta', () => {
actIds.map((id) => [id, {
id: uuidv4(), processId, bpmnElementId: id, name: id,
type: 'task', directCostFixed: 100, executionTimeMinutes: 30, assignedResources: [],
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0, automatedAssignedResources: [],
}])
)

View File

@@ -32,6 +32,8 @@ function makeProcess(id = 'proc-1'): Process {
automationInvestment: 0,
createdAt: 1000,
updatedAt: 2000,
groupId: null,
tags: [],
}
}
@@ -48,6 +50,7 @@ function makeActivity(id: string, processId: string, bpmnElementId: string): Act
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
automatedAssignedResources: [],
}
}

View File

@@ -56,6 +56,7 @@ function buildInput(
automatable: false,
automatedCostFixed: 0,
automatedTimeMinutes: 0,
automatedAssignedResources: [],
},
])
)

View File

@@ -51,6 +51,7 @@ function buildActivities(xml: string): Map<string, Activity> {
automatable: AUTOMATABLE_IDS.has(el.bpmnElementId),
automatedCostFixed: AUTOMATABLE_IDS.has(el.bpmnElementId) ? AUTOMATED_COST : 0,
automatedTimeMinutes: AUTOMATABLE_IDS.has(el.bpmnElementId) ? AUTOMATED_TIME : 0,
automatedAssignedResources: [],
}])
)
}

View File

@@ -52,6 +52,7 @@ function buildSimInput(
automatable: automatableIds.has(el.bpmnElementId),
automatedCostFixed: automatableIds.has(el.bpmnElementId) ? automatedCost : 0,
automatedTimeMinutes: automatableIds.has(el.bpmnElementId) ? automatedTime : 0,
automatedAssignedResources: [],
}])
)
@@ -312,6 +313,7 @@ describe('Caso C — medium-with-gateways, 1 automatable en ceros + investment=0
automatable: el.bpmnElementId === 'task_valid',
automatedCostFixed: 0, // cero — trigger de transparencia
automatedTimeMinutes: 0, // cero — trigger de transparencia
automatedAssignedResources: [],
}])
)