Primera version para produccion
This commit is contained in:
16
docker-compose.dev.yml
Normal file
16
docker-compose.dev.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: efc_frontend_dev:latest
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
command: ["npm", "run", "dev", "--", "--host"]
|
||||
restart: unless-stopped
|
||||
@@ -1,4 +1,5 @@
|
||||
import Documents from './pages/Documents';
|
||||
import Agenda from './pages/Agenda';
|
||||
import Vucem from './pages/Vucem';
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { UserProvider } from './context/UserContext';
|
||||
@@ -101,6 +102,12 @@ function AppContent() {
|
||||
<Procesos />
|
||||
</RequireAuth>
|
||||
} />
|
||||
{/* Ruta para agenda */}
|
||||
<Route path="/agenda" element={
|
||||
<RequireAuth>
|
||||
<Agenda />
|
||||
</RequireAuth>
|
||||
} />
|
||||
{/* Ruta para Uso de Almacenamiento */}
|
||||
<Route path="/tablero/almacenamiento" element={
|
||||
<RequireAuth>
|
||||
|
||||
@@ -5,6 +5,17 @@ import { useUser } from '../context/UserContext';
|
||||
export default function Sidebar() {
|
||||
// Leer si el usuario es importador desde localStorage
|
||||
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
|
||||
// Leer grupos del usuario desde localStorage
|
||||
let userGroups = [];
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
userGroups = JSON.parse(localStorage.getItem('user_groups') || '[]');
|
||||
} catch {
|
||||
userGroups = [];
|
||||
}
|
||||
}
|
||||
// Si los grupos son exactamente [3,5]
|
||||
const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -33,15 +44,22 @@ export default function Sidebar() {
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Mi Organización',
|
||||
path: '/organization',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
// Ocultar 'Mi Organización' si es importador o si esGroup35
|
||||
...(
|
||||
(!isImportador && !isGroup35)
|
||||
? [
|
||||
{
|
||||
name: 'Mi Organización',
|
||||
path: '/organization',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []
|
||||
)
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -55,6 +73,16 @@ export default function Sidebar() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Agenda',
|
||||
path: '/agenda',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -92,50 +120,62 @@ export default function Sidebar() {
|
||||
]
|
||||
},
|
||||
// Nueva sección Tableros
|
||||
{
|
||||
title: 'Tableros',
|
||||
items: [
|
||||
{
|
||||
name: 'Uso de Almacenamiento',
|
||||
path: '/tablero/almacenamiento',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Acceso a Usuarios',
|
||||
items: [
|
||||
...(
|
||||
isImportador
|
||||
? []
|
||||
: [
|
||||
...(
|
||||
isGroup35
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: 'Tableros',
|
||||
items: [
|
||||
{
|
||||
name: 'Usuarios',
|
||||
path: '/users',
|
||||
name: 'Uso de Almacenamiento',
|
||||
path: '/tablero/almacenamiento',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
),
|
||||
{
|
||||
name: 'Vucem',
|
||||
path: '/vucem',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
),
|
||||
...(
|
||||
isGroup35
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: 'Acceso a Usuarios',
|
||||
items: [
|
||||
...(
|
||||
isImportador
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'Usuarios',
|
||||
path: '/users',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
),
|
||||
{
|
||||
name: 'Vucem',
|
||||
path: '/vucem',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
)
|
||||
];
|
||||
|
||||
// Filtrar secciones según si es importador
|
||||
|
||||
@@ -18,6 +18,17 @@ import { colors } from '../theme';
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
export default function Admin() {
|
||||
// Leer grupos del usuario desde localStorage
|
||||
let userGroups = [];
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
userGroups = JSON.parse(localStorage.getItem('user_groups') || '[]');
|
||||
} catch {
|
||||
userGroups = [];
|
||||
}
|
||||
}
|
||||
// Si los grupos son exactamente [3,5]
|
||||
const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5);
|
||||
// Estado de servicios
|
||||
const [services, setServices] = useState(null);
|
||||
// Estado de descargas
|
||||
@@ -285,7 +296,7 @@ export default function Admin() {
|
||||
</div>
|
||||
</div>
|
||||
{/* Análisis de actividad de usuario */}
|
||||
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && (
|
||||
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && !isGroup35 && (
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 mb-4 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.65s forwards',
|
||||
|
||||
13
src/pages/Agenda.jsx
Normal file
13
src/pages/Agenda.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Agenda() {
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-blue-900 mb-4">Agenda</h1>
|
||||
<p className="text-lg text-gray-700 mb-6"></p>
|
||||
<div className="bg-white rounded-lg shadow p-6 text-gray-600">
|
||||
<p>¡Bienvenido a la Agenda! Próximamente podrás ver y administrar tus actividades aquí.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -266,34 +266,7 @@ export default function Organization() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Acciones */}
|
||||
<div className="mt-8 bg-white rounded-xl shadow-lg border border-gray-200 p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-lg flex items-center justify-center shadow-lg mr-4">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Acciones</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg">
|
||||
<svg className="w-5 h-5 mr-2" 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>
|
||||
Editar Organización
|
||||
</button>
|
||||
|
||||
<button className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 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-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
Ver Reportes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* ...existing code... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
|
||||
|
||||
|
||||
|
||||
|
||||
export default function Procesos() {
|
||||
const [procesos, setProcesos] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -21,6 +22,10 @@ export default function Procesos() {
|
||||
const [estadoFilter, setEstadoFilter] = useState('');
|
||||
const [servicioFilter, setServicioFilter] = useState('');
|
||||
|
||||
// Sorting
|
||||
const [sortField, setSortField] = useState('');
|
||||
const [sortOrder, setSortOrder] = useState('asc'); // 'asc' | 'desc'
|
||||
|
||||
// Estado para loading de ejecución de servicio
|
||||
const [executingId, setExecutingId] = useState(null);
|
||||
|
||||
@@ -34,6 +39,9 @@ export default function Procesos() {
|
||||
let endpoint = '';
|
||||
// Determinar endpoint según el tipo de servicio
|
||||
switch (proc.servicio) {
|
||||
case 3:
|
||||
endpoint = '/services/pedimento_completo';
|
||||
break;
|
||||
case 4: // Partidas
|
||||
endpoint = '/services/partidas';
|
||||
break;
|
||||
@@ -43,7 +51,10 @@ export default function Procesos() {
|
||||
case 6: // Acuse
|
||||
endpoint = '/services/acuse';
|
||||
break;
|
||||
case 8: // Acuse Cove
|
||||
case 8: // Coves
|
||||
endpoint = '/services/Coves';
|
||||
break;
|
||||
case 9: // Acuse Cove
|
||||
endpoint = '/services/acuseCove';
|
||||
break;
|
||||
default:
|
||||
@@ -102,7 +113,9 @@ export default function Procesos() {
|
||||
if (pedimentoPedimentoFilter) params.append('pedimento__pedimento', pedimentoPedimentoFilter);
|
||||
if (estadoFilter) params.append('estado', estadoFilter);
|
||||
if (servicioFilter) params.append('servicio', servicioFilter);
|
||||
// ...existing code...
|
||||
if (sortField) {
|
||||
params.append('ordering', (sortOrder === 'desc' ? '-' : '') + sortField);
|
||||
}
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`, { headers });
|
||||
@@ -117,7 +130,7 @@ export default function Procesos() {
|
||||
}
|
||||
}
|
||||
fetchProcesos();
|
||||
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter]);
|
||||
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 min-h-screen">
|
||||
@@ -131,6 +144,9 @@ export default function Procesos() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
Procesos del Sistema
|
||||
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in" title="Total de procesos">
|
||||
{count}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Estado actual de los procesos de la agencia aduanal</p>
|
||||
</div>
|
||||
@@ -160,10 +176,19 @@ export default function Procesos() {
|
||||
.animate-fadein-slideup {
|
||||
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
|
||||
}
|
||||
@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;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
|
||||
<h2 className="text-2xl font-bold text-blue-800 mb-6">Procesamiento de Pedimentos</h2>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-2xl font-bold text-blue-800">Procesamiento de Pedimentos</h2>
|
||||
</div>
|
||||
{/* Filtros */}
|
||||
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
@@ -206,9 +231,10 @@ export default function Procesos() {
|
||||
<option value="6">Acuse</option>
|
||||
<option value="7">EDocument</option>
|
||||
<option value="8">Cove</option>
|
||||
<option value="9">Acuse Cove</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
{/* ...filtros anteriores... */}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-500 py-8">Cargando procesos...</div>
|
||||
@@ -220,14 +246,51 @@ export default function Procesos() {
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
|
||||
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
|
||||
<tr>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">ID</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Organización</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Estado</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Pedimento</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Servicio</th>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
|
||||
onClick={() => {
|
||||
setSortField('id');
|
||||
setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
}}
|
||||
>
|
||||
ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
|
||||
onClick={() => {
|
||||
setSortField('organizacion_name');
|
||||
setSortOrder(sortField === 'organizacion_name' && sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
}}
|
||||
>
|
||||
Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
|
||||
onClick={() => {
|
||||
setSortField('estado');
|
||||
setSortOrder(sortField === 'estado' && sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
}}
|
||||
>
|
||||
Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
|
||||
onClick={() => {
|
||||
setSortField('pedimento');
|
||||
setSortOrder(sortField === 'pedimento' && sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
}}
|
||||
>
|
||||
Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
|
||||
onClick={() => {
|
||||
setSortField('servicio');
|
||||
setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
}}
|
||||
>
|
||||
Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(12 * 40px)' }}>
|
||||
{procesos.length === 0 ? (
|
||||
<tr>
|
||||
@@ -259,6 +322,7 @@ export default function Procesos() {
|
||||
: proc.servicio === 6 ? 'Acuse'
|
||||
: proc.servicio === 7 ? 'EDocument'
|
||||
: proc.servicio === 8 ? 'Cove'
|
||||
: proc.servicio === 9 ? 'Acuse Cove'
|
||||
: String(proc.servicio)
|
||||
}</td>
|
||||
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">
|
||||
@@ -277,11 +341,21 @@ export default function Procesos() {
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 text-xs text-blue-700 hover:bg-blue-100 disabled:opacity-60"
|
||||
onClick={() => handleEjecutarServicio(proc)}
|
||||
disabled={executingId === proc.id}
|
||||
disabled={
|
||||
executingId === proc.id ||
|
||||
proc.estado === 2 || // Procesando
|
||||
proc.estado === 3 || // Finalizado
|
||||
proc.estado === 4 // Error
|
||||
}
|
||||
>
|
||||
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
|
||||
</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Pasar a espera</button>
|
||||
<button
|
||||
className={`block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100${(proc.estado === 2 || proc.estado === 4) ? '' : ' opacity-50 cursor-not-allowed'}`}
|
||||
disabled={!(proc.estado === 2 || proc.estado === 4)}
|
||||
>
|
||||
Pasar a espera
|
||||
</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Editar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,532 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const API_URL = 'http://192.168.1.195:8000/api/v1/vucem/vucem/';
|
||||
|
||||
export default function Vucem() {
|
||||
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: '',
|
||||
key: null,
|
||||
cer: null,
|
||||
is_importador: false,
|
||||
acusecove: false,
|
||||
acuseedocument: false,
|
||||
is_active: true,
|
||||
};
|
||||
const [form, setForm] = useState(initialForm);
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// Fetch list
|
||||
const fetchVucem = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(API_URL, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error('Error al cargar VUCEM');
|
||||
const data = await res.json();
|
||||
setVucemList(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Error al cargar VUCEM');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
// Table y header estilo Users.jsx
|
||||
return (
|
||||
<div className="p-8 min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex flex-col items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xl w-full text-center">
|
||||
<h1 className="text-3xl font-extrabold text-blue-800 mb-4">Vucem</h1>
|
||||
<p className="text-gray-600 mb-6">Esta es la vista de integración con VUCEM. Aquí podrás consultar, gestionar o integrar funcionalidades relacionadas con la Ventanilla Única de Comercio Exterior Mexicana.</p>
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<svg className="w-16 h-16 text-blue-400 mx-auto" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header Mejorado igual que Users.jsx */}
|
||||
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6">
|
||||
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
||||
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
VUCEM
|
||||
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">{vucemList.length}</span>
|
||||
<span className="inline-block bg-lime-200 text-lime-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in" title="Activos">
|
||||
Activos: {vucemList.filter(v => v.is_active).length}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Gestiona y supervisa los accesos y certificados VUCEM registrados en el sistema.</p>
|
||||
</div>
|
||||
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="text-blue-700 font-semibold">Próximamente podrás ver información y acciones de VUCEM aquí.</span>
|
||||
</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;
|
||||
}
|
||||
`}</style>
|
||||
{/* Filtros y botón Crear VUCEM igual que Users.jsx */}
|
||||
<div className="bg-white shadow rounded-lg mb-6">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex-1 min-w-0 max-w-md">
|
||||
<div className="relative bg-gray-50 rounded-lg border border-gray-200 shadow-sm">
|
||||
<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-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:bg-white block w-full pl-10 pr-10 py-3 sm:text-sm border-0 bg-transparent rounded-lg placeholder-gray-500 transition-all duration-200"
|
||||
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-200"
|
||||
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>
|
||||
<div className="mt-4 sm:mt-0 sm:ml-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-indigo-600 to-indigo-700 hover:from-indigo-700 hover:to-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Crear VUCEM
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Tabla igual que Users.jsx */}
|
||||
<div className="overflow-hidden">
|
||||
<div style={{ minHeight: 'calc(8 * 56px)', maxHeight: 'calc(8 * 56px)', overflowY: 'auto', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden">
|
||||
<thead className="bg-gray-50 sticky top-0 z-20">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Usuario</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Patente</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Key</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cer</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Activo</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 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 }}>
|
||||
<span className="text-gray-500 text-lg">Cargando VUCEM...</span>
|
||||
</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 }}>
|
||||
<span className="text-danger-600 text-lg">Error: {error}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : paginatedList.length > 0 ? (
|
||||
paginatedList.map((vucem, idx) => (
|
||||
<tr
|
||||
key={vucem.id}
|
||||
className={
|
||||
`transition-all duration-300 hover:scale-[1.025] hover:shadow-lg hover:bg-indigo-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 text-sm font-medium text-gray-900">{vucem.usuario}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{vucem.patente}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{vucem.key ? (
|
||||
<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>
|
||||
Archivo cargado
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-400">
|
||||
<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>
|
||||
Sin archivo
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{vucem.cer ? (
|
||||
<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>
|
||||
Archivo cargado
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-400">
|
||||
<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>
|
||||
Sin archivo
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
{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 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>
|
||||
Inactivo
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<button onClick={() => { setEditVucem(vucem); setShowEditModal(true); }} className="text-indigo-600 hover:underline font-semibold mr-3">Editar</button>
|
||||
<button onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }} className="text-red-600 hover:underline font-semibold">Eliminar</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-gray-500">No hay registros</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Paginación igual que Users.jsx */}
|
||||
{filteredList.length > 0 && (
|
||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
||||
<div className="flex items-center mb-4 sm:mb-0">
|
||||
<span className="text-sm text-gray-700">
|
||||
Mostrando <span className="font-semibold">{filteredList.length}</span> de <span className="font-semibold">{vucemList.length}</span> registros
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className={`px-3 py-1 rounded-lg border text-base font-semibold transition-colors ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-gray-700 border-gray-300 hover:bg-indigo-50 hover:text-indigo-700'}`}
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<span className="mx-2 text-base font-medium text-gray-700">Página {page} de {totalPages}</span>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className={`px-3 py-1 rounded-lg border text-base font-semibold transition-colors ${page === totalPages ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-gray-700 border-gray-300 hover:bg-indigo-50 hover:text-indigo-700'}`}
|
||||
>
|
||||
Siguiente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de creación */}
|
||||
{showCreateModal && (
|
||||
<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-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Crear VUCEM</h3>
|
||||
<button onClick={closeModals} className="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg className="w-6 h-6" 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>
|
||||
<form className="space-y-6 w-full" onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData();
|
||||
formData.append('usuario', form.usuario);
|
||||
formData.append('password', form.password);
|
||||
formData.append('patente', form.patente);
|
||||
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 token = localStorage.getItem('access');
|
||||
const res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) throw new Error('Error al crear VUCEM');
|
||||
await fetchVucem();
|
||||
closeModals();
|
||||
} catch (err) {
|
||||
alert('Error al crear VUCEM');
|
||||
}
|
||||
}}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Usuario *</label>
|
||||
<input name="usuario" value={form.usuario} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="usuario" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Password *</label>
|
||||
<input name="password" type="password" value={form.password} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="password" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Patente *</label>
|
||||
<input name="patente" value={form.patente} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="patente" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Key (.key)</label>
|
||||
<input name="key" type="file" accept=".key" onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm" />
|
||||
{form.key && <span className="text-xs text-gray-500">{form.key.name}</span>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Cer (.cer)</label>
|
||||
<input name="cer" type="file" accept=".cer" onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm" />
|
||||
{form.cer && <span className="text-xs text-gray-500">{form.cer.name}</span>}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="is_importador" type="checkbox" checked={form.is_importador} onChange={handleInputChange} />
|
||||
<label className="text-sm">Importador</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="acusecove" type="checkbox" checked={form.acusecove} onChange={handleInputChange} />
|
||||
<label className="text-sm">Acuse COVE</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="acuseedocument" type="checkbox" checked={form.acuseedocument} onChange={handleInputChange} />
|
||||
<label className="text-sm">Acuse e-Document</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="is_active" type="checkbox" checked={form.is_active} onChange={handleInputChange} />
|
||||
<label className="text-sm">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<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-indigo-500 transition-colors disabled:opacity-50">Cancelar</button>
|
||||
<button type="submit" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center">Crear</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal de edición */}
|
||||
{showEditModal && editVucem && (
|
||||
<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-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Editar VUCEM</h3>
|
||||
<button onClick={closeModals} className="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg className="w-6 h-6" 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>
|
||||
<form className="space-y-6 w-full" 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);
|
||||
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 token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}${editVucem.id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) throw new Error('Error al actualizar VUCEM');
|
||||
await fetchVucem();
|
||||
closeModals();
|
||||
} catch (err) {
|
||||
alert('Error al actualizar VUCEM');
|
||||
}
|
||||
}}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Usuario *</label>
|
||||
<input name="usuario" value={form.usuario} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="usuario" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Password (dejar vacío para no cambiar)</label>
|
||||
<input name="password" type="password" value={form.password} onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="password" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Patente *</label>
|
||||
<input name="patente" value={form.patente} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="patente" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Key (.key)</label>
|
||||
<input name="key" type="file" accept=".key" onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm" />
|
||||
{form.key && <span className="text-xs text-gray-500">{form.key.name}</span>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Cer (.cer)</label>
|
||||
<input name="cer" type="file" accept=".cer" onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm" />
|
||||
{form.cer && <span className="text-xs text-gray-500">{form.cer.name}</span>}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="is_importador" type="checkbox" checked={form.is_importador} onChange={handleInputChange} />
|
||||
<label className="text-sm">Importador</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="acusecove" type="checkbox" checked={form.acusecove} onChange={handleInputChange} />
|
||||
<label className="text-sm">Acuse COVE</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="acuseedocument" type="checkbox" checked={form.acuseedocument} onChange={handleInputChange} />
|
||||
<label className="text-sm">Acuse e-Document</label>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input name="is_active" type="checkbox" checked={form.is_active} onChange={handleInputChange} />
|
||||
<label className="text-sm">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<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-indigo-500 transition-colors disabled:opacity-50">Cancelar</button>
|
||||
<button type="submit" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center">Actualizar</button>
|
||||
</div>
|
||||
</form>
|
||||
</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 VUCEM?</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-indigo-500 transition-colors disabled:opacity-50">Cancelar</button>
|
||||
<button type="button" onClick={async () => {
|
||||
if (!deleteVucem) return;
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}${deleteVucem.id}/`, {
|
||||
method: 'DELETE',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error('Error al eliminar VUCEM');
|
||||
await fetchVucem();
|
||||
closeModals();
|
||||
} catch (err) {
|
||||
alert('Error al eliminar VUCEM');
|
||||
}
|
||||
}} 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user