diff --git a/sprints/sprint-4/ETAPA_7B_PROMPT.md b/sprints/sprint-4/ETAPA_7B_PROMPT.md new file mode 100644 index 0000000..d3a1a33 --- /dev/null +++ b/sprints/sprint-4/ETAPA_7B_PROMPT.md @@ -0,0 +1,158 @@ +# 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 + +--- + +## Orden de ejecución + +1. Fix 3 — `SupabaseAuthService.signInWithGoogle()` (1 línea, más fácil de verificar primero) +2. Fix 2 — función `formatCostPerHour` con precisión dinámica + aplicar en todos los contextos +3. Fix 1 — query param en ResourcesPanel + banner + botón en ResourceCatalogPage +4. `npm run build` — debe pasar sin errores TypeScript +5. `npm run test` — debe mantener 521+ tests verdes +6. Commit: `fix(sprint-4/etapa-7b): selector cuenta Google + redondeo costo/hora + contexto catálogo` + +--- + +## 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 +- [ ] Build limpio sin errores TypeScript +- [ ] 521+ tests verdes +- [ ] **Validación visual del director** antes del OK formal diff --git a/src/auth/SupabaseAuthService.ts b/src/auth/SupabaseAuthService.ts index e444a57..2ac358f 100644 --- a/src/auth/SupabaseAuthService.ts +++ b/src/auth/SupabaseAuthService.ts @@ -5,7 +5,12 @@ export class SupabaseAuthService implements AuthService { async signInWithGoogle(): Promise { const { error } = await supabase.auth.signInWithOAuth({ provider: 'google', - options: { redirectTo: window.location.origin }, + options: { + redirectTo: window.location.origin, + queryParams: { + prompt: 'select_account', // fuerza el selector de cuentas de Google, incluso con sesión activa + }, + }, }) if (error) throw error } diff --git a/src/features/resources/ResourceCatalogPage.tsx b/src/features/resources/ResourceCatalogPage.tsx index c9563b7..361be0c 100644 --- a/src/features/resources/ResourceCatalogPage.tsx +++ b/src/features/resources/ResourceCatalogPage.tsx @@ -1,20 +1,27 @@ import { useEffect, useState } from 'react' -import { Users2, Search, Plus, Pencil, Trash2 } from 'lucide-react' +import { useNavigate, useSearch } from '@tanstack/react-router' +import { Users2, Search, Plus, Pencil, Trash2, ArrowLeft, CornerUpLeft } from 'lucide-react' import { AppHeader } from '@/components/AppHeader' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table' +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' import { useAuth } from '@/auth/AuthContext' import { supabaseResourceRepo, type ResourceWithCreator } from '@/persistence/supabase/resource-repo' -import { formatCurrency } from '@/lib/format' +import { supabaseProcessRepo } from '@/persistence/supabase/process-repo' +import { formatCostPerHour } from '@/lib/format' import { RESOURCE_TYPE_LABELS, RESOURCE_TYPE_STYLES, ResourceTypeAvatar } from './resource-display' import { ResourceFormSheet } from './ResourceFormSheet' import type { Resource, ResourceType } from '@/domain/types' export function ResourceCatalogPage() { const { user } = useAuth() + const navigate = useNavigate() + const search_ = useSearch({ from: '/recursos' }) + const fromProcess = (search_ as { fromProcess?: string }).fromProcess + const [fromProcessName, setFromProcessName] = useState(null) const [resources, setResources] = useState([]) const [isLoading, setIsLoading] = useState(true) const [search, setSearch] = useState('') @@ -24,6 +31,15 @@ export function ResourceCatalogPage() { const [deleteError, setDeleteError] = useState(null) const [confirmingDeleteId, setConfirmingDeleteId] = useState(null) + useEffect(() => { + if (!fromProcess) return + supabaseProcessRepo.getById(fromProcess).then((p) => setFromProcessName(p?.name ?? null)) + }, [fromProcess]) + + function backToProcess() { + if (fromProcess) navigate({ to: '/workspace/$processId', params: { processId: fromProcess } }) + } + async function load() { setIsLoading(true) try { @@ -92,6 +108,31 @@ export function ResourceCatalogPage() { + {fromProcess && ( +
+
+ +
+

+ Estás viendo el catálogo desde el proceso {fromProcessName ? `"${fromProcessName}"` : ''} +

+

+ Seleccioná un recurso y usalo en una actividad al volver al workspace. +

+
+
+ +
+ )} + {deleteError && (
{deleteError} @@ -164,7 +205,7 @@ export function ResourceCatalogPage() { {RESOURCE_TYPE_LABELS[resource.type]} - {formatCurrency(resource.costPerHour)} + {formatCostPerHour(resource.costPerHour)} {resource.creatorName} {confirmingDeleteId === resource.id ? ( @@ -179,6 +220,18 @@ export function ResourceCatalogPage() {
) : (
+ {fromProcess && ( + + + + + + Volver al workspace para asignar este recurso a una actividad + + + )} diff --git a/src/features/workspace/ActivityPanel.tsx b/src/features/workspace/ActivityPanel.tsx index 082c15c..413e2dd 100644 --- a/src/features/workspace/ActivityPanel.tsx +++ b/src/features/workspace/ActivityPanel.tsx @@ -11,7 +11,7 @@ import { Switch } from '@/components/ui/switch' import { Separator } from '@/components/ui/separator' import { useProcessStore } from '@/store/process-store' import { useAuth } from '@/auth/AuthContext' -import { formatCurrency } from '@/lib/format' +import { formatCurrency, formatCostPerHour } from '@/lib/format' import type { Activity } from '@/domain/types' interface ActivityPanelProps { @@ -254,7 +254,7 @@ export function ActivityPanel({ selectedElementId }: ActivityPanelProps) {

{resource.name}

-

{formatCurrency(resource.costPerHour)}/h

+

{formatCostPerHour(resource.costPerHour)}/h