feat(sprint-5/etapa-4): procesos por recurso + campo empresa en perfil

Feature A — Ver procesos asociados a un recurso:
- resource-repo.ts: getProcessCountsPerResource() (una query, N+1 evitado)
- resource-repo.ts: getProcessesForResource() (on-demand al expandir)
- ResourceCatalogPage: carga conteos en paralelo, columna "Procesos" con
  badge naranja clicable, fila expandida con links a /workspace/:id

Feature B — Campo empresa en perfil:
- 013_add_company_to_users.sql: ALTER TABLE users ADD COLUMN IF NOT EXISTS company text
- AuthUser: agrega company? opcional
- SupabaseAuthService: lee company y name de tabla users en onAuthStateChange
- AuthContext: agrega updateUser() para reflejar cambios sin recargar
- ProfileSheet.tsx: Sheet con nombre/empresa editables, email read-only
- AppHeader: muestra empresa en dropdown + item "Editar perfil"

528 tests Vitest verdes. Build limpio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 19:56:13 -03:00
parent e8a4a1b3e0
commit 6d14e64f33
8 changed files with 354 additions and 117 deletions

View File

@@ -56,6 +56,48 @@ export const supabaseResourceRepo = {
return (data ?? []).map((row) => fromRowWithCreator(row as Record<string, unknown>))
},
// Devuelve un map resourceId → cantidad de procesos DISTINTOS que usan ese recurso.
// Una sola query para evitar N+1; agregación client-side con Set.
async getProcessCountsPerResource(): Promise<Record<string, number>> {
const { data, error } = await supabase
.from('activity_resource_assignments')
.select('resource_id, activity:activities!inner(process_id)')
if (error) throw error
if (!data) return {}
const processSets: Record<string, Set<string>> = {}
for (const row of data) {
const resourceId = row.resource_id as string
const processId = (row.activity as unknown 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])
)
},
// Devuelve los procesos distintos que usan el recurso (llamada on-demand al expandir fila).
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 []
const seen = new Set<string>()
const result: { id: string; name: string }[] = []
for (const row of data) {
const process = (row.activity as unknown as { process: { id: string; name: string } }).process
if (!seen.has(process.id)) {
seen.add(process.id)
result.push({ id: process.id, name: process.name })
}
}
return result
},
// No elimina si el recurso tiene asignaciones activas en activity_resource_assignments
async delete(resourceId: string): Promise<void> {
const { count, error: countError } = await supabase