import React, { useEffect, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { createUser, updateUser } from '../api/users.ts'; import { fetchRoles, fetchUserRoles, assignUserRole, revokeUserRole } from '../api/rbac'; import { useNotification } from '../context/NotificationContext'; const initialForm = { username: '', email: '', first_name: '', last_name: '', password: '', confirmPassword: '', rfc: [], userType: 'agente', // 'agente' | 'importador' is_active: true, }; export default function UserForm() { const { id } = useParams(); const isEditing = Boolean(id); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { showMessage } = useNotification(); const initialType = searchParams.get('type') === 'importador' ? 'importador' : 'agente'; const [form, setForm] = useState({ ...initialForm, userType: initialType }); const [importadores, setImportadores] = useState([]); const [submitting, setSubmitting] = useState(false); const [loadingUser, setLoadingUser] = useState(isEditing); // RBAC: roles disponibles de la organización const [availableRoles, setAvailableRoles] = useState([]); const [loadingRoles, setLoadingRoles] = useState(true); // IDs de roles seleccionados en el formulario const [selectedRoleIds, setSelectedRoleIds] = useState([]); // Asignaciones actuales del usuario (para calcular diff en edición): [{ id, role }] const [currentUserRoles, setCurrentUserRoles] = useState([]); // 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); // Inyectar animaciones 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; } @keyframes bounce-slow { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } .animate-bounce-slow { animation: bounce-slow 2.2s infinite; } `; document.head.appendChild(style); } }, []); // Cargar roles disponibles de la organización useEffect(() => { fetchRoles() .then(data => { const list = Array.isArray(data) ? data : (data?.results ?? []); setAvailableRoles(list); // Preselección por tipo inicial solo en creación if (!isEditing) { setSelectedRoleIds(getDefaultRoleIds(list, initialType)); } }) .catch(err => { showMessage(err.message || 'Error al cargar los perfiles disponibles', 'error'); setAvailableRoles([]); }) .finally(() => setLoadingRoles(false)); }, []); // Cargar importadores useEffect(() => { const access = localStorage.getItem('access'); if (!access) { const _hub = import.meta.env.VITE_HUB_URL || 'http://localhost:3001'; window.location.href = `${_hub}/login?return_to=${encodeURIComponent(window.location.origin + '/auth/sso')}`; return; } fetch(`${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, { headers: { Authorization: `Bearer ${access}` }, }) .then(r => { if (!r.ok) throw new Error(`Error ${r.status} al cargar importadores`); return r.json(); }) .then(data => setImportadores(Array.isArray(data) ? data : [])) .catch(err => { showMessage(err.message || 'Error al cargar el catálogo de importadores', 'error'); setImportadores([]); }); }, []); // Cargar datos del usuario si es edición useEffect(() => { if (!isEditing) return; const access = localStorage.getItem('access'); Promise.all([ fetch(`${import.meta.env.VITE_EFC_API_URL}/user/users/${id}/`, { headers: { Authorization: `Bearer ${access}` }, }).then(r => r.json()), fetchUserRoles(id).catch(() => []), ]) .then(([data, userRoles]) => { const isImportador = data.is_importador === true; setForm({ username: data.username || '', email: data.email || '', first_name: data.first_name || '', last_name: data.last_name || '', password: '', confirmPassword: '', rfc: Array.isArray(data.rfc) ? data.rfc : (data.rfc ? [data.rfc] : []), userType: isImportador ? 'importador' : 'agente', is_active: data.is_active !== false, }); // user-roles respuesta: [{ id, user: {...}, role: { id, nombre, ... }, created_at }] const normalized = (Array.isArray(userRoles) ? userRoles : (userRoles?.results ?? [])) .map(ur => ({ id: ur.id, role: typeof ur.role === 'object' ? ur.role?.id : ur.role, })) .filter(ur => ur.role); setCurrentUserRoles(normalized); setSelectedRoleIds(normalized.map(ur => ur.role)); setLoadingUser(false); }) .catch(() => { showMessage('Error al cargar datos del usuario', 'error'); setLoadingUser(false); }); }, [id, isEditing, showMessage]); // Preseleccionar roles por defecto según tipo de usuario function getDefaultRoleIds(roles, type) { const keyword = type === 'importador' ? 'importador' : 'agente'; const matches = roles .filter(r => r.nombre?.toLowerCase().includes(keyword)) .map(r => r.id); return matches; } const validatePassword = (password) => { const v = { length: password.length >= 8, uppercase: /[A-Z]/.test(password), lowercase: /[a-z]/.test(password), number: /\d/.test(password), special: /[!@#$%^&*(),.?":{}|<>]/.test(password), }; setPasswordValidation(v); setShowPasswordValidation(password.length > 0); }; const validatePasswordMatch = (password, confirm) => { setPasswordsMatch(password === confirm); setShowPasswordMatchValidation(confirm.length > 0); }; const isPasswordValid = () => Object.values(passwordValidation).every(Boolean); const isFormValid = () => { if (!isEditing) { return isPasswordValid() && passwordsMatch && form.password.length > 0 && form.confirmPassword.length > 0; } if (form.password.length > 0) { return isPasswordValid() && passwordsMatch && form.confirmPassword.length > 0; } return true; }; const handleChange = (e) => { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); if (name === 'password') { validatePassword(value); if (form.confirmPassword) validatePasswordMatch(value, form.confirmPassword); } if (name === 'confirmPassword') validatePasswordMatch(form.password, value); }; const handleUserTypeChange = (type) => { setForm(prev => ({ ...prev, userType: type, rfc: type === 'agente' ? [] : prev.rfc, })); // Preseleccionar roles según tipo setSelectedRoleIds(getDefaultRoleIds(availableRoles, type)); }; const handleRoleToggle = (roleId) => { setSelectedRoleIds(prev => prev.includes(roleId) ? prev.filter(id => id !== roleId) : [...prev, roleId] ); }; const handleRfcToggle = (rfc) => { setForm(prev => { const current = Array.isArray(prev.rfc) ? prev.rfc : []; const next = current.includes(rfc) ? current.filter(r => r !== rfc) : [...current, rfc]; return { ...prev, rfc: next }; }); }; // Sincronizar roles: calcular diff y llamar assign/revoke const syncRoles = async (userId) => { const currentRoleIds = currentUserRoles.map(ur => ur.role); const toAdd = selectedRoleIds.filter(id => !currentRoleIds.includes(id)); const toRemove = currentUserRoles.filter(ur => !selectedRoleIds.includes(ur.role)); await Promise.all([ ...toAdd.map(roleId => assignUserRole(userId, roleId).catch(() => null)), ...toRemove.map(ur => revokeUserRole(ur.id).catch(() => null)), ]); }; const handleSubmit = async (e) => { e.preventDefault(); if (!isFormValid()) return; setSubmitting(true); try { const payload = { username: form.username, email: form.email, first_name: form.first_name, last_name: form.last_name, is_importador: form.userType === 'importador', is_active: form.is_active, }; if (form.userType === 'importador') { payload.rfc = Array.isArray(form.rfc) ? form.rfc : []; } if (form.password) payload.password = form.password; if (isEditing) { await updateUser(id, payload); await syncRoles(id); showMessage('Usuario actualizado exitosamente', 'success'); } else { const newUser = await createUser(payload); const newUserId = newUser?.id; if (newUserId && selectedRoleIds.length > 0) { await Promise.all( selectedRoleIds.map(roleId => assignUserRole(newUserId, roleId).catch(() => null)) ); } showMessage('Usuario creado exitosamente', 'success'); } navigate('/users'); } catch (err) { showMessage(err.message, 'error'); } finally { setSubmitting(false); } }; const isImportador = form.userType === 'importador'; if (loadingUser) { return (
{isEditing ? 'Modifica los datos del usuario seleccionado' : 'Registro en el Sistema de Gestión de Usuarios'}