Se agregaron datos del ticker 2025-08-046
This commit is contained in:
@@ -1,8 +1,213 @@
|
||||
// Modal para relacionar importadores a una credencial VUCEM
|
||||
function RelacionarImportadoresModal({ open, onClose, vucem }) {
|
||||
const [importadoresDisponibles, setImportadoresDisponibles] = React.useState([]);
|
||||
const [importadoresSeleccionados, setImportadoresSeleccionados] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open || !vucem) return;
|
||||
// Obtener ambos listados en paralelo
|
||||
Promise.all([
|
||||
fetchWithAuth(import.meta.env.VITE_EFC_API_URL + '/customs/importadores/').then(res => res.json()),
|
||||
fetchWithAuth(import.meta.env.VITE_EFC_API_URL + `/vucem/usuario-importador/?vucem=${vucem.id}`).then(res => res.json())
|
||||
]).then(([importadoresAll, relaciones]) => {
|
||||
// Agrupar relaciones por RFC, tomar la más reciente (última) para cada RFC
|
||||
const relacionesPorRfc = {};
|
||||
relaciones.results.forEach(rel => {
|
||||
relacionesPorRfc[rel.rfc] = rel; // sobrescribe, así queda la última
|
||||
});
|
||||
const seleccionados = Object.values(relacionesPorRfc).map(rel => {
|
||||
const base = importadoresAll.find(i => i.rfc === rel.rfc) || {};
|
||||
return {
|
||||
...rel,
|
||||
nombre: base.nombre || rel.rfc,
|
||||
organizacion: base.organizacion || rel.organizacion
|
||||
};
|
||||
});
|
||||
setImportadoresSeleccionados(seleccionados);
|
||||
// Disponibles = todos menos los seleccionados (por RFC)
|
||||
const seleccionadosRFC = new Set(seleccionados.map(i => i.rfc));
|
||||
setImportadoresDisponibles(importadoresAll.filter(i => !seleccionadosRFC.has(i.rfc)));
|
||||
});
|
||||
}, [open, vucem]);
|
||||
|
||||
if (!open || !vucem) return null;
|
||||
|
||||
// Mover importador de disponibles a seleccionados
|
||||
const seleccionarImportador = async (imp) => {
|
||||
// POST para crear la relación
|
||||
try {
|
||||
// Usar siempre la organización del registro vucem
|
||||
const body = {
|
||||
vucem: vucem.id,
|
||||
rfc: imp.rfc,
|
||||
organizacion: vucem.organizacion
|
||||
};
|
||||
const res = await fetchWithAuth(
|
||||
import.meta.env.VITE_EFC_API_URL + '/vucem/usuario-importador/',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error('Error al relacionar importador');
|
||||
const data = await res.json();
|
||||
// Guardar el id de la relación en el importador seleccionado
|
||||
const impConRelacion = {
|
||||
...imp,
|
||||
id: data.id, // para que el botón de quitar funcione correctamente
|
||||
usuario_importador_id: data.id
|
||||
};
|
||||
setImportadoresDisponibles(importadoresDisponibles.filter(i => i.rfc !== imp.rfc));
|
||||
setImportadoresSeleccionados([...importadoresSeleccionados, impConRelacion]);
|
||||
} catch (err) {
|
||||
alert('No se pudo relacionar el importador.');
|
||||
}
|
||||
};
|
||||
// Mover importador de seleccionados a disponibles
|
||||
const quitarImportador = async (imp) => {
|
||||
// DELETE para eliminar la relación usando el id de la relación
|
||||
try {
|
||||
// Buscar el id de la relación usuario-importador en el objeto importador
|
||||
const relacionId = imp.usuario_importador_id || imp.id_relacion || imp.id;
|
||||
if (!relacionId) {
|
||||
alert('No se encontró el id de la relación usuario-importador.');
|
||||
return;
|
||||
}
|
||||
const url = `${import.meta.env.VITE_EFC_API_URL}/vucem/usuario-importador/${relacionId}/`;
|
||||
const res = await fetchWithAuth(url, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Error al eliminar relación');
|
||||
setImportadoresSeleccionados(importadoresSeleccionados.filter(i => i.rfc !== imp.rfc));
|
||||
setImportadoresDisponibles([...importadoresDisponibles, imp]);
|
||||
} catch (err) {
|
||||
alert('No se pudo eliminar la relación del importador.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
|
||||
<div className="relative mx-auto w-full max-w-3xl bg-white rounded-lg shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||
{/* Header formal en escala de azules */}
|
||||
<div className="bg-gradient-to-r from-blue-700 to-blue-900 rounded-t-lg p-4 text-white border-b-2 border-blue-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-blue-500 bg-opacity-30 rounded-lg p-2 border border-blue-400 border-opacity-30">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold tracking-wide">Relacionar Importadores</h3>
|
||||
<p className="text-blue-200 text-xs font-medium">Asocia importadores a la credencial seleccionada</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-blue-100 hover:text-white transition-colors p-2 hover:bg-blue-600 hover:bg-opacity-50 rounded-lg border border-blue-500 border-opacity-30"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Contenido del modal */}
|
||||
<div className="p-4 max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center mb-4 gap-2 border-b pb-2 border-blue-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Usuario:</span>
|
||||
<span className="inline-block bg-blue-100 text-blue-700 rounded px-2 py-0.5 text-xs font-bold shadow-sm">{vucem.usuario}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Patente:</span>
|
||||
<span className="inline-block bg-blue-100 text-blue-700 rounded px-2 py-0.5 text-xs font-bold shadow-sm">{vucem.patente}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full">
|
||||
{/* Importadores disponibles */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-blue-700 mb-2">Disponibles</h3>
|
||||
<ul className="border border-blue-100 rounded-lg divide-y divide-blue-50 bg-gray-50 shadow-sm">
|
||||
{importadoresDisponibles.length === 0 && (
|
||||
<li className="text-gray-400 text-center py-3 text-sm">Sin importadores</li>
|
||||
)}
|
||||
{importadoresDisponibles.map(imp => (
|
||||
<li
|
||||
key={imp.id}
|
||||
className="px-3 py-2 cursor-pointer hover:bg-blue-100/70 transition rounded flex items-center gap-2 group text-sm"
|
||||
onClick={() => seleccionarImportador(imp)}
|
||||
>
|
||||
<span className="flex-1 font-medium text-gray-700 truncate">
|
||||
<span className="inline-block bg-white border border-blue-200 text-blue-700 rounded px-2 py-0.5 text-xs font-semibold mr-2 align-middle shadow-sm">
|
||||
{imp.rfc}
|
||||
</span>
|
||||
{imp.nombre}
|
||||
</span>
|
||||
<button
|
||||
className="ml-2 px-3 py-1 bg-blue-600 text-white rounded shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 text-xs font-semibold"
|
||||
title="Agregar"
|
||||
>
|
||||
Agregar
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Importadores seleccionados */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-blue-700 mb-2">Seleccionados</h3>
|
||||
<ul className="border border-blue-200 rounded-lg divide-y divide-blue-100 bg-blue-50 shadow-sm">
|
||||
{importadoresSeleccionados.length === 0 && (
|
||||
<li className="text-gray-400 text-center py-3 text-sm">Sin seleccionados</li>
|
||||
)}
|
||||
{importadoresSeleccionados.map(imp => (
|
||||
<li
|
||||
key={imp.id}
|
||||
className="px-3 py-2 cursor-pointer hover:bg-blue-200/80 transition rounded flex items-center gap-2 group text-sm"
|
||||
onClick={() => quitarImportador(imp)}
|
||||
>
|
||||
<button
|
||||
className="mr-2 px-3 py-1 bg-gray-300 text-gray-800 rounded shadow hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 text-xs font-semibold"
|
||||
title="Quitar"
|
||||
>
|
||||
Quitar
|
||||
</button>
|
||||
<span className="flex-1 font-medium text-gray-700 truncate">
|
||||
<span className="inline-block bg-white border border-blue-300 text-blue-700 rounded px-2 py-0.5 text-xs font-semibold mr-2 align-middle shadow-sm">
|
||||
{imp.rfc}
|
||||
</span>
|
||||
{imp.nombre}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 w-full">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-1.5 bg-blue-600 text-white rounded-lg font-semibold shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||||
.animate-fade-in { animation: fade-in 0.3s ease; }
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth, putFormDataWithAuth, postFormDataWithAuth, patchWithAuth } from '../fetchWithAuth';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
export default function Vucem() {
|
||||
// Estado para modal de relacionar importadores
|
||||
const [showRelacionarModal, setShowRelacionarModal] = useState(false);
|
||||
const [selectedVucem, setSelectedVucem] = useState(null);
|
||||
const [vucemList, setVucemList] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
@@ -292,6 +497,14 @@ export default function Vucem() {
|
||||
// Table y header estilo Users.jsx
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Modal Relacionar Importadores */}
|
||||
{showRelacionarModal && selectedVucem && (
|
||||
<RelacionarImportadoresModal
|
||||
open={showRelacionarModal}
|
||||
onClose={() => setShowRelacionarModal(false)}
|
||||
vucem={selectedVucem}
|
||||
/>
|
||||
)}
|
||||
{/* Header modernizado con gradientes azules */}
|
||||
<div className="mb-8 relative overflow-hidden rounded-2xl shadow-xl bg-gradient-to-br from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-blue-900/30"></div>
|
||||
@@ -638,6 +851,18 @@ export default function Vucem() {
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedVucem(vucem);
|
||||
setShowRelacionarModal(true);
|
||||
}}
|
||||
className="inline-flex items-center p-1 border border-purple-300 shadow-sm rounded text-purple-600 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-200 transition-all duration-200"
|
||||
title="Relacionar importadores"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
|
||||
className="inline-flex items-center p-2 border border-blue-300 shadow-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
|
||||
@@ -736,6 +961,16 @@ export default function Vucem() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => handleAbrirRelacion(vucem)}
|
||||
className="inline-flex items-center justify-center p-2 border border-purple-300 shadow-sm font-medium rounded-lg text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200"
|
||||
title="Relacionar importadores"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline text-xs">Relacionar</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
|
||||
className="inline-flex items-center justify-center p-2 border border-blue-300 shadow-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
||||
@@ -745,6 +980,7 @@ export default function Vucem() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => toggleVucemStatus(vucem.id, vucem.is_active)}
|
||||
className={`inline-flex items-center justify-center p-2 border shadow-sm font-medium rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 ${vucem.is_active
|
||||
|
||||
Reference in New Issue
Block a user