Files
Craze-Data-check/src/components/UserManagementView.tsx
T

354 lines
14 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Users, Search, CheckCircle, XCircle, Trash2, Shield, Loader2, AlertCircle, Clock } from 'lucide-react';
import { AuthSession } from '../lib/auth';
import { safeFetch } from '../lib/supabase';
interface User {
id: string;
email: string;
created_at: string;
status: 'Master' | 'Validated' | 'Pending';
}
interface UserManagementViewProps {
session: AuthSession;
}
export function UserManagementView({ session }: UserManagementViewProps) {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [warning, setWarning] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [processingId, setProcessingId] = useState<string | null>(null);
const [deleteConfirmUser, setDeleteConfirmUser] = useState<User | null>(null);
const fetchUsers = async () => {
setLoading(true);
setError(null);
setWarning(null);
try {
const res = await safeFetch('/api/users-admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action: 'list' })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Failed to fetch users.');
}
const data = await res.json();
setUsers(data.users || []);
setWarning(data.warning || null);
} catch (err: any) {
console.error('[UserManagement] fetch error:', err);
setError(err.message || 'Error fetching user list.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, [session]);
const handleToggleValidation = async (user: User, approve: boolean) => {
setProcessingId(user.id);
setError(null);
setWarning(null);
try {
const res = await safeFetch('/api/users-admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'validate',
targetUserId: user.id,
email: user.email,
validated: approve
})
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Failed to update user approval status.');
}
const data = await res.json().catch(() => ({}));
// Update local state
setUsers(prev => prev.map(u => {
if (u.id === user.id) {
return { ...u, status: approve ? 'Validated' : 'Pending' };
}
return u;
}));
if (data.warning) {
setWarning(data.warning);
}
} catch (err: any) {
setError(err.message || 'Error updating approval status.');
} finally {
setProcessingId(null);
}
};
const handleDeleteUser = async (userId: string) => {
setProcessingId(userId);
setError(null);
setDeleteConfirmUser(null);
try {
const res = await safeFetch('/api/users-admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'delete',
targetUserId: userId
})
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Failed to delete user.');
}
// Remove from local list
setUsers(prev => prev.filter(u => u.id !== userId));
} catch (err: any) {
setError(err.message || 'Error deleting user.');
} finally {
setProcessingId(null);
}
};
const filteredUsers = users.filter(user =>
user.email.toLowerCase().includes(search.toLowerCase())
);
const formatDate = (dateStr: string) => {
try {
return new Date(dateStr).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateStr;
}
};
return (
<div className="flex-1 flex flex-col h-full overflow-hidden bg-[#041021]">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-lg">
<Users className="w-6 h-6 text-blue-400" />
</div>
<div>
<h2 className="text-xl font-bold text-white">Validación de Usuarios</h2>
<p className="text-sm text-slate-400">
Administra los accesos y los registros de usuarios en la plataforma.
</p>
</div>
</div>
<div className="relative">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar por email..."
value={search}
onChange={e => setSearch(e.target.value)}
className="bg-slate-900 border border-slate-800 rounded-md pl-9 pr-4 py-2 text-sm text-white placeholder:text-slate-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 w-64 transition-all"
/>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 overflow-auto p-6">
{error && (
<div className="mb-6 p-4 bg-red-950/30 border border-red-500/30 rounded-lg flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<div className="flex-1 text-sm text-red-300">
<span className="font-semibold">Error:</span> {error}
</div>
<button
onClick={() => setError(null)}
className="text-red-400 hover:text-red-300 text-xs font-semibold px-2 py-1 rounded"
>
Descartar
</button>
</div>
)}
{warning && (
<div className="mb-6 p-4 bg-amber-950/30 border border-amber-500/30 rounded-lg flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
<div className="flex-1 text-sm text-amber-200">
<span className="font-semibold">Aviso:</span> {warning}
</div>
<button
onClick={() => setWarning(null)}
className="text-amber-300 hover:text-amber-200 text-xs font-semibold px-2 py-1 rounded"
>
Descartar
</button>
</div>
)}
{loading ? (
<div className="h-64 flex flex-col items-center justify-center text-slate-400">
<Loader2 className="w-8 h-8 animate-spin text-blue-500 mb-3" />
<p className="text-sm">Cargando lista de usuarios...</p>
</div>
) : filteredUsers.length === 0 ? (
<div className="h-64 flex flex-col items-center justify-center border border-dashed border-slate-800 rounded-xl text-slate-500">
<Users className="w-12 h-12 mb-3 opacity-20" />
{search ? (
<p className="text-sm">No se encontraron usuarios que coincidan con la búsqueda.</p>
) : (
<p className="text-sm">No hay registros de usuarios registrados.</p>
)}
</div>
) : (
<div className="bg-[#08152c] border border-slate-800/60 rounded-xl overflow-hidden shadow-xl">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-800/80 bg-slate-900/30 text-slate-400 text-xs font-semibold uppercase tracking-wider">
<th className="py-4 px-6">Email</th>
<th className="py-4 px-6">Fecha de Registro</th>
<th className="py-4 px-6">Estado</th>
<th className="py-4 px-6 text-right">Acciones</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50 text-slate-300 text-sm">
{filteredUsers.map(user => {
const isProcessing = processingId === user.id;
return (
<tr key={user.id} className="hover:bg-slate-900/10 transition-colors">
<td className="py-4 px-6 font-medium text-white max-w-xs truncate">
{user.email}
</td>
<td className="py-4 px-6 text-slate-400">
{formatDate(user.created_at)}
</td>
<td className="py-4 px-6">
{user.status === 'Master' ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-indigo-500/10 text-indigo-400 border border-indigo-500/20">
<Shield className="w-3.5 h-3.5" />
Administrador Principal
</span>
) : user.status === 'Validated' ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
<CheckCircle className="w-3.5 h-3.5" />
Validado
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-400 border border-amber-500/20">
<Clock className="w-3.5 h-3.5" />
Pendiente Validación
</span>
)}
</td>
<td className="py-4 px-6 text-right">
{user.status === 'Master' ? (
<span className="text-slate-500 text-xs italic">Protegido</span>
) : (
<div className="flex items-center justify-end gap-2">
{user.status === 'Pending' ? (
<button
onClick={() => handleToggleValidation(user, true)}
disabled={isProcessing}
className="inline-flex items-center gap-1 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white disabled:opacity-50 text-xs font-semibold rounded-md transition-colors shadow-sm"
>
{isProcessing ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<CheckCircle className="w-3.5 h-3.5" />
)}
Validar
</button>
) : (
<button
onClick={() => handleToggleValidation(user, false)}
disabled={isProcessing}
className="inline-flex items-center gap-1 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 disabled:opacity-50 text-xs font-semibold rounded-md border border-slate-700 transition-colors"
>
{isProcessing ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<XCircle className="w-3.5 h-3.5" />
)}
Revocar
</button>
)}
<button
onClick={() => setDeleteConfirmUser(user)}
disabled={isProcessing}
className="inline-flex items-center justify-center p-1.5 bg-red-950/30 hover:bg-red-900/50 text-red-400 hover:text-red-300 border border-red-900/20 disabled:opacity-50 rounded-md transition-colors"
title="Eliminar usuario"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
{/* Confirmation Modal */}
{deleteConfirmUser && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-[#08152c] border border-slate-800 rounded-xl max-w-md w-full overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200">
<div className="p-6">
<div className="flex items-center gap-3 text-red-400 mb-4">
<Trash2 className="w-6 h-6 shrink-0" />
<h3 className="text-lg font-bold text-white">¿Eliminar usuario definitivamente?</h3>
</div>
<p className="text-sm text-slate-300 mb-2">
Estás a punto de eliminar la cuenta del usuario:
</p>
<p className="text-sm font-mono bg-slate-900/60 border border-slate-800/80 p-2.5 rounded text-blue-400 break-all mb-4">
{deleteConfirmUser.email}
</p>
<p className="text-xs text-red-400/90 leading-relaxed">
Esta acción no se puede deshacer. Se eliminarán sus accesos y toda su información asociada al servicio de autenticación.
</p>
</div>
<div className="bg-slate-900/50 border-t border-slate-800/60 px-6 py-4 flex items-center justify-end gap-3">
<button
onClick={() => setDeleteConfirmUser(null)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm font-semibold rounded-md transition-colors"
>
Cancelar
</button>
<button
onClick={() => handleDeleteUser(deleteConfirmUser.id)}
className="px-4 py-2 bg-red-600 hover:bg-red-500 text-white text-sm font-semibold rounded-md transition-colors shadow-sm"
>
Eliminar Cuenta
</button>
</div>
</div>
</div>
)}
</div>
);
}