Fix 4 Parte A: .maybeSingle() ya estaba aplicado (lo hiciste vos/el linter). Encontré y arreglé un bug adicional: con data === null (sin simulaciones), el código llamaba fromRow(null) y explotaba al leer row.id. Ahora getLatestByProcess retorna undefined limpiamente cuando no hay filas. Fix 4 Parte B: revisé ProcessCardWithSim en LibraryPage.tsx — ya tiene guard de cancelación y dependencias estables (process.id + acción de Zustand, que es una referencia fija). No encontré un loop real; lo más probable es que "~6 queries" correspondan a 6 procesos distintos disparando una query cada uno (correcto), posiblemente duplicado visualmente en dev por StrictMode (inocuo, no pasa en producción). Fixes 1, 2, 3 ya estaban implementados de antes y siguen verdes. Falta tu validación visual — en particular confirmar que el network tab ya no muestra 406 en simulations.
8.3 KiB
Sprint 4 — Etapa 7B: Tres fixes post-validación visual
Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind. Etapa 7 completada con OK formal. Durante la validación visual del director se detectaron tres issues que entran en esta etapa.
Fix 1 — Catálogo con contexto de proceso (Opción A aprobada)
Problema
Cuando el usuario navega desde el ResourcesPanel del workspace al catálogo (/recursos), no hay forma de volver al proceso de origen ni de indicar que un recurso está disponible para usar en ese proceso.
Solución
Paso 1: ResourcesPanel — pasar contexto en la URL
En src/features/workspace/ResourcesPanel.tsx, el link "Ver catálogo completo" debe incluir el processId del proceso actual como query param:
// Antes:
navigate({ to: '/recursos' })
// Después:
navigate({ to: '/recursos', search: { fromProcess: currentProcess.id } })
Paso 2: ResourceCatalogPage — detectar contexto y mostrar UI de retorno
En src/features/resources/ResourceCatalogPage.tsx:
Leer el query param fromProcess (puede ser undefined si el usuario navegó directamente).
Si fromProcess está presente:
-
Banner contextual en la parte superior de la página (antes de la barra de herramientas), con fondo suave naranja InQ:
[←] Estás viendo el catálogo desde el proceso "[nombre del proceso]" Seleccioná un recurso y usalo en una actividad al volver al workspace. [← Volver al proceso]- El nombre del proceso se resuelve buscando
fromProcessen el store o con una query puntual a Supabase - El botón "← Volver al proceso" navega de vuelta a
/workspace/:processId
- El nombre del proceso se resuelve buscando
-
Botón "← Usar en proceso" en cada fila de la tabla (columna Acciones, junto a editar/eliminar):
- Solo visible cuando
fromProcessestá presente - Al hacer click: navega a
/workspace/:processId - No hace ninguna asignación automática — la asignación sigue siendo en ActivityPanel
- Tooltip: "Volver al workspace para asignar este recurso a una actividad"
- Solo visible cuando
Colores del banner:
- Fondo:
#FEF3E7(naranja InQ muy suave) - Borde izquierdo:
#F59845(naranja InQ) - Texto secundario:
var(--color-text-secondary) - Botón "Volver": estilo secundario (borde, sin relleno)
Si fromProcess no está presente (usuario llegó al catálogo directamente): sin banner, sin botón extra. La página funciona exactamente igual que en Etapa 7.
Fix 2 — Redondeo del costo por hora para valores pequeños
Problema
Un recurso con costo $0.037/h se muestra como $0.04 — pérdida de información para recursos de bajo costo unitario (licencias de software, servicios cloud por hora, etc.).
Solución
Crear o actualizar la función de formateo de costo por hora para que use precisión dinámica. Ubicar en src/lib/format.ts (o donde esté la función formatCurrency / formatCostPerHour):
export function formatCostPerHour(value: number, currency = 'USD'): string {
if (value === 0) return '$0'
// Determinar decimales según magnitud
let decimals: number
if (value >= 100) decimals = 0 // $150/h → $150
else if (value >= 10) decimals = 1 // $12.5/h → $12.5
else if (value >= 1) decimals = 2 // $1.23/h → $1.23
else if (value >= 0.1) decimals = 3 // $0.123/h → $0.123
else decimals = 4 // $0.0123/h → $0.0123
return `$${value.toFixed(decimals)}`
}
Aplicar esta función en todos los lugares donde se muestra el costo por hora de un recurso:
- Tabla del catálogo
/recursos - ResourcesPanel (lista de recursos usados en el proceso)
- ActivityPanel (selector de recursos y fila de recurso asignado)
- Cualquier otro lugar donde aparezca
costPerHour
Verificación: ingresar un recurso con costo 0.037 y confirmar que se muestra $0.037 (no $0.04).
Fix 3 — Google OAuth: forzar selector de cuenta
Problema
Al hacer click en "Iniciar sesión con Google", el navegador usa directamente la sesión anterior sin mostrar el selector de cuentas de Google. Impide cambiar de cuenta o seleccionar entre múltiples cuentas del navegador.
Solución
En src/auth/SupabaseAuthService.ts, en el método signInWithGoogle(), agregar prompt: 'select_account' en los queryParams:
async signInWithGoogle(): Promise<void> {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
prompt: 'select_account', // 🆕 fuerza el selector de cuentas de Google
},
},
})
if (error) throw error
}
prompt: 'select_account' es un parámetro estándar de OAuth 2.0 / OpenID Connect que instruye a Google a siempre mostrar el selector de cuentas, incluso si hay una sesión activa.
Qué NO entra en esta etapa
- Asignación automática de recursos desde el catálogo (eso sería Opción B — diferida)
- Cambios en PDF, simulación o workspace salvo los indicados
- Ningún cambio de schema en Supabase
Fix 4 — simulations: 406 por .single() sin filas + re-fetch en loop
Problema
En el network tab se observan múltiples requests con 406 Not Acceptable:
GET simulations?select=*&process_id=eq.{id}&order=executed_at.desc&limit=1 → 406
Causa: supabase-js v2 .single() envía Accept: application/vnd.pgrst.object+json, que exige exactamente 1 fila. Si el proceso no tiene simulaciones aún (0 filas), PostgREST responde 406. El mismo query aparece ~6 veces para los mismos processId — señal de un useEffect re-fetching por dependencia incorrecta.
Solución
Parte A — Fix del 406: en src/persistence/supabase/simulation-repo.ts, en el método que busca la última simulación por proceso, cambiar .single() por .maybeSingle():
// Antes:
const { data, error } = await supabase
.from('simulations')
.select('*')
.eq('process_id', processId)
.order('executed_at', { ascending: false })
.limit(1)
.single() // ← lanza 406 cuando no hay filas
// Después:
const { data, error } = await supabase
.from('simulations')
.select('*')
.eq('process_id', processId)
.order('executed_at', { ascending: false })
.limit(1)
.maybeSingle() // ← retorna null sin error cuando no hay filas
El caller debe manejar data === null como "sin simulación" (no como error).
Parte B — Fix del re-fetch en loop: localizar el useEffect que dispara esta query (probablemente en LibraryPage.tsx o en un hook de simulación). Revisar que sus dependencias no incluyan un objeto o array que se re-crea en cada render (patrón clásico de loop infinito). Si el array de dependencias contiene processes (que es un array nuevo en cada render del store), estabilizarlo con useMemo o extraer solo los IDs que realmente cambian.
Orden de ejecución
- Fix 3 —
SupabaseAuthService.signInWithGoogle()(1 línea) - Fix 4 —
.maybeSingle()en simulation-repo + revisar re-fetch loop - Fix 2 — función
formatCostPerHourcon precisión dinámica + aplicar en todos los contextos - Fix 1 — query param en ResourcesPanel + banner + botón en ResourceCatalogPage
npm run build— debe pasar sin errores TypeScriptnpm run test— debe mantener 521+ tests verdes- Commit:
fix(sprint-4/etapa-7b): selector cuenta Google + redondeo costo/hora + contexto catálogo + simulations maybeSingle
Criterios de validación
- Login muestra el selector de cuentas de Google (no auto-login)
- Recurso con costo
0.037se muestra como$0.037, no$0.04 - Recurso con costo
45se muestra como$45, no$45.00(sin decimales innecesarios) - Navegar desde ResourcesPanel → catálogo muestra el banner con nombre del proceso
- Botón "← Usar en proceso" aparece en cada fila cuando hay contexto de proceso
- Click en "← Usar en proceso" navega a
/workspace/:processId - Navegar a
/recursosdirectamente (sinfromProcess) no muestra banner ni botón extra - Network tab sin 406 en requests de
simulations - Network tab: el query de simulaciones no se repite en loop para el mismo
processId - Build limpio sin errores TypeScript
- 521+ tests verdes
- Validación visual del director antes del OK formal