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:
91
src/components/ProfileSheet.tsx
Normal file
91
src/components/ProfileSheet.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Check } from 'lucide-react'
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { useAuth } from '@/auth/AuthContext'
|
||||
|
||||
interface ProfileSheetProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
||||
const { user, updateUser } = useAuth()
|
||||
const [name, setName] = useState('')
|
||||
const [company, setCompany] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && user) {
|
||||
setName(user.name)
|
||||
setCompany(user.company ?? '')
|
||||
}
|
||||
}, [open, user])
|
||||
|
||||
async function handleSave() {
|
||||
if (!user || !name.trim()) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('users')
|
||||
.update({ name: name.trim(), company: company.trim() || null })
|
||||
.eq('id', user.id)
|
||||
if (error) throw error
|
||||
updateUser({ name: name.trim(), company: company.trim() || undefined })
|
||||
onOpenChange(false)
|
||||
} catch (err) {
|
||||
console.error('Error al guardar perfil:', err)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Editar perfil</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Nombre</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Empresa</Label>
|
||||
<Input
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
placeholder="InQuality"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Email</Label>
|
||||
<Input value={user?.email ?? ''} readOnly className="bg-muted text-muted-foreground cursor-not-allowed" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="mt-6 flex gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!name.trim() || saving} className="gap-1.5">
|
||||
<Check className="h-4 w-4" />
|
||||
Guardar
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user