{tituloTipo}
+{data?.mensaje}
+ + {/* Resumen numérico */} + {data?.resumen && ( +{xml.nombre_archivo}
+{xml.tipo_documento}
+Todos los XMLs analizados están correctos
+ )} +Pendientes ({data.pendientes.length})
+COVEs registrados ({data.coves.length})
+Auditoría Global
+La tarea fue enviada al worker. Consulta el resultado cuando haya finalizado.
+{resultado.mensaje}
+ + {/* Grid de cifras */} +| Pedimento | +Documentos faltantes | +Progreso | +
|---|---|---|
| + {item.pedimento} + | +
+
+ {todosLosIds.map((id, j) => (
+
+ {id}
+
+ ))}
+ {todosLosIds.length === 0 && (
+ sin detalle
+ )}
+
+ |
+ + {item.descargados ?? '?'}/{item.total ?? '?'} + | +
| Pedimento | +Error | +
|---|---|
| {item.pedimento} | +{item.error || '—'} | +
{previewError}
|
-
+
+ |
{/* Botón EDoc */}
) : previewType === 'pdf' ? (
-
-
) : previewType === 'img' ? (
@@ -7228,6 +7284,91 @@ useEffect(() => {
)}
+ {/* Modal de advertencia: EDoc/Acuse con documentos de error */}
+ {edocErrorModal.open && edocErrorModal.edoc && (
+
+
+ )}
+
+
+
+
+
+
+ )}
+
);
}
\ No newline at end of file
diff --git a/src/pages/Reports.jsx b/src/pages/Reports.jsx
index 4622dc6..dc09619 100644
--- a/src/pages/Reports.jsx
+++ b/src/pages/Reports.jsx
@@ -198,6 +198,7 @@ export default function Reports() {
const [organizaciones, setOrganizaciones] = useState([]);
const [importadores, setImportadores] = useState([]);
+ const [rfcOptions, setRfcOptions] = useState([]);
useEffect(() => {
const fetchOrganizaciones = async () => {
@@ -241,6 +242,27 @@ export default function Reports() {
pedimento: ''
});
+ // Cargar RFCs cuando cambia la organización seleccionada en filtros globales
+ useEffect(() => {
+ const fetchRfcs = async () => {
+ if (!globalFilters.organizacion) {
+ setRfcOptions([]);
+ return;
+ }
+ try {
+ const url = `${import.meta.env.VITE_EFC_API_URL}/reports/exportmodel/datastage/?organizacion=${globalFilters.organizacion}`;
+ const res = await fetchWithAuth(url);
+ if (!res.ok) throw new Error('Error al obtener RFCs');
+ const data = await res.json();
+ setRfcOptions(data.rfcs || []);
+ } catch (err) {
+ console.error('Error fetching RFCs:', err);
+ setRfcOptions([]);
+ }
+ };
+ fetchRfcs();
+ }, [globalFilters.organizacion]);
+
const renderGlobalFilters = () => (
+
+
+
+
+
+
+ + {edocErrorModal.tipo === 'acuse' ? 'Acuse de EDoc con errores' : 'EDoc con errores'} +++ Este EDocument ({edocErrorModal.edoc.numero_edocument}) cuenta con errores en la respuesta recibida. + Revisa el documento de error antes de volver a intentarlo. + ++ Puedes continuar con el flujo de trabajo, pero se recomienda corregir el error primero. + + + {/* Lista de documentos de error */} + {(() => { + const tipoFiltro = edocErrorModal.tipo === 'acuse' ? 26 : 22; + const docsError = edocErrorModal.edoc.documentos?.filter(d => d.document_type === tipoFiltro) || []; + return docsError.length > 0 ? ( +
+
+ ) : null;
+ })()}
+
+ Documentos de error +
+
+
@@ -269,16 +291,17 @@ export default function Reports() {
value={globalFilters.organizacion || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
- organizacion: e.target.value
+ organizacion: e.target.value,
+ rfc: ''
}))}
- className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500
+ className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm
transition-all duration-200 bg-white appearance-none"
>
-
+
{organizaciones.results && organizaciones.results.map(org => (
))}
@@ -318,13 +341,11 @@ export default function Reports() {
}))}
className="w-full px-3 py-2 border border-green-300 rounded-md shadow-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white text-slate-900 text-sm font-mono uppercase"
style={{ textTransform: 'uppercase' }}
+ disabled={!globalFilters.organizacion}
>
-
- {importadores.filter(imp => {
- if (!globalFilters.organizacion) return true;
- return imp.organizacion === globalFilters.organizacion;
- }).map(imp => (
-
+
+ {rfcOptions.map(rfc => (
+
))}
{/* Filtros avanzados */}
@@ -777,6 +752,16 @@ export default function Users() {
@@ -807,7 +828,12 @@ export default function Reports() {
.map(([modelo]) => modelo);
if (modelosConCampos.length === 0) {
- alert('Por favor selecciona al menos un campo en algún modelo');
+ showMessage('Por favor selecciona al menos un campo en algún modelo', 'error');
+ return;
+ }
+
+ if (!globalFilters.organizacion) {
+ showMessage('Debes seleccionar una organización antes de generar el reporte', 'error');
return;
}
diff --git a/src/pages/UserForm.jsx b/src/pages/UserForm.jsx
new file mode 100644
index 0000000..15724e5
--- /dev/null
+++ b/src/pages/UserForm.jsx
@@ -0,0 +1,770 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
+import { createUser, updateUser } from '../api/users.ts';
+import { useNotification } from '../context/NotificationContext';
+
+const initialForm = {
+ username: '',
+ email: '',
+ first_name: '',
+ last_name: '',
+ password: '',
+ confirmPassword: '',
+ rfc: [],
+ userType: 'agente', // 'agente' | 'importador'
+ groups: [],
+ is_active: true,
+};
+
+// Perfiles disponibles en el sistema
+const AVAILABLE_GROUPS = [
+ { id: 1, label: 'Admin', description: 'Administrador del sistema' },
+ { id: 2, label: 'Developer', description: 'Desarrollador' },
+ { id: 3, label: 'User', description: 'Acceso base (requerido)' },
+ { id: 4, label: 'Agente Aduanal', description: 'Agente aduanal' },
+ { id: 5, label: 'Importador', description: 'Importador general' }
+];
+
+export default function UserForm() {
+ const { id } = useParams(); // presente si es edición
+ const isEditing = Boolean(id);
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const { showMessage } = useNotification();
+
+ // Preseleccionar tipo desde query param (?type=agente|importador)
+ const initialType = searchParams.get('type') === 'importador' ? 'importador' : 'agente';
+
+ const [form, setForm] = useState({
+ ...initialForm,
+ userType: initialType,
+ groups: initialType === 'importador' ? [3, 5] : [4, 3],
+ });
+ const [importadores, setImportadores] = useState([]);
+ const [submitting, setSubmitting] = useState(false);
+ const [loadingUser, setLoadingUser] = useState(isEditing);
+
+ // 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 importadores
+ useEffect(() => {
+ const access = localStorage.getItem('access');
+ if (!access) { window.location.href = '/login'; return; }
+ fetch(`${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, {
+ headers: { Authorization: `Bearer ${access}` },
+ })
+ .then(r => r.json())
+ .then(data => setImportadores(Array.isArray(data) ? data : []))
+ .catch(() => setImportadores([]));
+ }, []);
+
+ // Cargar datos del usuario si es edición
+ useEffect(() => {
+ if (!isEditing) return;
+ const access = localStorage.getItem('access');
+ fetch(`${import.meta.env.VITE_EFC_API_URL}/user/users/${id}/`, {
+ headers: { Authorization: `Bearer ${access}` },
+ })
+ .then(r => r.json())
+ .then(data => {
+ const isImportador = data.is_importador === true ||
+ (Array.isArray(data.groups) && data.groups.includes(5));
+ setForm({
+ username: data.username || '',
+ email: data.email || '',
+ first_name: data.first_name || '',
+ last_name: data.last_name || '',
+ password: '',
+ confirmPassword: '',
+ // rfc es M2M: viene como array de PKs (strings de RFC)
+ rfc: Array.isArray(data.rfc) ? data.rfc : (data.rfc ? [data.rfc] : []),
+ userType: isImportador ? 'importador' : 'agente',
+ groups: Array.isArray(data.groups) ? data.groups : [],
+ is_active: data.is_active !== false,
+ });
+ setLoadingUser(false);
+ })
+ .catch(() => {
+ showMessage('Error al cargar datos del usuario', 'error');
+ setLoadingUser(false);
+ });
+ }, [id, isEditing, showMessage]);
+
+ 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;
+ }
+ // En edición la contraseña es opcional
+ 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,
+ // Limpiar RFCs si cambia a agente
+ rfc: type === 'agente' ? [] : prev.rfc,
+ // Preseleccionar perfiles por defecto según tipo
+ groups: type === 'importador' ? [3, 5] : [4, 3],
+ }));
+ };
+
+ const handleGroupToggle = (groupId) => {
+ setForm(prev => {
+ const groups = prev.groups.includes(groupId)
+ ? prev.groups.filter(g => g !== groupId)
+ : [...prev.groups, groupId];
+ return { ...prev, groups };
+ });
+ };
+
+ 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 };
+ });
+ };
+
+ 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,
+ groups: form.groups,
+ 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);
+ showMessage('Usuario actualizado exitosamente', 'success');
+ } else {
+ await createUser(payload);
+ showMessage('Usuario creado exitosamente', 'success');
+ }
+ navigate('/users');
+ } catch (err) {
+ showMessage(err.message, 'error');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const isImportador = form.userType === 'importador';
+
+ if (loadingUser) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/src/pages/Users.jsx b/src/pages/Users.jsx
index f5c7361..9fc9dfc 100644
--- a/src/pages/Users.jsx
+++ b/src/pages/Users.jsx
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import { fetchUsers, createUser, updateUser, deleteUser, getCurrentUser } from '../api/users.ts';
import { useNotification } from '../context/NotificationContext';
@@ -47,6 +48,7 @@ export default function Users() {
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const { showMessage } = useNotification();
+ const navigate = useNavigate();
// Estados para validación de contraseña
const [passwordValidation, setPasswordValidation] = useState({
@@ -552,44 +554,17 @@ export default function Users() {
+
+ {/* Header */}
+
+
+
+
+ {/* Formulario */}
+
+
+
+
+
+ {/* Botón regresar */}
+ + {isEditing ? 'Editar Usuario' : 'Nuevo Usuario'} +++ {isEditing ? 'Modifica los datos del usuario seleccionado' : 'Registro en el Sistema de Gestión de Usuarios'} + +
+
+
|
+
- ID: {user.id}
+
|