From 7ffab83caf82c9bdf26f7d839f4d18debf0493c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Sat, 20 Jun 2026 00:07:46 -0300 Subject: [PATCH] =?UTF-8?q?Feature=20A=20=E2=80=94=20Procesos=20por=20recu?= =?UTF-8?q?rso=20(/recursos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OBSERVATIONS.md | 6 + sprints/sprint-5/ETAPA_4_PROMPT.md | 273 +++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 sprints/sprint-5/ETAPA_4_PROMPT.md diff --git a/OBSERVATIONS.md b/OBSERVATIONS.md index 772e296..10ad485 100644 --- a/OBSERVATIONS.md +++ b/OBSERVATIONS.md @@ -29,6 +29,12 @@ ## Hallazgos resueltos durante el sprint +### [RESOLVED] Etapas 3 + 3B — Heatmap gradiente + toggle + persistencia + +**Estado:** OK formal del director — 2026-06-19. +**Entregado:** gradiente 4 stops (#10b981→#f59e0b→#f97316→#ef4444), modos percentile/minmax, `useHeatmapMode` hook con localStorage, HeatmapModeToggle, leyenda PDF 4 stops, zoom controls movidos a bottom-left. +**Lección operativa:** Dokploy con Autodeploy desactivado requiere `git push origin main` explícito antes de validar en producción. Los commits `a65bec4` y `e8a4a1b` estaban locales sin pushear — Dokploy desplegaba Etapa 2 mientras se validaba Etapa 3. La validación fallida fue contra código desactualizado, no contra bugs reales. + ### [RESOLVED] Etapa 2 — Zoom BPMN fix + controles + skeleton **Estado:** OK formal del director — 2026-06-19. diff --git a/sprints/sprint-5/ETAPA_4_PROMPT.md b/sprints/sprint-5/ETAPA_4_PROMPT.md new file mode 100644 index 0000000..d4e3e9c --- /dev/null +++ b/sprints/sprint-5/ETAPA_4_PROMPT.md @@ -0,0 +1,273 @@ +# Sprint 5 — Etapa 4: Procesos por recurso + campo empresa en perfil + +## Contexto + +InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind + Supabase. +Etapas 1, 2, 3 y 3B completadas con OK formal. Esta etapa agrega dos features pequeñas de producto. + +--- + +## Feature A — Ver procesos asociados a un recurso + +### Contexto actual + +`ResourceCatalogPage` tiene 5 columnas: Recurso, Tipo, Costo/h, Creado por, Acciones. El botón Eliminar ya protege contra borrado de recursos en uso (verifica `activity_resource_assignments`), pero el usuario no puede ver qué procesos usan el recurso antes de intentar eliminarlo. + +### Implementación + +**Paso 1 — Nueva query en `resource-repo.ts`:** + +Agregar un método `getProcessCountsPerResource()` que devuelva un map de `resourceId → cantidad de procesos distintos`. Implementar con una sola query (no una por recurso) para evitar N+1: + +```ts +async getProcessCountsPerResource(): Promise> { + const { data, error } = await supabase + .from('activity_resource_assignments') + .select('resource_id, activity:activities!inner(process_id)') + + if (error) throw error + if (!data) return {} + + // Agregar client-side: contar procesos DISTINTOS por recurso + const processSets: Record> = {} + for (const row of data) { + const resourceId = row.resource_id + const processId = (row.activity as { process_id: string }).process_id + if (!processSets[resourceId]) processSets[resourceId] = new Set() + processSets[resourceId].add(processId) + } + + return Object.fromEntries( + Object.entries(processSets).map(([k, v]) => [k, v.size]) + ) +} +``` + +**Paso 2 — Cargar los conteos en `ResourceCatalogPage`:** + +Al cargar los recursos (`useEffect` de mount), también llamar a `getProcessCountsPerResource()` en paralelo: + +```ts +const [resources, processCounts] = await Promise.all([ + supabaseResourceRepo.getAllWithCreator(), + supabaseResourceRepo.getProcessCountsPerResource(), +]) +``` + +Guardar `processCounts` en estado local del componente. + +**Paso 3 — Nueva columna "Procesos" en la tabla:** + +Agregar columna entre "Creado por" y "Acciones": + +- Header: "Procesos" +- Si `processCounts[resource.id] === 0` o undefined: mostrar "—" en color muted +- Si `processCounts[resource.id] > 0`: mostrar badge clicable con el número + +```tsx +{count > 0 ? ( + +) : ( + +)} +``` + +El color naranja InQ (`text-orange-600`, `bg-orange-50`) es correcto para indicar "hay uso activo". + +**Paso 4 — Expansión inline con lista de procesos:** + +Al hacer click en el badge, expandir una fila adicional debajo con los nombres de los procesos. + +Para esto necesitás una query adicional que devuelva los nombres — la query anterior solo devuelve conteos. Agregar un segundo método en `resource-repo.ts`: + +```ts +async getProcessesForResource(resourceId: string): Promise<{ id: string; name: string }[]> { + const { data, error } = await supabase + .from('activity_resource_assignments') + .select('activity:activities!inner(process:processes!inner(id, name))') + .eq('resource_id', resourceId) + + if (error) throw error + if (!data) return [] + + // Deduplicar por proceso id + const seen = new Set() + const result: { id: string; name: string }[] = [] + for (const row of data) { + const process = (row.activity as any).process + if (!seen.has(process.id)) { + seen.add(process.id) + result.push({ id: process.id, name: process.name }) + } + } + return result +} +``` + +Esta query se llama **on demand** (solo cuando el usuario expande la fila), no al cargar la página. + +La fila expandida muestra: +- Lista de nombres de proceso, cada uno como link a `/workspace/:id` +- Usar `` de TanStack Router + +```tsx +// Fila expandida (colspan completo de la tabla): + + +
+ {processes.map(p => ( + + {p.name} + + ))} +
+ + +``` + +--- + +## Feature B — Campo empresa en perfil de usuario + +### Contexto actual + +`AuthUser` en `/src/auth/types.ts` tiene: `id, email, name, avatarUrl?, platformRole`. Sin `company`. + +`buildAuthUser()` construye el objeto desde OAuth `user_metadata` de Google. No lee la tabla `users` de Supabase directamente. + +El AppHeader dropdown muestra: avatar + nombre + email + botón "Cerrar sesión". + +### Implementación + +**Paso 1 — Migración en Supabase:** + +Crear archivo de migración `supabase/migrations/006_add_company_to_users.sql`: + +```sql +ALTER TABLE users ADD COLUMN IF NOT EXISTS company text; +``` + +Ejecutar en Supabase Studio SQL Editor. Verificar que no rompe datos existentes (la columna es nullable sin default). + +**Paso 2 — Extender `AuthUser`:** + +En `/src/auth/types.ts`: +```ts +export interface AuthUser { + id: string + email: string + name: string + avatarUrl?: string + platformRole: 'platform_admin' | 'member' | 'guest' + company?: string // 🆕 nullable, viene de tabla users +} +``` + +**Paso 3 — Leer `company` desde la tabla `users`:** + +`buildAuthUser()` actualmente solo usa `user_metadata` de OAuth. Para `company` necesitamos leer la tabla `users` de Supabase (que ya existe y se populó en Sprint 4). + +La forma más limpia: después de que el usuario autentique, hacer una query puntual a `users` para obtener el `company`. Este fetch puede ir en el `onAuthStateChange` callback, justo después de construir el `AuthUser` base: + +```ts +// En AuthContext o SupabaseAuthService, después de obtener el usuario de OAuth: +const { data: userRow } = await supabase + .from('users') + .select('company, name') + .eq('id', user.id) + .maybeSingle() + +const authUser: AuthUser = { + ...buildAuthUser(session.user), + company: userRow?.company ?? undefined, + // Si el nombre fue editado en la app (no solo el de Google), usar el de la tabla: + name: userRow?.name ?? buildAuthUser(session.user).name, +} +``` + +**Paso 4 — UI en AppHeader: mostrar empresa** + +En el dropdown del AppHeader, agregar la empresa debajo del email si está definida: + +```tsx +
+ {user.name} + {user.email} + {user.company && ( + {user.company} + )} +
+``` + +**Paso 5 — Edición de perfil:** + +Agregar un item "Editar perfil" en el dropdown (antes de "Cerrar sesión"), que abre un `Sheet` (el mismo componente Sheet de shadcn/ui ya usado en `ResourceFormSheet`): + +El Sheet de perfil tiene: +- **Nombre** — input editable (texto libre) +- **Empresa** — input editable (texto libre, placeholder "InQuality") +- **Email** — solo lectura (viene de Google OAuth, no editable) +- Botón "Guardar" — hace `UPDATE users SET name = ?, company = ? WHERE id = ?` +- Botón "Cancelar" + +Al guardar: actualizar el store de auth local para que el AppHeader refleje el cambio sin recargar. + +```ts +// En el repo o en el store, al guardar perfil: +await supabase + .from('users') + .update({ name, company }) + .eq('id', user.id) + +// Actualizar el AuthUser en el store local: +updateUser({ name, company }) +``` + +`updateUser` debe existir en `AuthContext` o `useAuth` — si no existe, crearlo. + +--- + +## Qué NO entra en esta etapa + +- Cambios en la lógica de simulación o heatmap +- Gestión de roles o permisos de usuario +- Página de perfil separada (el Sheet del dropdown es suficiente) +- Campos adicionales más allá de nombre y empresa + +--- + +## Orden de ejecución + +1. Migración SQL `006_add_company_to_users.sql` — ejecutar en Supabase + crear archivo +2. Extender `AuthUser` con `company?` en `src/auth/types.ts` +3. Modificar `buildAuthUser` / `onAuthStateChange` para leer `company` y `name` de tabla `users` +4. AppHeader: mostrar empresa en dropdown si existe +5. AppHeader: agregar "Editar perfil" → Sheet con nombre/empresa editables +6. `resource-repo.ts`: agregar `getProcessCountsPerResource()` y `getProcessesForResource()` +7. `ResourceCatalogPage`: cargar conteos en paralelo con recursos, agregar columna "Procesos" +8. `ResourceCatalogPage`: expansión inline de fila con lista de procesos linkeados +9. `npm run build` — 0 errores TypeScript +10. `npm run lint` — 0 warnings nuevos +11. `npm run test` — mantener 528+ tests Vitest verdes +12. Commit: `feat(sprint-5/etapa-4): procesos por recurso + campo empresa en perfil` + +--- + +## Criterios de validación + +- [ ] Columna "Procesos" visible en `/recursos` con conteo correcto (verificar con un recurso usado en ≥1 proceso) +- [ ] Recurso sin asignaciones muestra "—" en la columna Procesos +- [ ] Click en el badge expande la lista de procesos con nombres y links funcionales +- [ ] Click en un proceso navega al workspace correcto +- [ ] Migración SQL ejecutada sin errores; `SELECT company FROM users` no falla +- [ ] AppHeader muestra empresa si está definida (probar editando desde el Sheet) +- [ ] Sheet de perfil: guardar nombre y empresa persiste en Supabase y se refleja en el dropdown +- [ ] Email en el Sheet es solo lectura +- [ ] 528+ tests Vitest verdes +- [ ] **Validación visual del director** antes del OK formal