# 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: ```typescript // 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: 1. **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 `fromProcess` en el store o con una query puntual a Supabase - El botón "← Volver al proceso" navega de vuelta a `/workspace/:processId` 2. **Botón "← Usar en proceso" en cada fila de la tabla** (columna Acciones, junto a editar/eliminar): - Solo visible cuando `fromProcess` está 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" **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`): ```typescript 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`: ```typescript async signInWithGoogle(): Promise { 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()`: ```typescript // 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 1. Fix 3 — `SupabaseAuthService.signInWithGoogle()` (1 línea) 2. Fix 4 — `.maybeSingle()` en simulation-repo + revisar re-fetch loop 3. Fix 2 — función `formatCostPerHour` con precisión dinámica + aplicar en todos los contextos 4. Fix 1 — query param en ResourcesPanel + banner + botón en ResourceCatalogPage 5. `npm run build` — debe pasar sin errores TypeScript 6. `npm run test` — debe mantener 521+ tests verdes 7. 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.037` se muestra como `$0.037`, no `$0.04` - [ ] Recurso con costo `45` se 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 `/recursos` directamente (sin `fromProcess`) 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