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
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:
75
CLAUDE.md
75
CLAUDE.md
@@ -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:
|
||||
|
||||
@@ -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`
|
||||
|
||||
133
sprints/sprint-7/ETAPA_7B_PROMPT.md
Normal file
133
sprints/sprint-7/ETAPA_7B_PROMPT.md
Normal 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)
|
||||
@@ -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'
|
||||
|
||||
@@ -91,6 +91,7 @@ export function ImportPage() {
|
||||
tags: [],
|
||||
ownerId: user!.id,
|
||||
updatedBy: user!.id,
|
||||
orgId: null,
|
||||
}
|
||||
|
||||
const activityElements = extractActivityElements(graph)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}</>,
|
||||
}))
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
60
tests/features/workspace/workspace-back-button.test.ts
Normal file
60
tests/features/workspace/workspace-back-button.test.ts
Normal 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('/')
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user