import { useEffect, useState } from 'react'; import { Loader2, Trash2, UserPlus, X, Check, AlertCircle, Shield, Eye, Users as UsersIcon } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { toast } from 'sonner'; import { supabase } from '../lib/supabase'; import type { UserRole } from '../types'; interface UserProfileRow { id: string; email: string; role: UserRole; display_name: string | null; created_at: string; } // Toggle para reactivar el borrado de usuarios desde la UI. const SHOW_DELETE_USER = false as boolean; const ROLE_OPTIONS: { value: UserRole; label: string; description: string; icon: typeof Shield }[] = [ { value: 'admin', label: 'Admin', description: 'Acceso total y gestión de usuarios', icon: Shield }, { value: 'internal_staff', label: 'Staff', description: 'Gestión completa de reservas', icon: UsersIcon }, { value: 'external_availability_viewer', label: 'Viewer', description: 'Solo lee disponibilidad', icon: Eye }, ]; const apiUrl = import.meta.env.VITE_API_URL; async function authFetch(path: string, init: RequestInit = {}): Promise { const { data } = await supabase.auth.getSession(); const token = data.session?.access_token; const headers = new Headers(init.headers); headers.set('Content-Type', 'application/json'); if (token) headers.set('Authorization', `Bearer ${token}`); return fetch(`${apiUrl}${path}`, { ...init, headers }); } export function UserManagement() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Invitación deshabilitada por ahora (sin SMTP). El modal y endpoint se conservan. const [inviteOpen, _setInviteOpen] = useState(false); const [savingId, setSavingId] = useState(null); const loadUsers = async () => { setLoading(true); setError(null); try { const res = await authFetch('/api/users'); if (!res.ok) { const body = await res.json().catch(() => ({} as { error?: string })); throw new Error(body?.error ?? `Error ${res.status}`); } const body = await res.json() as { users: UserProfileRow[] }; setUsers(body.users); } catch (err) { setError(err instanceof Error ? err.message : 'Error al cargar usuarios'); } finally { setLoading(false); } }; useEffect(() => { if (!apiUrl) { setError('VITE_API_URL no configurado. La gestión de usuarios necesita el API.'); setLoading(false); return; } loadUsers(); }, []); const handleRoleChange = async (userId: string, newRole: UserRole) => { setSavingId(userId); try { const res = await authFetch(`/api/users/${userId}`, { method: 'PATCH', body: JSON.stringify({ role: newRole }), }); if (!res.ok) { const body = await res.json().catch(() => ({} as { error?: string })); throw new Error(body?.error ?? 'Error al actualizar rol'); } setUsers(prev => prev.map(u => u.id === userId ? { ...u, role: newRole } : u)); toast.success('Rol actualizado'); } catch (err) { toast.error(err instanceof Error ? err.message : 'Error'); } finally { setSavingId(null); } }; // Borrado deshabilitado en UI; se conserva para reactivar. const handleDelete = async (user: UserProfileRow) => { if (!confirm(`¿Eliminar a ${user.email}? Esta acción no se puede deshacer.`)) return; setSavingId(user.id); try { const res = await authFetch(`/api/users/${user.id}`, { method: 'DELETE' }); if (!res.ok) { const body = await res.json().catch(() => ({} as { error?: string })); throw new Error(body?.error ?? 'Error al eliminar'); } setUsers(prev => prev.filter(u => u.id !== user.id)); toast.success('Usuario eliminado'); } catch (err) { toast.error(err instanceof Error ? err.message : 'Error'); } finally { setSavingId(null); } }; return (

Usuarios

Gestiona accesos y roles del equipo. Las altas se hacen desde Supabase.

{error && (
{error}
)} {loading ? (
) : (
{SHOW_DELETE_USER && } {users.length === 0 ? ( ) : users.map(user => ( {SHOW_DELETE_USER && ( )} ))}
Email / Nombre Rol CreadoAcciones
No hay usuarios registrados.
{user.display_name ?? user.email}
{user.display_name && (
{user.email}
)}
{new Date(user.created_at).toLocaleDateString('es-ES')}
)} {inviteOpen && ( _setInviteOpen(false)} onInvited={(newUser) => { setUsers(prev => [newUser, ...prev]); _setInviteOpen(false); toast.success('Invitación enviada'); }} /> )}
); } interface InviteModalProps { onClose: () => void; onInvited: (u: UserProfileRow) => void; } function InviteModal({ onClose, onInvited }: InviteModalProps) { const [email, setEmail] = useState(''); const [role, setRole] = useState('internal_staff'); const [displayName, setDisplayName] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); setError(null); try { const res = await authFetch('/api/users/invite', { method: 'POST', body: JSON.stringify({ email, role, display_name: displayName || undefined }), }); const body = await res.json().catch(() => ({} as { error?: string; user?: UserProfileRow })); if (!res.ok) throw new Error(body?.error ?? 'Error al invitar'); if (body.user) onInvited(body.user); } catch (err) { setError(err instanceof Error ? err.message : 'Error'); } finally { setSubmitting(false); } }; return ( <>

Invitar usuario

Recibirá un correo con magic link para acceder.

setEmail(e.target.value)} placeholder="usuario@ejemplo.com" className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/40" />
setDisplayName(e.target.value)} placeholder="Nombre visible..." className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/40" />
{ROLE_OPTIONS.map(opt => { const Icon = opt.icon; return ( ); })}
{error && (
{error}
)}
); }