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>
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
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>
|
|
)
|
|
}
|