import React, { useEffect, useState } from 'react'; import { fetchUsers, createUser, updateUser, deleteUser, getCurrentUser } from '../api/users.ts'; import { useNotification } from '../context/NotificationContext'; const initialForm = { username: '', email: '', first_name: '', last_name: '', password: '', confirmPassword: '', rfc: '', // Agregar campo RFC para importadores }; export default function Users() { // Inyectar animaciones solo una vez en el cliente useEffect(() => { if (typeof window !== 'undefined' && !document.getElementById('users-animations')) { const style = document.createElement('style'); style.id = 'users-animations'; style.innerHTML = ` @keyframes fadeInUpUsers { 0% { opacity: 0; transform: translateY(32px); } 100% { opacity: 1; transform: translateY(0); } } .fade-in-up-users { animation: fadeInUpUsers 0.7s cubic-bezier(0.22, 1, 0.36, 1) both; } `; document.head.appendChild(style); } }, []); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [form, setForm] = useState(initialForm); const [editingId, setEditingId] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [createType, setCreateType] = useState('agente'); // 'agente' o 'importador' const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [userToDelete, setUserToDelete] = useState(null); const [submitting, setSubmitting] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); // all, active, inactive, admin const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const { showMessage } = useNotification(); // Estados para validación de contraseña const [passwordValidation, setPasswordValidation] = useState({ length: false, uppercase: false, lowercase: false, number: false, special: false, }); const [showPasswordValidation, setShowPasswordValidation] = useState(false); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [passwordsMatch, setPasswordsMatch] = useState(true); const [showPasswordMatchValidation, setShowPasswordMatchValidation] = useState(false); const loadUsers = () => { setLoading(true); fetchUsers() .then(data => { setUsers(data); setLoading(false); }) .catch(err => { console.error('Error loading users:', err); setError('Error al cargar usuarios'); setLoading(false); }); }; useEffect(() => { // Si no hay token, limpiar y redirigir a login inmediatamente const accessToken = localStorage.getItem('access'); if (!accessToken) { localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('username'); localStorage.removeItem('user_email'); localStorage.removeItem('user_id'); localStorage.removeItem('user_groups'); localStorage.removeItem('user_first_name'); localStorage.removeItem('user_last_name'); localStorage.removeItem('user_is_importador'); window.location.href = '/login'; return; } loadUsers(); // Siempre sincroniza la información del usuario autenticado en localStorage getCurrentUser() .then(data => { console.log('Respuesta de /api/users/me/:', data); if (data && data.username) { localStorage.setItem('username', data.username); if (data.email) localStorage.setItem('user_email', data.email); if (data.id) localStorage.setItem('user_id', String(data.id)); if (data.groups) localStorage.setItem('user_groups', JSON.stringify(data.groups)); if (data.first_name) localStorage.setItem('user_first_name', data.first_name); if (data.last_name) localStorage.setItem('user_last_name', data.last_name); if (typeof data.is_importador !== 'undefined') localStorage.setItem('user_is_importador', String(data.is_importador)); console.log('Guardado en localStorage: username', data.username); } else { console.log('No se encontró username en la respuesta'); } }) .catch((err) => { console.log('Error en fetch /api/users/me/', err); }); // eslint-disable-next-line }, [showMessage]); // Debounce para el término de búsqueda useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 300); return () => clearTimeout(timer); }, [searchTerm]); const handleChange = e => { const { name, value } = e.target; setForm({ ...form, [name]: value }); // Validar contraseña en tiempo real if (name === 'password') { validatePassword(value); // Validar coincidencia si ya hay confirmación if (form.confirmPassword) { validatePasswordMatch(value, form.confirmPassword); } } // Validar coincidencia de contraseñas if (name === 'confirmPassword') { validatePasswordMatch(form.password, value); } }; // Función para validar contraseña const validatePassword = (password) => { const validation = { length: password.length >= 8, uppercase: /[A-Z]/.test(password), lowercase: /[a-z]/.test(password), number: /\d/.test(password), special: /[!@#$%^&*(),.?":{}|<>]/.test(password), }; setPasswordValidation(validation); setShowPasswordValidation(password.length > 0); }; // Función para validar coincidencia de contraseñas const validatePasswordMatch = (password, confirmPassword) => { const match = password === confirmPassword; setPasswordsMatch(match); setShowPasswordMatchValidation(confirmPassword.length > 0); }; // Verificar si la contraseña es válida const isPasswordValid = () => { return Object.values(passwordValidation).every(valid => valid); }; // Verificar si el formulario es válido para envío const isFormValid = () => { return isPasswordValid() && passwordsMatch && form.password.length > 0 && form.confirmPassword.length > 0; }; // (Eliminado duplicado de handleSubmit, ahora solo existe la versión unificada más abajo) // Submit para crear usuario (agente o importador) const handleSubmit = async e => { e.preventDefault(); setSubmitting(true); try { if (editingId) { await updateUser(editingId, form); showMessage('Usuario actualizado exitosamente', 'success'); setShowEditModal(false); } else { const groups = createType === 'importador' ? [3, 5] : [4, 3]; const extra = createType === 'importador' ? { is_importador: true } : {}; await createUser({ ...form, groups, ...extra }); showMessage(createType === 'importador' ? 'Importador creado exitosamente' : 'Usuario creado exitosamente', 'success'); setShowCreateModal(false); } setForm(initialForm); setEditingId(null); loadUsers(); } catch (err) { showMessage(err.message, 'error'); } finally { setSubmitting(false); } }; const handleDeleteConfirm = async () => { if (!userToDelete) return; setSubmitting(true); try { await deleteUser(userToDelete.id); showMessage('Usuario eliminado exitosamente', 'success'); setShowDeleteModal(false); setUserToDelete(null); loadUsers(); } catch (err) { showMessage(err.message, 'error'); } finally { setSubmitting(false); } }; const handleCancel = () => { setForm(initialForm); setEditingId(null); setShowCreateModal(false); setShowEditModal(false); setShowDeleteModal(false); setUserToDelete(null); // Resetear estados de validación de contraseña setPasswordValidation({ length: false, uppercase: false, lowercase: false, number: false, special: false, }); setShowPasswordValidation(false); setShowPassword(false); setShowConfirmPassword(false); setPasswordsMatch(true); setShowPasswordMatchValidation(false); }; // Función para normalizar texto (quitar acentos y espacios extra) const normalizeText = (text) => { if (!text) return ''; return text .toString() .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") // Quitar acentos .replace(/\s+/g, ' ') // Espacios múltiples a uno solo .trim(); }; // Función mejorada para filtrar usuarios const filteredUsers = users.filter(user => { // Filtro por término de búsqueda let matchesSearch = true; if (debouncedSearchTerm.trim()) { const searchWords = debouncedSearchTerm.trim().split(/\s+/); const userSearchableText = normalizeText(` ${user.username} ${user.email} ${user.first_name || ''} ${user.last_name || ''} ${user.first_name && user.last_name ? `${user.first_name} ${user.last_name}` : ''} ${user.is_staff || user.is_superuser ? 'admin administrador staff superuser' : 'usuario user'} ${user.is_active !== false ? 'activo active' : 'inactivo inactive'} `); matchesSearch = searchWords.every(word => userSearchableText.includes(normalizeText(word)) ); } // Filtro por estado let matchesStatus = true; switch (statusFilter) { case 'active': matchesStatus = user.is_active !== false; break; case 'inactive': matchesStatus = user.is_active === false; break; case 'admin': matchesStatus = Array.isArray(user.groups) && user.groups.includes(1) && user.groups.includes(3) && user.groups.includes(4); break; case 'user': matchesStatus = !user.is_staff && !user.is_superuser; break; case 'importador': matchesStatus = (Array.isArray(user.groups) && user.groups.includes(3) && user.groups.includes(6)) || user.is_importador === true; break; default: matchesStatus = true; } return matchesSearch && matchesStatus; }); // Cálculos de paginación const totalUsers = filteredUsers.length; const totalPages = Math.ceil(totalUsers / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const currentUsers = filteredUsers.slice(startIndex, endIndex); // Reset página cuando cambia el filtro useEffect(() => { setCurrentPage(1); }, [debouncedSearchTerm, statusFilter]); const handlePageChange = (page) => { setCurrentPage(page); }; const handleItemsPerPageChange = (newItemsPerPage) => { setItemsPerPage(newItemsPerPage); setCurrentPage(1); // Reset a la primera página }; // Función para resaltar términos de búsqueda const highlightText = (text, searchTerm) => { if (!searchTerm.trim() || !text) return text; const words = searchTerm.trim().split(/\s+/); let highlightedText = String(text); words.forEach(word => { if (word.length > 1) { // Solo resaltar palabras de más de 1 carácter const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); highlightedText = highlightedText.replace(regex, '$1'); } }); return highlightedText; }; // Función para obtener el badge de estado const getStatusBadge = () => { return 'bg-green-100 text-green-800'; }; if (loading) { return (
{[1, 2, 3].map((i) => (
))}
); } if (error) { return (

Error al cargar usuarios

{error}

); } // Cuenta de usuarios verificados y activos const verifiedCount = users.filter(u => u.is_verified === true).length; const activeCount = users.filter(u => u.is_active === true).length; return (
{/* Header Mejorado */}

Usuarios
{users.length} Activos: {activeCount}

Gestiona y supervisa los usuarios registrados en el sistema.

{/* Efecto decorativo de fondo */}
{/* Animación personalizada para el icono */} {/* Stats Cards con animación */}
Total
{users.length}
Activos
{activeCount}
Completos
{users.filter(u => u.first_name && u.last_name).length}
Recientes
{users.filter(u => u.id % 3 === 0).length}
{/* Search and Actions */}
{/* Barra de búsqueda principal */}
setSearchTerm(e.target.value)} title="Busca por: nombre de usuario, email, nombre completo, estado (activo, inactivo, admin)" /> {searchTerm && (
)}
{/* Modal para crear usuario (agente o importador) eliminado */}
{/* Filtros avanzados */}
Filtrar por:
{[ { key: 'all', label: 'Todos', count: users.length }, { key: 'agente', label: 'Agente Aduanal', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(3) && u.groups.includes(4) : false).length }, { key: 'importador', label: 'Importador', count: users.filter(u => (Array.isArray(u.groups) && u.groups.includes(3) && u.groups.includes(6)) || u.is_importador === true).length }, { key: 'admin', label: 'Admin', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(1) && u.groups.includes(3) && u.groups.includes(4) : false).length }, { key: 'developer', label: 'Developer', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(2) && u.groups.includes(3) && u.groups.includes(4) : false).length }, { key: 'inactive', label: 'Inactivos', count: users.filter(u => u.is_active === false).length } ].map(filter => ( ))}
{/* Información de resultados */} {(debouncedSearchTerm || statusFilter !== 'all') && (
Mostrando {totalUsers} de {users.length} usuarios {debouncedSearchTerm && ( para "{debouncedSearchTerm}" )}
)}
{/* Vista responsiva: tabla para desktop, cards para mobile */} {/* Tabla para pantallas grandes */}
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */} {loading ? ( ) : error ? ( ) : currentUsers.length > 0 ? ( <> {currentUsers.map((user, idx) => ( ))} {/* Rellenar con filas vacías si hay menos de 8 */} {currentUsers.length < 8 && !loading && !error && Array.from({ length: 8 - currentUsers.length }).map((_, idx) => ( ))} ) : ( )}
Usuario Email Estado Nombre Completo Acciones
Cargando usuarios...
Error: {error}
{user.profile_picture ? ( Avatar ) : (
)}
ID: {user.id}
{user.is_active !== false ? ( Activo ) : ( Inactivo )} {(user.is_staff || user.is_superuser) && ( Admin )}
Sin nombre' }} />
 

No hay usuarios

Aún no tienes usuarios registrados.

{/* Cards para pantallas pequeñas */}
{loading ? (
Cargando usuarios...
) : error ? (
Error: {error}
) : currentUsers.length > 0 ? (
{currentUsers.map((user, idx) => (
{user.profile_picture ? ( Avatar ) : (
)}

ID: {user.id}

Estado:
{user.is_active !== false ? ( Activo ) : ( Inactivo )} {(user.is_staff || user.is_superuser) && ( Admin )}
Nombre:

Sin nombre' }} />

))}
) : (

No hay usuarios

Aún no tienes usuarios registrados.

)}
{/* Paginación mejorada */} {totalPages > 1 && (
Mostrando {((currentPage - 1) * itemsPerPage) + 1} a{' '} {Math.min(currentPage * itemsPerPage, filteredUsers.length)} de{' '} {filteredUsers.length} usuarios
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => ( ))}
)}
{/* Empty state */} {currentUsers.length === 0 && !loading && (
{debouncedSearchTerm || statusFilter !== 'all' ? ( // Estado cuando hay filtros aplicados <>

No se encontraron usuarios

{debouncedSearchTerm && statusFilter !== 'all' ? ( <>No hay usuarios {statusFilter === 'active' ? 'activos' : statusFilter === 'inactive' ? 'inactivos' : statusFilter === 'admin' ? 'administradores' : 'regulares'} que coincidan con "{debouncedSearchTerm}" ) : debouncedSearchTerm ? ( <>No hay usuarios que coincidan con "{debouncedSearchTerm}" ) : ( <>No hay usuarios {statusFilter === 'active' ? 'activos' : statusFilter === 'inactive' ? 'inactivos' : statusFilter === 'admin' ? 'administradores' : 'regulares'} )}

) : ( // Estado cuando no hay usuarios en absoluto <>

Aún no hay usuarios

Comienza creando tu primer usuario para gestionar el acceso al sistema.

)}
)} {/* Modales */} {/* Modal Crear Usuario */} {showCreateModal && (
{/* Header formal con gradiente */}
{createType === 'importador' ? ( ) : ( )}

{createType === 'importador' ? 'Registro de Nuevo Importador' : 'Registro de Nuevo Agente'}

{createType === 'importador' ? 'Sistema de Gestión de Importadores' : 'Sistema de Gestión de Agentes Aduanales'}

{/* Contenido del formulario */}
{/* Sección de Información Personal */}

Información Personal

Datos de identificación del usuario

{/* Sección de RFC (solo para importador) */} {createType === 'importador' && (

Información Fiscal

Datos fiscales del importador

Formato: 12-13 caracteres (ABCD123456ABC)

)} {/* Sección de Seguridad */}

Credenciales de Acceso

Configuración de seguridad de la cuenta

{/* Indicadores de validación de contraseña */} {showPasswordValidation && (
Requisitos de contraseña: {isPasswordValid() && (
Válida
)}
Mínimo 8 caracteres
Una letra mayúscula
Una letra minúscula
Un número
Un carácter especial (!@#$%^&*(),.?":{}|<>)
)} {/* Campo de confirmación de contraseña */}
0 ? 'border-green-300 focus:ring-green-500 focus:border-green-500' : showPasswordMatchValidation && !passwordsMatch ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : 'border-slate-300 focus:ring-red-500 focus:border-red-500' }`} placeholder="Confirme la contraseña" />
{/* Indicador de coincidencia de contraseñas */} {showPasswordMatchValidation && (
{passwordsMatch ? 'Las contraseñas coinciden' : 'Las contraseñas no coinciden'}
)}
{/* Botones de acción */}
)} {/* Modal Editar Usuario */} {showEditModal && (

Editar Usuario

Deja este campo vacío si no deseas cambiar la contraseña

)} {/* Modal Eliminar Usuario */} {showDeleteModal && userToDelete && (

Eliminar Usuario

¿Estás seguro que deseas eliminar al usuario {userToDelete.username}?

{userToDelete.profile_picture ? ( Avatar ) : (
)}

{userToDelete.username}

{userToDelete.email}

{(userToDelete.first_name || userToDelete.last_name) && (

{`${userToDelete.first_name} ${userToDelete.last_name}`.trim()}

)}

Esta acción no se puede deshacer.

)}
); }