Etapa 7 ya estaba completamente ejecutada en esta sesión. Todo está en su lugar:
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

Fix A1 — isProfileLoaded en AuthContext 

useState(false) inicial
setIsProfileLoaded(true) si onAuthStateChange devuelve null (no hay usuario que sincronizar)
setIsProfileLoaded(false) en SIGNED_IN (resetea para el nuevo sync)
setIsProfileLoaded(true) en el finally del sync effect (después de syncProfileFromDB, éxito o error)
Fix A2 — Guard en LibraryPage 

if (!user || !isProfileLoaded) devuelve skeleton animado antes de cualquier contenido rol-dependiente
Fix A3 — Campo "cliente" read-only para client_editor 

GlobalSettingsPanel.tsx: <p> estático en lugar de <Input> cuando isClientRole
WorkspacePage.tsx: topbar muestra el valor sin input ni lápiz para client_editor
Fix B — Migración 016 + aplicada a DB 

Archivo supabase/migrations/016_add_simulations_update_policy.sql creado
Policy simulations_update confirmada activa en DB (pg_policies devuelve las 4 policies de simulations)
Fix adicional — 403 en POST /processes  (ejecutado en el turno inmediato anterior)

updateEditable() en process-repo.ts usa .update() en lugar de .upsert() para todos los paths de edición
Tests: 586 verdes, build limpio.

Lo que falta según el checklist del prompt es la validación visual del director (msebem@gmail.com en la app real). Eso requiere tu OK después de verificar en el navegador.

domain/types.ts — orgId: string | null agregado a Process (campo de DB ya existente, ahora mapeado en el dominio).

process-repo.ts — fromRow incluye orgId: (row.org_id as string | null) ?? null. Ambas queries (getById y getAll) usan select('*'), por lo que el campo viene gratis.

WorkspacePage.tsx:

Import supabase para el fetch de org name
Estado orgName + useEffect que hace SELECT name FROM organizations WHERE id = orgId cuando cambia currentProcess?.orgId
Variable isClientRole (solo client_editor — client_viewer ya fue redirigido)
backLabel: "Procesos" para client_editor, nombre de org (o "Volver" mientras carga) si hay orgId, "Biblioteca" si no hay
handleBack(): navega a /?org=<orgId> para admin con proceso de cliente, a / para el resto
El Home button + Tooltip fueron reemplazados por <Button variant="ghost" size="sm"> con ArrowLeft + backLabel
LibraryPage.tsx:

Import useSearch
Lee orgParam = useSearch({ from: '/' }) as { org?: string }
useEffect que, cuando orgParam está presente y la carga terminó, busca el proceso con p.orgId === orgParam, obtiene su groupId, y hace scrollIntoView al elemento group-{groupId}
Los GroupCards en el grid están ahora envueltos en <div id="group-{group.id}"> para ser alcanzables por el scroll
Tests — workspace-back-button.test.ts con 6 casos: admin+orgName, admin+orgName loading, admin sin org, member sin org, client_editor, client_viewer. Fixtures actualizados en 13 archivos con orgId: null. Mock de useSearch agregado en library-ui.test.tsx.
This commit is contained in:
2026-07-07 14:12:10 -03:00
parent cad8fbca4f
commit ee356e5de6
23 changed files with 414 additions and 35 deletions

View File

@@ -199,6 +199,81 @@ Antes de presentar cualquier PDF o pantalla:
---
## Gestión de versiones y commits
> Aplica a partir de Sprint 8. Claude Code es responsable del ciclo completo de versionado.
### Conventional Commits — obligatorio en todos los commits
```
feat(scope): descripción ← feature nueva visible al usuario
fix(scope): descripción ← corrección de bug
refactor(scope): descripción ← refactor sin cambio de comportamiento externo
test(scope): descripción ← solo tests, sin cambio de código de producción
docs(scope): descripción ← solo documentación
chore(scope): descripción ← tareas de infra, build, deps
```
`scope` es el área afectada: `auth`, `workspace`, `library`, `report`, `pdf`, `admin`, `db`, `e2e`, etc.
Ejemplos correctos:
```
feat(workspace): back button contextual con navegación a biblioteca del cliente
fix(process-repo): cambiar upsert por update para evitar 403 en client_editor
refactor(auth): agregar isProfileLoaded para evitar race condition de roles
```
### CHANGELOG.md
Actualizar al cerrar cada etapa. Formato Keep a Changelog:
```markdown
## [Unreleased]
### Added
- Back button contextual en workspace para admin
### Fixed
- 403 en Guardar configuración para client_editor (upsert → update)
```
Al cerrar un sprint, mover `[Unreleased]` a `[vX.Y.Z] - YYYY-MM-DD`.
### SemVer — criterios
- **MAJOR:** cambio incompatible (nuevo modelo de datos, cambio de rol fundamental, migración forzada de usuarios)
- **MINOR:** feature nueva visible al usuario (nueva página, nueva funcionalidad en workspace)
- **PATCH:** corrección de bug o mejora de UX sin feature nueva
Sprint 7 = `v0.7.x` (MVP multi-tenant). Sprint 8 arranca en `v0.8.0`.
### Secuencia de cierre de etapa
Al completar una etapa con cambios en producción:
```bash
git add -A
git commit -m "feat(scope): descripción concisa"
# Actualizar CHANGELOG.md
git add CHANGELOG.md
git commit -m "docs(changelog): etapa N sprint X"
git push origin main
```
Al cerrar un sprint completo, agregar además:
```bash
git tag -a vX.Y.Z -m "Release vX.Y.Z: descripción del sprint"
git push --tags
```
### Versión visible en la app
La versión se lee de `package.json` (`version` field) y se muestra en el footer de todas las páginas (o al menos en `/library` y `/report`). Formato: `InQ ROI v1.2.3`.
Si no existe un componente de footer global, crearlo al implementar esta feature por primera vez.
---
## Política de iniciativa proactiva
Antes de aplicar un cambio fuera del scope del prompt actual, evaluar en orden:

View File

@@ -385,4 +385,71 @@ En tres etapas (AdminLayout, WorkspacePage, ImportPage), Claude Code implementó
- Email visible en panel admin (requiere join con `auth.users` via service_role)
- Historial de simulaciones (diferido desde Sprint 3)
**Cerrado:** 2026-07-06.
**Cerrado:** 2026-07-07 (con extensión de hotfixes: Etapa 6 ConfirmInvitePage, fix upsert→update en process-repo, Etapa 7 race condition AuthContext + simulations_update policy, Etapa 7B back button contextual en workspace).
---
## Decisiones estratégicas confirmadas para Sprint 8
> Registradas el 2026-07-07 durante la sesión de cierre de Sprint 7.
### client_editor: permisos de BPMN ampliados
**Decisión del director:** `client_editor` podrá:
1. **Actualizar BPMN de procesos existentes** asignados a su org (reemplazar `bpmn_xml` vía UPDATE)
2. **Importar procesos nuevos** con el botón "Importar BPMN", creándolos con `org_id = current_org()`
**Implicancias técnicas para el brief:**
- `updateEditable()` debe incluir `bpmn_xml` cuando el caller tiene permisos para modificarlo
- Política INSERT en `processes`: agregar rama `client_editor AND org_id = current_org()` (los procesos nuevos quedan dentro de su org)
- Botón "Importar BPMN" visible para `client_editor` (actualmente bloqueado en Etapa 5)
- Los procesos creados por `client_editor` tienen `owner_id = auth.uid()` y `org_id = current_org()`
- `client_editor` NO puede mover procesos entre orgs ni cambiar `owner_id`
**Precaución:** relajar INSERT en processes para client_editor es cambio de modelo. El brief de Sprint 8 debe incluir una migración que ajuste la política y tests E2E que validen el scope (no pueden crear procesos sin org_id, no pueden crear procesos en otra org).
### Versionado profesional con changelog
**Decisión del director:** a partir de Sprint 8, Claude Code es responsable del ciclo completo de versiones:
1. **Commits:** formato Conventional Commits obligatorio
- `feat(scope): descripción` — feature nueva
- `fix(scope): descripción` — corrección de bug
- `refactor(scope): descripción` — refactor sin cambio de comportamiento
- `test(scope): descripción` — solo tests
- `docs(scope): descripción` — solo documentación
- `chore(scope): descripción` — tareas de infra/build
2. **CHANGELOG.md:** actualizar al cerrar cada etapa, formato Keep a Changelog
3. **SemVer para versiones:** `MAJOR.MINOR.PATCH`
- MAJOR: cambio incompatible de producto (nuevo modelo de datos, cambio de rol fundamental)
- MINOR: feature nueva visible al usuario
- PATCH: fix de bug, mejora de UX sin feature nueva
4. **Git tags:** `git tag -a vX.Y.Z -m "Release vX.Y.Z: descripción"` + `git push --tags`
5. **Versión visible en la app:** footer de todas las páginas, leer de `package.json` o `src/lib/version.ts`. Formato: `InQ ROI v1.2.3`.
**Instrucción:** la sección "Gestión de versiones y commits" se agrega a `simulador-web/CLAUDE.md` como Sprint 8 Etapa 0, antes de cualquier feature nueva.
### Página de ayuda (`/help`)
**Decisión del director:** Sprint 8 incluye una ruta `/help` con documentación interna del producto para todos los roles.
**Contenido mínimo:**
- Cómo parametrizar actividades (tiempos, costos, recursos)
- Cómo simular y leer el reporte
- Cómo exportar PDF y CSV
- Referencia rápida de los campos del panel Global
**Requisito de método añadido al pipeline de sprints:** cualquier `ETAPA_X_PROMPT.md` que modifique la UI incluirá un ítem en los criterios de validación: `[ ] Verificar si el cambio afecta el contenido de /help — actualizar si corresponde`. Agregar al template de etapa en `methodology/` antes de abrir Sprint 8.
### Botón "volver" contextual en workspace
**Decisión del director:** implementado en Sprint 7 como Etapa 7B. Brief en `sprints/sprint-7/ETAPA_7B_PROMPT.md`.
Comportamiento:
- Admin en proceso de cliente → "← [nombre del cliente]" → `/library?org=<uuid>` con scroll al cliente
- Admin en proceso sin org → "← Biblioteca" → `/library`
- client_editor/viewer → "← Procesos" → `/library`

View File

@@ -0,0 +1,133 @@
# Etapa 7B — Botón "volver" contextual en workspace
**Sprint:** 7
**Fecha:** 2026-07-07
**Alcance:** workspace header únicamente. Sin tocar LibraryPage, AuthContext, migraciones, ni lógica de simulación.
---
## Problema
Cuando un admin (platform_admin/member) abre el workspace de un proceso que pertenece a un cliente (org no-provider), no hay forma de volver a la vista de los procesos de ese cliente. El ícono de casa (🏠) lleva a la biblioteca general, pero pierde el contexto de qué cliente se estaba revisando.
El mismo problema aplica a client_editor: al entrar al workspace, no hay un back button visible.
---
## Comportamiento esperado
### Para admin (platform_admin / member)
Si el proceso tiene `org_id` asignado (proceso de un cliente):
```
← Solar Banco [nombre del proceso] [botones de acción]
```
El botón "← {org.name}" navega a `/library?org={org_id}`.
Si el proceso NO tiene `org_id` (proceso sin cliente asignado):
```
← Biblioteca [nombre del proceso] [botones de acción]
```
Navega a `/library`.
### Para client_editor / client_viewer
```
← Procesos [nombre del proceso] [botones de acción]
```
Navega a `/library`. (Para estos roles, `/library` ya muestra solo los procesos de su org.)
---
## Cambios en código
**ANTES de empezar:** leer el componente del header del workspace para entender la estructura actual del breadcrumb/navegación. También leer cómo el store o el componente accede a la información del proceso actual (nombre, org_id). Verificar si la información de `org_id` y `org_name` está disponible en el estado del proceso cargado.
### 1. Library route — soporte para query param `?org=<uuid>`
En `LibraryPage.tsx` (o el router de la biblioteca), agregar soporte para el query param `org`:
- Leer `org` de los query params con TanStack Router (`useSearch`)
- Si `org` está presente: al cargar la biblioteca, hacer scroll automático al grupo/cliente cuyo `org_id` coincide, o resaltarlo visualmente
- Si `org` no está presente: comportamiento actual sin cambios
**No es necesario filtrar la lista** — mostrar todos los clientes como siempre, pero posicionar la vista en el cliente relevante. Esto preserva el contexto sin ocultar información.
El scroll puede ser simple: `document.getElementById('org-{org_id}')?.scrollIntoView({ behavior: 'smooth', block: 'start' })` con un `useEffect` que se dispara cuando `org` está en los params y la lista cargó.
### 2. Workspace header — back button contextual
En el componente del workspace header (WorkspaceHeader, WorkspacePage, o donde esté el breadcrumb):
Agregar antes del separador `|` y del nombre del proceso un botón de navegación contextual:
```typescript
// Determinar destino del back button
const backHref = process.orgId
? `/library?org=${process.orgId}`
: '/library'
const backLabel = process.orgName // si está disponible en el store
?? (isClientRole ? 'Procesos' : 'Biblioteca')
```
**UI:** botón ghost pequeño con ícono de flecha izquierda (`ArrowLeft` de Lucide) + texto. Consistente con el estilo del header actual. Ejemplo:
```
[←] Solar Banco | Conciliaciones ATM [editable] • ...
```
Si `process.orgName` no está disponible en el store actual, hay dos opciones:
a) Hacer un fetch adicional de la organización (simple: `supabase.from('organizations').select('name').eq('id', orgId).single()`) al cargar el workspace
b) O usar solo el botón sin nombre ("← Volver") y el admin sabrá dónde lo lleva
**Preferir opción (a)** si el costo de implementación es bajo (un SELECT adicional en el store de workspace). Si es complejo, usar (b) y registrar como mejora en BACKLOG.md.
---
## Tests
```typescript
// Mínimo:
// 1. Back button muestra texto correcto para admin con proceso de cliente
// (mock: process.orgId = 'some-uuid', process.orgName = 'Solar Banco')
// → expect button text to contain 'Solar Banco' o 'Volver'
// → expect link href to contain '/library?org=some-uuid'
// 2. Back button muestra "Biblioteca" para admin con proceso sin org
// (mock: process.orgId = null)
// → expect href = '/library'
// 3. Back button muestra "Procesos" para client_editor
// (mock: isClientRole = true)
// → expect href = '/library'
```
---
## Criterios de validación
- [ ] Admin en workspace de proceso con cliente → ve "← [nombre del cliente]" en el header
- [ ] Click en ese botón → navega a `/library?org=<uuid>` → biblioteca hace scroll al cliente correspondiente
- [ ] Admin en workspace de proceso sin cliente asignado → ve "← Biblioteca"
- [ ] client_editor en workspace → ve "← Procesos"
- [ ] El botón no aparece más de una vez, no rompe el layout del header
- [ ] `npm run test` → tests verdes
- [ ] `npm run build` limpio
- [ ] **Validación visual del director antes del OK formal**
---
## NO modificar
- Edge Functions
- Migraciones
- Lógica de simulación, ROI, PDF, CSV
- `ConfirmInvitePage`, `AdminPage`, `ReportPage`
- `AuthContext` (cubierto en Etapa 7)

View File

@@ -20,6 +20,7 @@ export interface Process {
tags: string[] // 🆕 Sprint 3 — tags libres para clasificación y búsqueda
ownerId: string // 🆕 Sprint 4 — owner del proceso (auth.uid() al crear)
updatedBy: string // 🆕 Sprint 4 — UUID del usuario que hizo el último update
orgId: string | null // 🆕 Sprint 7 — organización dueña del proceso (null = proceso interno InQ)
}
export type ActivityType = 'task' | 'subprocess'

View File

@@ -91,6 +91,7 @@ export function ImportPage() {
tags: [],
ownerId: user!.id,
updatedBy: user!.id,
orgId: null,
}
const activityElements = extractActivityElements(graph)

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useNavigate, useSearch } from '@tanstack/react-router'
import {
Upload, Search, Plus, Building2, FolderX, GitBranch,
Clock, TrendingUp, BarChart2, FileX2, X, Settings,
@@ -592,13 +592,14 @@ function HomeView({
? Math.max(...groupProcesses.map((p) => p.updatedAt))
: null
return (
<GroupCard
key={group.id}
group={group}
processCount={groupProcesses.length}
lastUpdated={lastUpdated}
onClick={() => onNavigateToGroup(group.id)}
/>
<div key={group.id} id={`group-${group.id}`}>
<GroupCard
group={group}
processCount={groupProcesses.length}
lastUpdated={lastUpdated}
onClick={() => onNavigateToGroup(group.id)}
/>
</div>
)
})}
<NewGroupCard
@@ -772,8 +773,9 @@ function GroupView({
export function LibraryPage() {
const navigate = useNavigate()
const { load, processes, groups, settings } = useLibraryStore()
const { load, processes, groups, settings, isLoading } = useLibraryStore()
const { user, isProfileLoaded } = useAuth()
const { org: orgParam } = useSearch({ from: '/' }) as { org?: string }
const [view, setView] = useState<'home' | 'group'>('home')
const [activeGroupId, setActiveGroupId] = useState<string | null>(null)
const [transitioning, setTransitioning] = useState(false)
@@ -785,6 +787,16 @@ export function LibraryPage() {
load()
}, [load, user?.id])
// Cuando la biblioteca cargó con ?org=<uuid>, hacer scroll al grupo que contiene ese org
useEffect(() => {
if (!orgParam || isLoading || processes.length === 0) return
const proc = processes.find((p) => p.orgId === orgParam)
if (!proc?.groupId) return
requestAnimationFrame(() => {
document.getElementById(`group-${proc.groupId}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}, [orgParam, isLoading, processes])
function navigateToGroup(groupId: string | null) {
setTransitioning(true)
setTimeout(() => {

View File

@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback, useMemo, useDeferredValue } f
import { useParams, useNavigate } from '@tanstack/react-router'
import {
Loader2, BarChart3, Play, AlertTriangle, AlertCircle,
PanelRightClose, PanelRightOpen, Home, GitMerge, MousePointer2,
PanelRightClose, PanelRightOpen, ArrowLeft, GitMerge, MousePointer2,
Check, Pencil
} from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -22,6 +22,7 @@ import { useLibraryStore } from '@/store/library-store'
import { useAuth } from '@/auth/AuthContext'
import { AppHeader } from '@/components/AppHeader'
import { ErrorBoundary } from '@/components/ErrorBoundary'
import { supabase } from '@/lib/supabase'
type SelectedCategory = 'activity' | 'gateway' | null
@@ -67,6 +68,14 @@ export function WorkspacePage() {
}
}, [user, processId, navigate])
// Nombre de la organización dueña del proceso (para el back button contextual)
const [orgName, setOrgName] = useState<string | null>(null)
useEffect(() => {
if (!currentProcess?.orgId) { setOrgName(null); return }
supabase.from('organizations').select('name').eq('id', currentProcess.orgId).single()
.then(({ data }) => { if (data) setOrgName(data.name as string) })
}, [currentProcess?.orgId])
// Validación XOR: todos los gateways exclusivos deben sumar 1.0
const xorValidation = useMemo(() => {
const invalidIds = gateways
@@ -183,6 +192,23 @@ export function WorkspacePage() {
)
}
// client_viewer ya fue redirigido antes de este punto; solo verificamos client_editor aquí
const isClientRole = user?.platformRole === 'client_editor'
const backLabel = isClientRole
? 'Procesos'
: currentProcess.orgId
? (orgName ?? 'Volver')
: 'Biblioteca'
function handleBack() {
const orgId = currentProcess?.orgId
if (orgId && !isClientRole) {
void navigate({ to: '/', search: { org: orgId } })
} else {
void navigate({ to: '/' })
}
}
// Label del tab "elemento" según lo seleccionado
const elementTabLabel = selectedCategory === 'gateway' ? 'Gateway' : 'Actividad'
const elementTabIcon = selectedCategory === 'gateway'
@@ -196,14 +222,15 @@ export function WorkspacePage() {
{/* ── Topbar ─────────────────────────────────────────────────── */}
<header className="min-h-12 bg-white border-b border-slate-200 flex items-center px-3 gap-3 shrink-0 z-10">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => navigate({ to: '/' })}>
<Home className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Inicio</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="sm"
className="gap-1 text-xs text-slate-500 h-8 px-2 shrink-0"
onClick={handleBack}
>
<ArrowLeft className="h-3.5 w-3.5" />
{backLabel}
</Button>
<Separator orientation="vertical" className="h-5" />

View File

@@ -39,6 +39,7 @@ function fromRow(row: Record<string, unknown>): Process {
updatedAt: new Date(row.updated_at as string).getTime(),
ownerId: (row.owner_id as string) ?? '',
updatedBy: (row.updated_by as string) ?? '',
orgId: (row.org_id as string | null) ?? null,
}
}

View File

@@ -36,6 +36,7 @@ const { authUserRef, libStoreRef } = vi.hoisted(() => {
clientName: null,
overhead: 0,
isShared: false,
orgId: null,
}
const sampleGroup = {
id: 'grp-1',
@@ -72,6 +73,7 @@ vi.mock('@/components/AppHeader', () => ({ AppHeader: () => null }))
vi.mock('@tanstack/react-router', () => ({
useNavigate: () => vi.fn(),
useSearch: () => ({}),
Link: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))

View File

@@ -229,7 +229,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proceso Test', clientName: '',
bpmnXml: '<def/>', currency: 'USD',
overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
expect(() => render(<MethodologyFooter process={proc} />)).not.toThrow()
})
@@ -239,7 +239,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proc', clientName: '',
bpmnXml: '', currency: 'USD',
overheadPercentage: 0.25, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
render(<MethodologyFooter process={proc} />)
const text = document.body.textContent ?? ''
@@ -251,7 +251,7 @@ describe('MethodologyFooter', () => {
id: 'p1', name: 'Proc', clientName: '',
bpmnXml: '', currency: 'USD',
overheadPercentage: 0.2, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
render(<MethodologyFooter process={proc} />)
const text = document.body.textContent ?? ''
@@ -342,7 +342,7 @@ describe('ReportPage — con datos completos', () => {
updatedAt: Date.now() - 1000 * 60 * 30,
groupId: null,
tags: [],
ownerId: 'test-user', updatedBy: 'test-user',
ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
const mockSimulation = {

View File

@@ -600,7 +600,7 @@ const mockProcessBase = {
currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1200, analysisHorizonYears: 3, automationInvestment: 50_000,
createdAt: Date.now() - 3600_000, updatedAt: Date.now() - 3600_000,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
const mockSimulationBase = {

View File

@@ -0,0 +1,60 @@
/**
* Tests del back button contextual en el workspace header.
* Verifica que backLabel y backTo se calculen correctamente según rol y orgId del proceso.
*/
import { describe, it, expect } from 'vitest'
type PlatformRole = 'platform_admin' | 'member' | 'client_editor' | 'client_viewer' | 'guest'
function resolveBackButton(
platformRole: PlatformRole | undefined,
orgId: string | null,
orgName: string | null,
): { backLabel: string; backTo: string } {
const isClientRole = platformRole === 'client_editor' || platformRole === 'client_viewer'
const backLabel = isClientRole
? 'Procesos'
: orgId
? (orgName ?? 'Volver')
: 'Biblioteca'
const backTo = orgId && !isClientRole ? `/?org=${orgId}` : '/'
return { backLabel, backTo }
}
describe('workspace back button — lógica de backLabel y backTo', () => {
it('1. admin con proceso de cliente → muestra nombre de org y navega a /?org=uuid', () => {
const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', 'Solar Banco')
expect(backLabel).toBe('Solar Banco')
expect(backTo).toBe('/?org=org-uuid-123')
})
it('2. admin con proceso de cliente mientras orgName carga → muestra "Volver" como fallback', () => {
const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', null)
expect(backLabel).toBe('Volver')
expect(backTo).toBe('/?org=org-uuid-123')
})
it('3. admin sin org asignada → muestra "Biblioteca" y navega a /', () => {
const { backLabel, backTo } = resolveBackButton('platform_admin', null, null)
expect(backLabel).toBe('Biblioteca')
expect(backTo).toBe('/')
})
it('4. member sin org → igual que platform_admin', () => {
const { backLabel, backTo } = resolveBackButton('member', null, null)
expect(backLabel).toBe('Biblioteca')
expect(backTo).toBe('/')
})
it('5. client_editor con proceso de cliente → muestra "Procesos" y navega a /', () => {
const { backLabel, backTo } = resolveBackButton('client_editor', 'org-uuid-123', 'Solar Banco')
expect(backLabel).toBe('Procesos')
expect(backTo).toBe('/')
})
it('6. client_viewer con proceso de cliente → muestra "Procesos" y navega a /', () => {
const { backLabel, backTo } = resolveBackButton('client_viewer', 'org-uuid-123', 'Solar Banco')
expect(backLabel).toBe('Procesos')
expect(backTo).toBe('/')
})
})

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, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', ...extras,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, ...extras,
}
}

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, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', ...extras,
createdAt: 0, updatedAt: 0, groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null, ...extras,
}
}

View File

@@ -66,7 +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: [], ownerId: 'test-user', updatedBy: 'test-user',
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
const simulation: Simulation = {

View File

@@ -51,7 +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: [], ownerId: 'test-user', updatedBy: 'test-user',
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
const perActivityActual = [

View File

@@ -43,7 +43,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 0,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
// Simulación SIN recursos en ninguna actividad

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ const mockProcess: Process = {
bpmnXml: '', currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 5000, analysisHorizonYears: 3, automationInvestment: 80000,
createdAt: 0, updatedAt: 0,
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
const mockRoi: RoiResult = {

View File

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

View File

@@ -94,7 +94,7 @@ describe('simulation store — invalidación por cambio en process store', () =>
currency: 'USD', overheadPercentage: 0.2,
annualFrequency: 500, analysisHorizonYears: 2, automationInvestment: 10_000,
createdAt: Date.now(), updatedAt: Date.now(),
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user',
groupId: null, tags: [], ownerId: 'test-user', updatedBy: 'test-user', orgId: null,
}
useProcessStore.setState({ currentProcess: proc })