mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:55:23 +02:00
354 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|