Se agregaron datos del ticker 2025-08-046

This commit is contained in:
2025-08-20 09:15:59 -06:00
parent 2bc70fc3c2
commit 3e498c57ad
7 changed files with 889 additions and 395 deletions

View File

@@ -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