1964 lines
121 KiB
JavaScript
1964 lines
121 KiB
JavaScript
// 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);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [editVucem, setEditVucem] = useState(null);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
const [deleteVucem, setDeleteVucem] = useState(null);
|
|
|
|
// Estado para formulario de creación/edición
|
|
const initialForm = {
|
|
usuario: '',
|
|
password: '',
|
|
patente: '',
|
|
efirma: '',
|
|
key: null,
|
|
cer: null,
|
|
is_importador: false,
|
|
acusecove: false,
|
|
acuseedocument: false,
|
|
is_active: true,
|
|
};
|
|
const [form, setForm] = useState(initialForm);
|
|
|
|
// Estados para controlar visibilidad de contraseñas y copiar
|
|
const [showPassword, setShowPassword] = useState({});
|
|
const [showEfirma, setShowEfirma] = useState({});
|
|
const [copySuccess, setCopySuccess] = useState('');
|
|
|
|
// Estados específicos para modal de edición (siempre ocultos por defecto)
|
|
const [showEditPassword, setShowEditPassword] = useState(false);
|
|
const [showEditEfirma, setShowEditEfirma] = useState(false);
|
|
|
|
// Estados específicos para modal de creación
|
|
const [showCreatePassword, setShowCreatePassword] = useState(false);
|
|
const [showCreateEfirma, setShowCreateEfirma] = useState(false);
|
|
|
|
// Funciones para alternar visibilidad
|
|
const togglePasswordVisibility = (id) => {
|
|
setShowPassword(prev => ({
|
|
...prev,
|
|
[id]: !prev[id]
|
|
}));
|
|
};
|
|
|
|
const toggleEfirmaVisibility = (id) => {
|
|
setShowEfirma(prev => ({
|
|
...prev,
|
|
[id]: !prev[id]
|
|
}));
|
|
};
|
|
|
|
// Funciones para alternar visibilidad en modal de edición
|
|
const toggleEditPasswordVisibility = () => {
|
|
setShowEditPassword(!showEditPassword);
|
|
};
|
|
|
|
const toggleEditEfirmaVisibility = () => {
|
|
setShowEditEfirma(!showEditEfirma);
|
|
};
|
|
|
|
// Función helper para truncar texto largo en la tabla
|
|
const truncateForTable = (text, maxLength = 20) => {
|
|
if (!text || text.length <= maxLength) return text;
|
|
const start = text.substring(0, 6);
|
|
const end = text.substring(text.length - 6);
|
|
return `${start}...${end}`;
|
|
};
|
|
|
|
// Función para copiar al portapapeles
|
|
const copyToClipboard = async (text, fieldName) => {
|
|
try {
|
|
// Verificar si el texto existe
|
|
if (!text || text.trim() === '') {
|
|
setCopySuccess(`${fieldName} está vacío`);
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
return;
|
|
}
|
|
|
|
// Intentar usar la Clipboard API moderna
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopySuccess(`${fieldName} copiado`);
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
} else {
|
|
// Fallback para navegadores más antiguos o contextos no seguros
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.opacity = '0';
|
|
textArea.style.pointerEvents = 'none';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
const successful = document.execCommand('copy');
|
|
if (successful) {
|
|
setCopySuccess(`${fieldName} copiado`);
|
|
} else {
|
|
setCopySuccess(`Error al copiar ${fieldName}`);
|
|
}
|
|
} catch (fallbackErr) {
|
|
console.error('Error en fallback:', fallbackErr);
|
|
setCopySuccess(`Error al copiar ${fieldName}`);
|
|
} finally {
|
|
document.body.removeChild(textArea);
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error al copiar:', err);
|
|
setCopySuccess(`Error al copiar ${fieldName}`);
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
}
|
|
};
|
|
|
|
// Handlers básicos para inputs
|
|
const handleInputChange = e => {
|
|
const { name, value, type, checked, files } = e.target;
|
|
if (type === 'checkbox') {
|
|
setForm(f => ({ ...f, [name]: checked }));
|
|
} else if (type === 'file') {
|
|
setForm(f => ({ ...f, [name]: files[0] }));
|
|
} else {
|
|
setForm(f => ({ ...f, [name]: value }));
|
|
}
|
|
};
|
|
const closeModals = () => {
|
|
setShowCreateModal(false);
|
|
setShowEditModal(false);
|
|
setShowDeleteModal(false);
|
|
setForm(initialForm);
|
|
setEditVucem(null);
|
|
setDeleteVucem(null);
|
|
// Resetear estados de visibilidad del modal de edición
|
|
setShowEditPassword(false);
|
|
setShowEditEfirma(false);
|
|
// Resetear estados de visibilidad del modal de creación
|
|
setShowCreatePassword(false);
|
|
setShowCreateEfirma(false);
|
|
};
|
|
|
|
// Fetch list
|
|
const fetchVucem = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetchWithAuth(`${API_URL}/vucem/vucem/`);
|
|
if (!res.ok) throw new Error('Error al cargar Ventanilla Unica');
|
|
const data = await res.json();
|
|
setVucemList(data);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError('Error al cargar Ventanilla Unica');
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
// Funciones de descarga
|
|
const downloadCertificate = async (id, usuario) => {
|
|
try {
|
|
const res = await fetchWithAuth(`${API_URL}/vucem/vucem/${id}/download_cer/`);
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 404) {
|
|
setCopySuccess('Certificado no encontrado');
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
return;
|
|
}
|
|
throw new Error('Error al descargar certificado');
|
|
}
|
|
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = `${usuario}_certificado.cer`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
setCopySuccess('Certificado descargado');
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
} catch (err) {
|
|
console.error('Error al descargar certificado:', err);
|
|
setCopySuccess('Error al descargar certificado');
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
}
|
|
};
|
|
|
|
const downloadKey = async (id, usuario) => {
|
|
try {
|
|
const res = await fetchWithAuth(`${API_URL}/vucem/vucem/${id}/download_key/`);
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 404) {
|
|
setCopySuccess('Clave no encontrada');
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
return;
|
|
}
|
|
throw new Error('Error al descargar clave');
|
|
}
|
|
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = `${usuario}_clave.key`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
setCopySuccess('Clave descargada');
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
} catch (err) {
|
|
console.error('Error al descargar clave:', err);
|
|
setCopySuccess('Error al descargar clave');
|
|
setTimeout(() => setCopySuccess(''), 2000);
|
|
}
|
|
};
|
|
|
|
// Función para activar/desactivar credencial
|
|
const toggleVucemStatus = async (id, currentStatus) => {
|
|
try {
|
|
const response = await patchWithAuth(`${API_URL}/vucem/vucem/${id}/`, {
|
|
is_active: !currentStatus
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Error al cambiar el estado');
|
|
}
|
|
|
|
// Recargar la lista para reflejar los cambios
|
|
await fetchVucem();
|
|
|
|
} catch (err) {
|
|
alert('Error al cambiar el estado de la credencial');
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchVucem();
|
|
}, []);
|
|
|
|
// Filtros visuales
|
|
const [filterUsuario, setFilterUsuario] = useState('');
|
|
const [filterPatente, setFilterPatente] = useState('');
|
|
const filteredList = vucemList.filter(v =>
|
|
v.usuario.toLowerCase().includes(filterUsuario.toLowerCase()) &&
|
|
v.patente.toLowerCase().includes(filterPatente.toLowerCase())
|
|
);
|
|
|
|
// Paginación estilo Users.jsx
|
|
const [page, setPage] = useState(1);
|
|
const pageSize = 10;
|
|
const totalPages = Math.max(1, Math.ceil(filteredList.length / pageSize));
|
|
const paginatedList = filteredList.slice((page - 1) * pageSize, page * pageSize);
|
|
|
|
// Reset page si cambia el filtro
|
|
useEffect(() => { setPage(1); }, [filterUsuario, filterPatente]);
|
|
|
|
// Cuando se selecciona un registro para editar, poblar el formulario con sus datos
|
|
useEffect(() => {
|
|
if (editVucem) {
|
|
setForm({
|
|
usuario: editVucem.usuario || '',
|
|
password: editVucem.password || '', // Mostrar el password actual para edición
|
|
patente: editVucem.patente || '',
|
|
efirma: editVucem.efirma || '', // Ya estaba incluido
|
|
key: null, // No se rellena, solo se sube si el usuario selecciona
|
|
cer: null, // No se rellena, solo se sube si el usuario selecciona
|
|
is_importador: !!editVucem.is_importador,
|
|
acusecove: !!editVucem.acusecove,
|
|
acuseedocument: !!editVucem.acuseedocument,
|
|
is_active: !!editVucem.is_active,
|
|
});
|
|
// Resetear visibilidad de campos sensibles cuando se abre el modal de edición
|
|
setShowEditPassword(false);
|
|
setShowEditEfirma(false);
|
|
}
|
|
}, [editVucem]);
|
|
|
|
// 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>
|
|
<div className="absolute -top-24 -right-24 w-48 h-48 bg-white/10 rounded-full blur-xl"></div>
|
|
<div className="absolute -bottom-12 -left-12 w-32 h-32 bg-white/5 rounded-full blur-lg"></div>
|
|
|
|
<div className="relative flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6">
|
|
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-2xl p-3 sm:p-4 shadow-lg animate-bounce-slow">
|
|
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
|
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-white tracking-tight">
|
|
Credenciales VU
|
|
</h1>
|
|
<div className="flex flex-wrap gap-2">
|
|
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-white/20 text-white backdrop-blur-sm animate-fade-in">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 0a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1V8z" clipRule="evenodd" />
|
|
</svg>
|
|
{vucemList.length} Total
|
|
</span>
|
|
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-green-500/20 text-green-100 backdrop-blur-sm animate-fade-in" style={{ animationDelay: '0.1s' }}>
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
{vucemList.filter(v => v.is_active).length} Activos
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<p className="text-blue-100 text-sm sm:text-base font-medium">
|
|
Gestiona certificados digitales, credenciales y configuraciones Ventanilla Unica del sistema aduanero
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<style>{`
|
|
@keyframes bounce-slow {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-8px); }
|
|
}
|
|
.animate-bounce-slow {
|
|
animation: bounce-slow 2.2s infinite;
|
|
}
|
|
@keyframes fade-in {
|
|
from { opacity: 0; transform: scale(0.9); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
.animate-fade-in {
|
|
animation: fade-in 0.7s ease;
|
|
}
|
|
@keyframes fade-in-up {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.fade-in-up-users {
|
|
animation: fade-in-up 0.4s ease forwards;
|
|
opacity: 0;
|
|
}
|
|
`}</style>
|
|
|
|
{/* Mensaje de éxito para copiar */}
|
|
{copySuccess && (
|
|
<div className="fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 flex items-center space-x-2 animate-fade-in">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span>{copySuccess}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Controles de búsqueda y filtros mejorados */}
|
|
<div className="bg-white shadow-lg rounded-xl border border-gray-100 mb-6 overflow-hidden">
|
|
<div className="bg-gradient-to-r from-gray-50 to-blue-50 px-4 sm:px-6 py-4 border-b border-gray-200">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
{/* Búsqueda principal */}
|
|
<div className="flex-1 max-w-md">
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
className="block w-full pl-10 pr-10 py-3 border border-gray-300 rounded-lg bg-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 shadow-sm"
|
|
placeholder="Buscar por usuario o patente..."
|
|
value={filterUsuario}
|
|
onChange={e => setFilterUsuario(e.target.value)}
|
|
autoComplete="off"
|
|
/>
|
|
{filterUsuario && (
|
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
<button
|
|
onClick={() => setFilterUsuario('')}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-100"
|
|
title="Limpiar búsqueda"
|
|
>
|
|
<svg className="h-4 w-4" 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>
|
|
</div>
|
|
|
|
{/* Información y botón crear */}
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
|
{filteredList.length !== vucemList.length && (
|
|
<div className="text-sm text-blue-700">
|
|
<span className="inline-flex items-center">
|
|
<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="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 0a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1V8z" />
|
|
</svg>
|
|
Mostrando {filteredList.length} de {vucemList.length} registros
|
|
{filterUsuario && <span className="ml-1">para "{filterUsuario}"</span>}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
type="button"
|
|
className="inline-flex items-center px-4 py-2.5 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
|
|
>
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Agregar Credencial
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Vista responsiva: tabla para desktop, cards para mobile */}
|
|
|
|
{/* Tabla para pantallas grandes */}
|
|
<div className="hidden lg:block bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100">
|
|
<div style={{ minHeight: 'calc(8 * 56px)', maxHeight: 'calc(8 * 56px)', overflowY: 'auto', position: 'relative' }}>
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gradient-to-r from-gray-50 to-blue-50 sticky top-0 z-20">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Usuario</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Password</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">e.firma</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Archivos</th>
|
|
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Estado</th>
|
|
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={6} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
|
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<span className="text-gray-500 text-lg">Cargando registros Ventanilla Unica...</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
) : error ? (
|
|
<tr>
|
|
<td colSpan={6} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
|
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
|
|
<div className="text-center">
|
|
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<span className="text-red-600 text-lg">Error: {error}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
) : paginatedList.length > 0 ? (
|
|
<>
|
|
{paginatedList.map((vucem, idx) => (
|
|
<tr
|
|
key={vucem.id}
|
|
className={
|
|
`transition-all duration-300 hover:scale-[1.015] hover:shadow-md hover:bg-blue-50 fade-in-up-users` +
|
|
(idx % 2 === 0 ? ' bg-white' : ' bg-gray-50')
|
|
}
|
|
style={{ animationDelay: `${0.05 * idx}s` }}
|
|
>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">{vucem.usuario}</div>
|
|
<div className="text-sm text-gray-500">Patente: {vucem.patente}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-gray-600 font-mono">
|
|
{showPassword[vucem.id] ? truncateForTable(vucem.password) || '(vacío)' : '••••••••'}
|
|
</span>
|
|
<button
|
|
onClick={() => togglePasswordVisibility(vucem.id)}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title={showPassword[vucem.id] ? "Ocultar password" : "Mostrar password"}
|
|
>
|
|
{showPassword[vucem.id] ? (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => copyToClipboard(vucem.password || '', 'Password')}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title="Copiar password"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-gray-600 font-mono">
|
|
{showEfirma[vucem.id] ? truncateForTable(vucem.efirma) || '(vacío)' : '••••••••'}
|
|
</span>
|
|
<button
|
|
onClick={() => toggleEfirmaVisibility(vucem.id)}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title={showEfirma[vucem.id] ? "Ocultar e.firma" : "Mostrar e.firma"}
|
|
>
|
|
{showEfirma[vucem.id] ? (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => copyToClipboard(vucem.efirma || '', 'e.firma')}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title="Copiar e.firma"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex flex-col gap-1">
|
|
{vucem.key ? (
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
Key
|
|
</span>
|
|
<button
|
|
onClick={() => downloadKey(vucem.id, vucem.usuario)}
|
|
className="inline-flex items-center p-1 border border-blue-300 shadow-sm text-xs font-medium rounded 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"
|
|
title="Descargar Key"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
Sin Key
|
|
</span>
|
|
)}
|
|
{vucem.cer ? (
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
Cer
|
|
</span>
|
|
<button
|
|
onClick={() => downloadCertificate(vucem.id, vucem.usuario)}
|
|
className="inline-flex items-center p-1 border border-green-300 shadow-sm text-xs font-medium rounded text-green-700 bg-white hover:bg-green-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200"
|
|
title="Descargar Certificado"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
Sin Cer
|
|
</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
|
<div className="flex flex-col items-center gap-1">
|
|
{vucem.is_active ? (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
Activo
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
Inactivo
|
|
</span>
|
|
)}
|
|
{vucem.is_importador && (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1z" clipRule="evenodd" />
|
|
</svg>
|
|
Importador
|
|
</span>
|
|
)}
|
|
</div>
|
|
</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"
|
|
title="Editar"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<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 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 transform hover:scale-105 ${vucem.is_active
|
|
? 'border-orange-300 text-orange-700 hover:bg-orange-50 focus:ring-orange-500'
|
|
: 'border-green-300 text-green-700 hover:bg-green-50 focus:ring-green-500'
|
|
}`}
|
|
title={vucem.is_active ? "Desactivar" : "Activar"}
|
|
>
|
|
{vucem.is_active ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L5.636 5.636" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }}
|
|
className="inline-flex items-center p-2 border border-red-300 shadow-sm font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 transform hover:scale-105"
|
|
title="Eliminar"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{/* Rellenar con filas vacías si hay menos de 8 */}
|
|
{paginatedList.length < 8 && !loading && !error && Array.from({ length: 8 - paginatedList.length }).map((_, idx) => (
|
|
<tr key={`empty-${idx}`}>
|
|
<td className="px-6 py-4 whitespace-nowrap" colSpan={6}> </td>
|
|
</tr>
|
|
))}
|
|
</>
|
|
) : (
|
|
<tr>
|
|
<td colSpan={6} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
|
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
|
|
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay registros</h3>
|
|
<p className="text-gray-500">Comienza creando tu primer registro de Ventanilla Unica.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{/* Cards para pantallas pequeñas */}
|
|
<div className="lg:hidden">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<span className="text-gray-500 text-lg">Cargando registros...</span>
|
|
</div>
|
|
</div>
|
|
) : error ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<span className="text-red-600 text-lg">Error: {error}</span>
|
|
</div>
|
|
</div>
|
|
) : paginatedList.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{paginatedList.map((vucem, idx) => (
|
|
<div key={vucem.id} className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-[1.02] fade-in-up-users" style={{ animationDelay: `${0.05 * idx}s` }}>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex items-center space-x-3 flex-1">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-sm font-semibold text-gray-900 truncate">{vucem.usuario}</h3>
|
|
<p className="text-xs text-gray-500 truncate">Patente: {vucem.patente}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<button
|
|
onClick={() => {
|
|
setSelectedVucem(vucem);
|
|
setShowRelacionarModal(true);
|
|
}}
|
|
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"
|
|
title="Editar"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<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
|
|
? 'border-orange-300 text-orange-700 hover:bg-orange-50 focus:ring-orange-500'
|
|
: 'border-green-300 text-green-700 hover:bg-green-50 focus:ring-green-500'
|
|
}`}
|
|
title={vucem.is_active ? "Desactivar" : "Activar"}
|
|
>
|
|
{vucem.is_active ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L5.636 5.636" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }}
|
|
className="inline-flex items-center justify-center p-2 border border-red-300 shadow-sm font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200"
|
|
title="Eliminar"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
<div>
|
|
<span className="font-medium text-gray-500">Password:</span>
|
|
<div className="mt-1 flex items-center space-x-2">
|
|
<span className="text-gray-600 font-mono">
|
|
{showPassword[vucem.id] ? truncateForTable(vucem.password) || '(vacío)' : '••••••••'}
|
|
</span>
|
|
<button
|
|
onClick={() => togglePasswordVisibility(vucem.id)}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title={showPassword[vucem.id] ? "Ocultar password" : "Mostrar password"}
|
|
>
|
|
{showPassword[vucem.id] ? (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => copyToClipboard(vucem.password || '', 'Password')}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title="Copiar password"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium text-gray-500">e.firma:</span>
|
|
<div className="mt-1 flex items-center space-x-2">
|
|
<span className="text-gray-600 font-mono">
|
|
{showEfirma[vucem.id] ? truncateForTable(vucem.efirma) || '(vacío)' : '••••••••'}
|
|
</span>
|
|
<button
|
|
onClick={() => toggleEfirmaVisibility(vucem.id)}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title={showEfirma[vucem.id] ? "Ocultar e.firma" : "Mostrar e.firma"}
|
|
>
|
|
{showEfirma[vucem.id] ? (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => copyToClipboard(vucem.efirma || '', 'e.firma')}
|
|
className="inline-flex items-center p-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
|
title="Copiar e.firma"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className="font-medium text-gray-500">Estado:</span>
|
|
<div className="mt-1 flex flex-wrap gap-1">
|
|
{vucem.is_active ? (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
Activo
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
Inactivo
|
|
</span>
|
|
)}
|
|
{vucem.is_importador && (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1z" clipRule="evenodd" />
|
|
</svg>
|
|
Importador
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<span className="font-medium text-gray-500">Archivos:</span>
|
|
<div className="mt-1 flex flex-wrap gap-2">
|
|
{vucem.key ? (
|
|
<div className="flex items-center gap-1">
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
Key
|
|
</span>
|
|
<button
|
|
onClick={() => downloadKey(vucem.id, vucem.usuario)}
|
|
className="inline-flex items-center p-1 border border-blue-300 shadow-sm text-xs font-medium rounded 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"
|
|
title="Descargar Key"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
Sin Key
|
|
</span>
|
|
)}
|
|
{vucem.cer ? (
|
|
<div className="flex items-center gap-1">
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
Cer
|
|
</span>
|
|
<button
|
|
onClick={() => downloadCertificate(vucem.id, vucem.usuario)}
|
|
className="inline-flex items-center p-1 border border-green-300 shadow-sm text-xs font-medium rounded text-green-700 bg-white hover:bg-green-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200"
|
|
title="Descargar Certificado"
|
|
>
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
|
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
Sin Cer
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-12">
|
|
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay registros</h3>
|
|
<p className="text-gray-500 text-center">Comienza creando tu primer registro.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Paginación mejorada */}
|
|
{totalPages > 1 && (
|
|
<div className="mt-8 border-t border-gray-200 pt-6">
|
|
<div className="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0">
|
|
<div className="text-sm text-gray-700">
|
|
Mostrando <span className="font-medium">{((page - 1) * pageSize) + 1}</span> a{' '}
|
|
<span className="font-medium">
|
|
{Math.min(page * pageSize, filteredList.length)}
|
|
</span> de{' '}
|
|
<span className="font-medium">{filteredList.length}</span> registros
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={page === 1}
|
|
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
|
>
|
|
<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="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Anterior
|
|
</button>
|
|
<div className="flex space-x-1">
|
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
|
|
<button
|
|
key={pageNum}
|
|
onClick={() => setPage(pageNum)}
|
|
className={`px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${page === pageNum
|
|
? 'bg-blue-600 text-white shadow-lg transform scale-105'
|
|
: 'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 hover:transform hover:scale-105'
|
|
}`}
|
|
>
|
|
{pageNum}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
|
disabled={page === totalPages}
|
|
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
|
>
|
|
Siguiente
|
|
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal de creación */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-2">
|
|
<div className="relative mx-auto w-full max-w-4xl 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">Registro de Credencial VU</h3>
|
|
<p className="text-blue-200 text-xs font-medium">Sistema de Gestión de Credenciales Aduanales</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={closeModals}
|
|
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 formulario */}
|
|
<div className="p-4 max-h-[85vh] overflow-y-auto">
|
|
<form className="space-y-4" onSubmit={async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData();
|
|
formData.append('usuario', form.usuario);
|
|
formData.append('password', form.password);
|
|
formData.append('patente', form.patente);
|
|
formData.append('efirma', form.efirma);
|
|
if (form.key) formData.append('key', form.key);
|
|
if (form.cer) formData.append('cer', form.cer);
|
|
formData.append('is_importador', form.is_importador);
|
|
formData.append('acusecove', form.acusecove);
|
|
formData.append('acuseedocument', form.acuseedocument);
|
|
formData.append('is_active', form.is_active);
|
|
try {
|
|
const res = await postFormDataWithAuth(`${API_URL}/vucem/vucem/`, formData);
|
|
if (!res.ok) throw new Error('Error al crear');
|
|
await fetchVucem();
|
|
closeModals();
|
|
} catch (err) {
|
|
alert('Error al crear');
|
|
}
|
|
}}>
|
|
|
|
{/* Sección de Información Básica */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-blue-600 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-slate-800">Información Básica del Usuario</h4>
|
|
<p className="text-xs text-slate-600">Datos de identificación del usuario VU</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Usuario VU <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
name="usuario"
|
|
value={form.usuario}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm"
|
|
placeholder="Ingrese el nombre de usuario en Ventanilla Única"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Número de Patente <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
name="patente"
|
|
value={form.patente}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm"
|
|
placeholder="Ingrese el número de patente"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Credenciales */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-slate-700 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-slate-800">Credenciales de Seguridad</h4>
|
|
<p className="text-xs text-slate-600">Información de autenticación del sistema</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-gray-700">
|
|
Password <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
name="password"
|
|
type={showCreatePassword ? "text" : "password"}
|
|
value={form.password}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 pr-20 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-sm"
|
|
placeholder="Ingrese el password"
|
|
/>
|
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowCreatePassword(!showCreatePassword)}
|
|
className="px-2 py-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title={showCreatePassword ? "Ocultar password" : "Mostrar password"}
|
|
>
|
|
{showCreatePassword ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L21.536 21.536" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyToClipboard(form.password, 'Password')}
|
|
className="px-2 py-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title="Copiar password"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-gray-700">
|
|
e.firma <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
name="efirma"
|
|
type={showCreateEfirma ? "text" : "password"}
|
|
value={form.efirma}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 pr-20 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-sm"
|
|
placeholder="Ingrese la e.firma"
|
|
/>
|
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowCreateEfirma(!showCreateEfirma)}
|
|
className="px-2 py-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title={showCreateEfirma ? "Ocultar e.firma" : "Mostrar e.firma"}
|
|
>
|
|
{showCreateEfirma ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L21.536 21.536" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyToClipboard(form.efirma, 'e.firma')}
|
|
className="px-2 py-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title="Copiar e.firma"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Archivos de Certificado */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-green-700 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" 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>
|
|
<h4 className="text-sm font-semibold text-slate-800">Certificados Digitales</h4>
|
|
<p className="text-xs text-slate-600">Archivos de certificado y clave privada</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Archivo de Clave (.key)
|
|
</label>
|
|
<input
|
|
name="key"
|
|
type="file"
|
|
accept=".key"
|
|
onChange={handleInputChange}
|
|
className="w-full px-3 py-2 border border-slate-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 file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-green-100 file:text-green-800 hover:file:bg-green-200 text-sm"
|
|
/>
|
|
{form.key && (
|
|
<div className="flex items-center text-xs text-green-700 bg-green-100 px-2 py-1.5 rounded-md border border-green-200">
|
|
<svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{form.key.name}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Archivo de Certificado (.cer)
|
|
</label>
|
|
<input
|
|
name="cer"
|
|
type="file"
|
|
accept=".cer"
|
|
onChange={handleInputChange}
|
|
className="w-full px-3 py-2 border border-slate-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 file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-green-100 file:text-green-800 hover:file:bg-green-200 text-sm"
|
|
/>
|
|
{form.cer && (
|
|
<div className="flex items-center text-xs text-green-700 bg-green-100 px-2 py-1.5 rounded-md border border-green-200">
|
|
<svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{form.cer.name}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Configuraciones */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-amber-600 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-slate-800">Configuraciones del Sistema</h4>
|
|
<p className="text-xs text-slate-600">Opciones y permisos de la credencial</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="is_importador"
|
|
type="checkbox"
|
|
checked={form.is_importador}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Importador</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="acusecove"
|
|
type="checkbox"
|
|
checked={form.acusecove}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Acuse COVE</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="acuseedocument"
|
|
type="checkbox"
|
|
checked={form.acuseedocument}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Acuse e-Document</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="is_active"
|
|
type="checkbox"
|
|
checked={form.is_active}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-green-600 border-slate-300 rounded focus:ring-green-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Credencial Activa</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botones de acción */}
|
|
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 pt-4 border-t border-slate-200">
|
|
<button
|
|
type="button"
|
|
onClick={closeModals}
|
|
className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-700 to-blue-900 hover:from-blue-800 hover:to-blue-950 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 flex items-center justify-center space-x-2"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span>Registrar Credencial</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal de edición */}
|
|
{showEditModal && editVucem && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-2">
|
|
<div className="relative mx-auto w-full max-w-4xl 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="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>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold tracking-wide">Editar Credencial</h3>
|
|
<p className="text-blue-200 text-xs font-medium">Modificación de Credenciales Aduanales</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={closeModals}
|
|
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 formulario */}
|
|
<div className="p-4 max-h-[85vh] overflow-y-auto">
|
|
<form className="space-y-4" onSubmit={async (e) => {
|
|
e.preventDefault();
|
|
if (!editVucem) return;
|
|
const formData = new FormData();
|
|
formData.append('usuario', form.usuario);
|
|
if (form.password) formData.append('password', form.password);
|
|
formData.append('patente', form.patente);
|
|
formData.append('efirma', form.efirma);
|
|
if (form.key) formData.append('key', form.key);
|
|
if (form.cer) formData.append('cer', form.cer);
|
|
formData.append('is_importador', form.is_importador);
|
|
formData.append('acusecove', form.acusecove);
|
|
formData.append('acuseedocument', form.acuseedocument);
|
|
formData.append('is_active', form.is_active);
|
|
try {
|
|
const res = await putFormDataWithAuth(`${API_URL}/vucem/vucem/${editVucem.id}/`, formData);
|
|
if (!res.ok) throw new Error('Error al actualizar');
|
|
await fetchVucem();
|
|
closeModals();
|
|
} catch (err) {
|
|
alert('Error al actualizar');
|
|
}
|
|
}}>
|
|
|
|
{/* Sección de Información Básica */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-blue-600 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-slate-800">Información Básica del Usuario</h4>
|
|
<p className="text-xs text-slate-600">Datos de identificación del usuario VU</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Usuario VU <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
name="usuario"
|
|
value={form.usuario}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm"
|
|
placeholder="Ingrese el nombre de usuario VU/RFC"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Número de Patente <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
name="patente"
|
|
value={form.patente}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm"
|
|
placeholder="Ingrese el número de patente"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Credenciales */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-slate-700 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-slate-800">Credenciales de Seguridad</h4>
|
|
<p className="text-xs text-slate-600">Información de autenticación del sistema</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-gray-700">
|
|
Password <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
name="password"
|
|
type={showEditPassword ? "text" : "password"}
|
|
value={form.password}
|
|
onChange={handleInputChange}
|
|
className="w-full px-3 py-2 pr-20 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-sm"
|
|
placeholder="Ingrese el password"
|
|
/>
|
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
|
<button
|
|
type="button"
|
|
onClick={toggleEditPasswordVisibility}
|
|
className="px-2 py-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title={showEditPassword ? "Ocultar password" : "Mostrar password"}
|
|
>
|
|
{showEditPassword ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L21.536 21.536" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyToClipboard(form.password, 'Password')}
|
|
className="px-2 py-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title="Copiar password"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-gray-700">
|
|
e.firma <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
name="efirma"
|
|
type={showEditEfirma ? "text" : "password"}
|
|
value={form.efirma}
|
|
onChange={handleInputChange}
|
|
required
|
|
className="w-full px-3 py-2 pr-20 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-sm"
|
|
placeholder="Ingrese la e.firma"
|
|
/>
|
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
|
<button
|
|
type="button"
|
|
onClick={toggleEditEfirmaVisibility}
|
|
className="px-2 py-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title={showEditEfirma ? "Ocultar e.firma" : "Mostrar e.firma"}
|
|
>
|
|
{showEditEfirma ? (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L21.536 21.536" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyToClipboard(form.efirma, 'e.firma')}
|
|
className="px-2 py-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
|
title="Copiar e.firma"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Archivos de Certificado */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-green-700 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" 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>
|
|
<h4 className="text-sm font-semibold text-slate-800">Certificados Digitales</h4>
|
|
<p className="text-xs text-slate-600">Archivos de certificado y clave privada</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Archivo de Clave (.key)
|
|
</label>
|
|
<input
|
|
name="key"
|
|
type="file"
|
|
accept=".key"
|
|
onChange={handleInputChange}
|
|
className="w-full px-3 py-2 border border-slate-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 file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-green-100 file:text-green-800 hover:file:bg-green-200 text-sm"
|
|
/>
|
|
{form.key && (
|
|
<div className="flex items-center text-xs text-green-700 bg-green-100 px-2 py-1.5 rounded-md border border-green-200">
|
|
<svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{form.key.name}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-semibold text-slate-700">
|
|
Archivo de Certificado (.cer)
|
|
</label>
|
|
<input
|
|
name="cer"
|
|
type="file"
|
|
accept=".cer"
|
|
onChange={handleInputChange}
|
|
className="w-full px-3 py-2 border border-slate-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 file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-green-100 file:text-green-800 hover:file:bg-green-200 text-sm"
|
|
/>
|
|
{form.cer && (
|
|
<div className="flex items-center text-xs text-green-700 bg-green-100 px-2 py-1.5 rounded-md border border-green-200">
|
|
<svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{form.cer.name}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Configuraciones */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
|
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
|
|
<div className="bg-amber-600 rounded-lg p-2 mr-3 shadow-sm">
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-slate-800">Configuraciones del Sistema</h4>
|
|
<p className="text-xs text-slate-600">Opciones y permisos de la credencial</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="is_importador"
|
|
type="checkbox"
|
|
checked={form.is_importador}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Importador</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="acusecove"
|
|
type="checkbox"
|
|
checked={form.acusecove}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Acuse COVE</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="acuseedocument"
|
|
type="checkbox"
|
|
checked={form.acuseedocument}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Acuse e-Document</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
|
<input
|
|
name="is_active"
|
|
type="checkbox"
|
|
checked={form.is_active}
|
|
onChange={handleInputChange}
|
|
className="w-4 h-4 text-green-600 border-slate-300 rounded focus:ring-green-500 focus:ring-1"
|
|
/>
|
|
<label className="text-xs font-semibold text-slate-700">Credencial Activa</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botones de acción */}
|
|
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 pt-4 border-t border-slate-200">
|
|
<button
|
|
type="button"
|
|
onClick={closeModals}
|
|
className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-700 to-blue-900 hover:from-blue-800 hover:to-blue-950 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 flex items-center justify-center space-x-2"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
<span>Actualizar Credencial</span>
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal de eliminación */}
|
|
{showDeleteModal && deleteVucem && (
|
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
|
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-96 shadow-lg rounded-md bg-white">
|
|
<div className="mt-3 text-center">
|
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
|
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">¿Eliminar Credencial?</h3>
|
|
<div className="mt-2 px-7 py-3">
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
¿Estás seguro que deseas eliminar el registro <span className="font-semibold">{deleteVucem.usuario}</span>?
|
|
</p>
|
|
<p className="text-sm text-red-600">Esta acción no se puede deshacer.</p>
|
|
</div>
|
|
<div className="flex justify-center space-x-3 pt-4">
|
|
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50">Cancelar</button>
|
|
<button type="button" onClick={async () => {
|
|
if (!deleteVucem) return;
|
|
try {
|
|
const res = await deleteWithAuth(`${API_URL}/vucem/vucem/${deleteVucem.id}/`);
|
|
if (!res.ok) throw new Error('Error al eliminar');
|
|
await fetchVucem();
|
|
closeModals();
|
|
} catch (err) {
|
|
alert('Error al eliminar');
|
|
}
|
|
}} className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors disabled:opacity-50 flex items-center">Eliminar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|