se agregatron cambios a expedientes datastage y documentos
This commit is contained in:
@@ -128,6 +128,7 @@ export default function Datastage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [form, setForm] = useState({ archivo: null, contribuyente: '' });
|
||||
const [importadores, setImportadores] = useState([]);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
@@ -256,9 +257,21 @@ export default function Datastage() {
|
||||
};
|
||||
|
||||
// Abrir modal de creación
|
||||
const openCreateModal = () => {
|
||||
const openCreateModal = async () => {
|
||||
setForm({ archivo: null, contribuyente: '' });
|
||||
setEditingId(null);
|
||||
// Fetch importadores
|
||||
try {
|
||||
const res = await fetchWithAuth(`${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, { method: 'GET' });
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
setImportadores(data);
|
||||
} else {
|
||||
setImportadores([]);
|
||||
}
|
||||
} catch {
|
||||
setImportadores([]);
|
||||
}
|
||||
setShowCreateModal(true);
|
||||
};
|
||||
|
||||
@@ -563,43 +576,132 @@ export default function Datastage() {
|
||||
{/* Modal de creación - estilo Users/Importers */}
|
||||
{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-4">
|
||||
<form onSubmit={handleCreate} className="relative mx-auto w-full max-w-xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-blue-700 to-blue-900 rounded-t-2xl p-4 text-white border-b-2 border-blue-500">
|
||||
<form onSubmit={handleCreate} className="relative mx-auto w-full max-w-2xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||
{/* Header formal con gradiente */}
|
||||
<div className="bg-gradient-to-r from-blue-700 via-blue-800 to-blue-900 rounded-t-2xl p-6 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-xl 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="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-blue-500 bg-opacity-30 backdrop-blur-sm rounded-xl p-3 border border-blue-400 border-opacity-30 shadow-lg">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<ellipse cx="12" cy="7" rx="8" ry="3" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 7v10c0 1.657 3.582 3 8 3s8-1.343 8-3V7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 17c0 1.657 3.582 3 8 3s8-1.343 8-3" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold tracking-wide">Nuevo Datastage</h3>
|
||||
<p className="text-blue-200 text-xs font-medium">Carga un archivo .zip y asigna un contribuyente</p>
|
||||
<h3 className="text-xl font-bold tracking-wide mb-1">Nuevo Datastage</h3>
|
||||
<p className="text-blue-200 text-sm font-medium">Carga un archivo .zip y asigna un contribuyente</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={() => setShowCreateModal(false)} className="text-blue-100 hover:text-white hover:bg-blue-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-blue-500 border-opacity-30">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="text-blue-100 hover:text-white hover:bg-blue-600 transition-all duration-200 p-2 hover:bg-opacity-50 rounded-lg border border-blue-400 border-opacity-30 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-blue-800"
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4 ">
|
||||
{error && <div className="text-red-500 mb-2">{error}</div>}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Archivo (.zip)</label>
|
||||
<input type="file" accept=".zip" className="w-full 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" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} required />
|
||||
<div className="p-6 space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700 text-sm flex items-start space-x-3">
|
||||
<svg className="w-5 h-5 flex-shrink-0 mt-0.5" 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>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sección de Archivo */}
|
||||
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
|
||||
<div className="flex items-center mb-4 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="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">Archivo de Datos</h4>
|
||||
<p className="text-xs text-slate-600">Selecciona el archivo ZIP a procesar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-semibold text-slate-700">
|
||||
Archivo (.zip) <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))}
|
||||
required
|
||||
className="w-full px-4 py-2.5 border-2 border-slate-300 border-dashed rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 text-sm hover:border-blue-500 cursor-pointer file:mr-4 file:py-1 file:px-4 file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 file:rounded-full hover:file:bg-blue-100"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Formato soportado: .ZIP (máximo 100MB)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Contribuyente</label>
|
||||
<input className="w-full 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" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
|
||||
|
||||
{/* Sección de Contribuyente */}
|
||||
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
|
||||
<div className="flex items-center mb-4 pb-2 border-b border-slate-300">
|
||||
<div className="bg-green-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="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>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-800">Datos del Contribuyente</h4>
|
||||
<p className="text-xs text-slate-600">Selecciona el RFC del contribuyente</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-semibold text-slate-700">
|
||||
RFC del Contribuyente <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2.5 border border-slate-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 text-sm"
|
||||
value={form.contribuyente}
|
||||
onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))}
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Selecciona un RFC</option>
|
||||
{importadores.map(imp => (
|
||||
<option key={imp.rfc} value={imp.rfc} className="font-mono">
|
||||
{imp.rfc}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Selecciona el RFC del contribuyente asociado a este datastage
|
||||
</p>
|
||||
</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={() => setShowCreateModal(false)} 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:ring-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Crear</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="w-full sm:w-auto px-6 py-2.5 border border-slate-300 rounded-lg shadow-sm text-sm font-medium 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.5 border border-transparent rounded-lg shadow-lg text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-blue-900 focus:ring-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<span>Crear Datastage</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -677,7 +677,7 @@ export default function Documents() {
|
||||
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
|
||||
className="border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||
>
|
||||
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
|
||||
{[5, 10, 20, 50, 100, 500, 1000].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -380,16 +380,27 @@ export default function Documents() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={refetch}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl 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 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Actualizar Ahora
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={refetch}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl 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 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Actualizar Ahora
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {}}
|
||||
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
|
||||
>
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Descargar Todos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{success && (
|
||||
<div className="mt-4 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4 shadow-sm">
|
||||
@@ -425,6 +436,7 @@ export default function Documents() {
|
||||
<th scope="col" className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Archivos</th>
|
||||
<th scope="col" className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Peso Total</th>
|
||||
<th scope="col" className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expediente</th>
|
||||
<th scope="col" className="px-4 py-2 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">
|
||||
@@ -507,6 +519,17 @@ export default function Documents() {
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-center">
|
||||
<button
|
||||
className="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
|
||||
onClick={() => {}}
|
||||
title="Descargar"
|
||||
>
|
||||
<svg className="h-5 w-5" 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-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -1,326 +1,321 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Line, Pie, Doughnut, Bar } from 'react-chartjs-2';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import fetchWithAuth from '../fetchWithAuth';
|
||||
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
ArcElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, ArcElement, Title, Tooltip, Legend);
|
||||
const initialFilters = {
|
||||
pedimento_app: '',
|
||||
aduana: '',
|
||||
patente: '',
|
||||
regimen: '',
|
||||
agente_aduanal: '',
|
||||
tipo_operacion: '',
|
||||
fecha_pago_gte: '',
|
||||
fecha_pago_lte: '',
|
||||
contribuyente__rfc: '',
|
||||
};
|
||||
|
||||
export default function TableroAlmacenamiento() {
|
||||
// Estado para la tabla de documentos y la opción seleccionada
|
||||
const [selectedMetric, setSelectedMetric] = useState('');
|
||||
const [documentos, setDocumentos] = useState([
|
||||
{ nombre: 'Factura_123.pdf', tipo: 'Factura', ext: 'PDF' },
|
||||
{ nombre: 'Pedimento_456.xml', tipo: 'Pedimento', ext: 'XML' },
|
||||
{ nombre: 'Manifiesto_789.docx', tipo: 'Manifiesto', ext: 'DOCX' },
|
||||
]);
|
||||
const [filters, setFilters] = useState(initialFilters);
|
||||
const [summary, setSummary] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Por ahora solo cambia el estado seleccionado, no fetch
|
||||
const handleMetricClick = (metric) => {
|
||||
setSelectedMetric(metric);
|
||||
// Fetch summary data
|
||||
const fetchSummary = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const params = Object.entries(filters)
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/dashboard/summary/${params ? `?${params}` : ''}`;
|
||||
const res = await fetchWithAuth(url);
|
||||
const data = await res.json();
|
||||
setSummary(data);
|
||||
} catch (err) {
|
||||
setSummary(null);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Datos simulados para las nuevas gráficas y KPIs
|
||||
const tiposArchivos = [
|
||||
{ tipo: 'PDF', espacio: 220 },
|
||||
{ tipo: 'XML', espacio: 120 },
|
||||
{ tipo: 'DOCX', espacio: 80 },
|
||||
{ tipo: 'JPG', espacio: 60 },
|
||||
{ tipo: 'Otros', espacio: 32 },
|
||||
];
|
||||
const topArchivos = [
|
||||
{ nombre: 'Factura_123.pdf', size: 2.5 },
|
||||
{ nombre: 'Reporte_2024.pdf', size: 2.1 },
|
||||
{ nombre: 'Pedimento_456.xml', size: 1.8 },
|
||||
{ nombre: 'Manifiesto_789.docx', size: 1.2 },
|
||||
{ nombre: 'Imagen_001.jpg', size: 1.0 },
|
||||
];
|
||||
const espacioTotal = 1024; // GB
|
||||
const espacioOcupado = 512; // GB
|
||||
const espacioLibre = espacioTotal - espacioOcupado;
|
||||
const usuarios = [
|
||||
{ nombre: 'Juan', docs: 120 },
|
||||
{ nombre: 'Ana', docs: 90 },
|
||||
{ nombre: 'Luis', docs: 70 },
|
||||
{ nombre: 'Sofía', docs: 60 },
|
||||
{ nombre: 'Carlos', docs: 40 },
|
||||
];
|
||||
const docsEsteMes = 45;
|
||||
const docsEliminados = 7;
|
||||
const usuariosActivos = 4;
|
||||
const porcentajeUsado = Math.round((espacioOcupado / espacioTotal) * 100);
|
||||
// Fetch initial data
|
||||
useEffect(() => {
|
||||
fetchSummary();
|
||||
}, []);
|
||||
|
||||
// Handle filter changes
|
||||
const handleFilterChange = (e) => {
|
||||
setFilters({ ...filters, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
// Card components for different sizes
|
||||
const Card = ({ title, children, icon, small }) => (
|
||||
<div className={`bg-white rounded-lg shadow-sm border border-slate-200 p-4 flex flex-col w-full ${small ? 'min-h-[120px]' : 'min-h-[200px]'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{icon && <span className={`${small ? 'text-slate-600' : 'text-blue-600'}`}>{icon}</span>}
|
||||
<span className={`text-sm font-semibold ${small ? 'text-slate-600' : 'text-slate-700'}`}>{title}</span>
|
||||
</div>
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 min-h-screen flex flex-col">
|
||||
{/* Header animado */}
|
||||
<div className="mb-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
||||
<div className="max-w-7xl mx-auto 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="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" />
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-800 px-4 py-5 sm:px-6 shadow">
|
||||
<div className="max-w-7xl mx-auto flex items-center gap-4">
|
||||
<div className="p-2 bg-blue-700 rounded-xl">
|
||||
<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="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>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
Uso de Almacenamiento
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Visualiza y analiza el uso de almacenamiento de la plataforma</p>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Resumen de Cumplimiento</h1>
|
||||
</div>
|
||||
{/* Efecto decorativo de fondo */}
|
||||
<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>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 2.2s infinite;
|
||||
}
|
||||
@keyframes fadein-slideup {
|
||||
0% { opacity: 0; transform: translateY(40px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fadein-slideup {
|
||||
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtros */}
|
||||
<div className="max-w-7xl mx-auto w-full mb-8 flex flex-col md:flex-row gap-4 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.12s forwards' }}>
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Organización</option>
|
||||
<option value="org1">Organización 1</option>
|
||||
<option value="org2">Organización 2</option>
|
||||
</select>
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Importador</option>
|
||||
<option value="imp1">Importador 1</option>
|
||||
<option value="imp2">Importador 2</option>
|
||||
</select>
|
||||
<input type="date" className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" />
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Año</option>
|
||||
<option value="2022">2022</option>
|
||||
<option value="2023">2023</option>
|
||||
<option value="2024">2024</option>
|
||||
<option value="2025">2025</option>
|
||||
</select>
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Mes</option>
|
||||
<option value="01">Enero</option>
|
||||
<option value="02">Febrero</option>
|
||||
<option value="03">Marzo</option>
|
||||
<option value="04">Abril</option>
|
||||
<option value="05">Mayo</option>
|
||||
<option value="06">Junio</option>
|
||||
<option value="07">Julio</option>
|
||||
<option value="08">Agosto</option>
|
||||
<option value="09">Septiembre</option>
|
||||
<option value="10">Octubre</option>
|
||||
<option value="11">Noviembre</option>
|
||||
<option value="12">Diciembre</option>
|
||||
</select>
|
||||
<div className="max-w-7xl mx-auto mt-6 mb-4 px-4">
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
fetchSummary();
|
||||
}} className="bg-white rounded-lg shadow-sm border border-slate-200 p-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{Object.keys(initialFilters).map((key) => (
|
||||
<div key={key}>
|
||||
<label className="block text-xs font-medium text-slate-600 mb-1" htmlFor={key}>
|
||||
{key.replace(/_/g, ' ').replace('gte', 'desde').replace('lte', 'hasta')}
|
||||
</label>
|
||||
<input
|
||||
type={key.includes('fecha') ? 'date' : 'text'}
|
||||
name={key}
|
||||
id={key}
|
||||
value={filters[key]}
|
||||
onChange={handleFilterChange}
|
||||
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Aplicar Filtros
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Cards y KPIs */}
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-6 gap-6 mb-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.18s forwards' }}>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-blue-800 mb-2">1,234</span>
|
||||
<span className="text-sm font-semibold text-blue-700">Total de Pedimentos</span>
|
||||
</div>
|
||||
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-indigo-800 mb-2">8,765</span>
|
||||
<span className="text-sm font-semibold text-indigo-700">Total de Documentos</span>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-green-800 mb-2">{espacioOcupado} GB</span>
|
||||
<span className="text-sm font-semibold text-green-700">Espacio Utilizado</span>
|
||||
</div>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-yellow-800 mb-2">2.5 GB</span>
|
||||
<span className="text-sm font-semibold text-yellow-700">Archivo más grande</span>
|
||||
</div>
|
||||
<div className="bg-pink-50 border border-pink-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-pink-800 mb-2">120 MB</span>
|
||||
<span className="text-sm font-semibold text-pink-700">Tamaño promedio</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-gray-800 mb-2">{espacioLibre} GB</span>
|
||||
<span className="text-sm font-semibold text-gray-700">Espacio Libre</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 animate-fadein-slideup opacity-0">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-blue-700 mb-1">{porcentajeUsado}%</span>
|
||||
<span className="text-xs text-gray-600">% Espacio Usado</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-green-700 mb-1">{docsEsteMes}</span>
|
||||
<span className="text-xs text-gray-600">Docs subidos este mes</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-red-700 mb-1">{docsEliminados}</span>
|
||||
<span className="text-xs text-gray-600">Docs eliminados este mes</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-indigo-700 mb-1">{usuariosActivos}</span>
|
||||
<span className="text-xs text-gray-600">Usuarios activos este mes</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Cards */}
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mr-4"></div>
|
||||
<span className="text-slate-600">Cargando resumen...</span>
|
||||
</div>
|
||||
) : summary ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
{/* Pedimentos */}
|
||||
<Card
|
||||
title="Pedimentos"
|
||||
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 17l4 4 4-4m-4-5v9" /></svg>}
|
||||
>
|
||||
<div className="text-2xl font-bold text-blue-700">{summary.pedimentos?.total ?? '-'}</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
|
||||
<div>
|
||||
<span className="block text-slate-500">Completos</span>
|
||||
<span className="block font-semibold">{summary.pedimentos?.completos ?? '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-slate-500">Pendientes</span>
|
||||
<span className="block font-semibold">{summary.pedimentos?.pendientes ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">Cumplimiento</span>
|
||||
<div className="w-full bg-slate-100 rounded h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded"
|
||||
style={{ width: `${summary.pedimentos?.cumplimiento ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-xs text-right text-blue-700 font-semibold mt-1">
|
||||
{summary.pedimentos?.cumplimiento ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t grid grid-cols-2 gap-4">
|
||||
{/* Documentos */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16h8M8 12h8M8 8h8" />
|
||||
</svg>
|
||||
<span className="text-xs font-semibold text-slate-600">Documentos</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className="text-lg font-bold text-slate-700">{summary.documentos?.descargados ?? '-'}</div>
|
||||
<span className="block text-xs text-slate-500">Descargados</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gráficas */}
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-3 gap-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.22s forwards' }}>
|
||||
{/* Gráfica 1: Espacio utilizado a lo largo del tiempo */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-blue-800 mb-4">Espacio utilizado a lo largo del tiempo</h2>
|
||||
<Line
|
||||
data={{
|
||||
labels: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Espacio Utilizado (GB)',
|
||||
data: [100, 150, 200, 250, 300, 400, 512],
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59,130,246,0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
title: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Mes' } },
|
||||
y: { title: { display: true, text: 'GB' } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Gráfica 2: Distribución de tipos de archivo */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-purple-800 mb-4">Distribución por tipo de archivo</h2>
|
||||
<Pie
|
||||
data={{
|
||||
labels: tiposArchivos.map(t => t.tipo),
|
||||
datasets: [
|
||||
{
|
||||
data: tiposArchivos.map(t => t.espacio),
|
||||
backgroundColor: ['#3b82f6', '#6366f1', '#f59e42', '#10b981', '#f472b6'],
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Gráfica 3: Espacio ocupado vs libre (donut) */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-green-800 mb-4">Espacio ocupado vs libre</h2>
|
||||
<Doughnut
|
||||
data={{
|
||||
labels: ['Ocupado', 'Libre'],
|
||||
datasets: [
|
||||
{
|
||||
data: [espacioOcupado, espacioLibre],
|
||||
backgroundColor: ['#3b82f6', '#d1fae5'],
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
cutout: '70%',
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Gráficas adicionales */}
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-2 gap-8 mt-8 animate-fadein-slideup opacity-0">
|
||||
{/* Top archivos más grandes */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-yellow-800 mb-4">Top 5 archivos más grandes</h2>
|
||||
<Bar
|
||||
data={{
|
||||
labels: topArchivos.map(a => a.nombre),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tamaño (GB)',
|
||||
data: topArchivos.map(a => a.size),
|
||||
backgroundColor: '#f59e42',
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Tamaño (GB)' } },
|
||||
y: { title: { display: false } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Documentos subidos por usuario */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-indigo-800 mb-4">Documentos subidos por usuario</h2>
|
||||
<Bar
|
||||
data={{
|
||||
labels: usuarios.map(u => u.nombre),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Documentos',
|
||||
data: usuarios.map(u => u.docs),
|
||||
backgroundColor: '#6366f1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: false } },
|
||||
y: { title: { display: true, text: 'Documentos' } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Remesas */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3" />
|
||||
</svg>
|
||||
<span className="text-xs font-semibold text-slate-600">Remesas</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<div className="text-lg font-bold text-slate-700">{summary.remesas?.total ?? '-'}</div>
|
||||
<span className="block text-xs text-slate-500">Total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Partidas */}
|
||||
<Card
|
||||
title="Partidas"
|
||||
icon={<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>}
|
||||
>
|
||||
<div className="text-2xl font-bold text-blue-700">{summary.partidas?.total ?? '-'}</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
|
||||
<div>
|
||||
<span className="block text-slate-500">Descargadas</span>
|
||||
<span className="block font-semibold">{summary.partidas?.partidas_descargadas ?? '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-slate-500">Pendientes</span>
|
||||
<span className="block font-semibold">{summary.partidas?.partidas_pendientes ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">Cumplimiento</span>
|
||||
<div className="w-full bg-slate-100 rounded h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded"
|
||||
style={{ width: `${summary.partidas?.cumplimiento ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-xs text-right text-blue-700 font-semibold mt-1">
|
||||
{summary.partidas?.cumplimiento ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* COVES */}
|
||||
<Card
|
||||
title="COVES"
|
||||
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 8h10M7 12h10M7 16h10" /></svg>}
|
||||
>
|
||||
<div className="text-2xl font-bold text-blue-700">{summary.coves?.total ?? '-'}</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
|
||||
<div>
|
||||
<span className="block text-slate-500">Procesados</span>
|
||||
<span className="block font-semibold">{summary.coves?.coves_procesados ?? '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-slate-500">Pendientes</span>
|
||||
<span className="block font-semibold">{summary.coves?.coves_pendientes ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">Cumplimiento</span>
|
||||
<div className="w-full bg-slate-100 rounded h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded"
|
||||
style={{ width: `${summary.coves?.coves_cumplimiento ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-xs text-right text-blue-700 font-semibold mt-1">
|
||||
{summary.coves?.coves_cumplimiento ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 border-t pt-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">Acuses</span>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="block text-slate-500">Procesados</span>
|
||||
<span className="block font-semibold">{summary.coves?.acuse_coves_procesados ?? '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-slate-500">Pendientes</span>
|
||||
<span className="block font-semibold">{summary.coves?.acuse_coves_pendientes ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-400 h-2 rounded"
|
||||
style={{ width: `${summary.coves?.acuse_coves_cumplimiento ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-xs text-right text-blue-400 font-semibold mt-1">
|
||||
{summary.coves?.acuse_coves_cumplimiento ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* EDocuments */}
|
||||
<Card
|
||||
title="EDocuments"
|
||||
icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v16h16V4H4zm4 4h8v8H8V8z" /></svg>}
|
||||
>
|
||||
<div className="text-2xl font-bold text-blue-700">{summary.edocuments?.total ?? '-'}</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2 text-xs">
|
||||
<div>
|
||||
<span className="block text-slate-500">Descargados</span>
|
||||
<span className="block font-semibold">{summary.edocuments?.edocs_descargados ?? '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-slate-500">Pendientes</span>
|
||||
<span className="block font-semibold">{summary.edocuments?.edocs_pendientes ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">Cumplimiento</span>
|
||||
<div className="w-full bg-slate-100 rounded h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded"
|
||||
style={{ width: `${summary.edocuments?.edocs_cumplimiento ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-xs text-right text-blue-700 font-semibold mt-1">
|
||||
{summary.edocuments?.edocs_cumplimiento ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 border-t pt-2">
|
||||
<span className="block text-xs text-slate-500 mb-1">Acuses</span>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="block text-slate-500">Descargados</span>
|
||||
<span className="block font-semibold">{summary.edocuments?.acuse_descargados ?? '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-slate-500">Pendientes</span>
|
||||
<span className="block font-semibold">{summary.edocuments?.acuses_pendientes ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 rounded h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-400 h-2 rounded"
|
||||
style={{ width: `${summary.edocuments?.acuses_cumplimiento ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-xs text-right text-blue-400 font-semibold mt-1">
|
||||
{summary.edocuments?.acuses_cumplimiento ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-slate-500 py-12">No hay datos para mostrar.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ const initialForm = {
|
||||
};
|
||||
|
||||
export default function Users() {
|
||||
// Estado para RFC importadores
|
||||
const [importadores, setImportadores] = useState([]);
|
||||
// Inyectar animaciones solo una vez en el cliente
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && !document.getElementById('users-animations')) {
|
||||
@@ -561,7 +563,22 @@ export default function Users() {
|
||||
<span className="sm:hidden">Agente</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowCreateModal(true); setCreateType('importador'); }}
|
||||
onClick={async () => {
|
||||
setCreateType('importador');
|
||||
// Fetch importadores RFC
|
||||
try {
|
||||
const res = await fetch(`${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, { method: 'GET', headers: { 'Authorization': `Bearer ${localStorage.getItem('access')}` } });
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
setImportadores(data);
|
||||
} else {
|
||||
setImportadores([]);
|
||||
}
|
||||
} catch {
|
||||
setImportadores([]);
|
||||
}
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
@@ -1185,17 +1202,19 @@ export default function Users() {
|
||||
<label className="block text-xs font-semibold text-slate-700">
|
||||
RFC del Importador <span className="text-red-600">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
<select
|
||||
name="rfc"
|
||||
value={form.rfc || ''}
|
||||
onChange={handleChange}
|
||||
required
|
||||
maxLength="13"
|
||||
className="w-full px-3 py-2 border border-green-300 rounded-md shadow-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm font-mono uppercase"
|
||||
placeholder="RFC13CARACTERES"
|
||||
className="w-full px-3 py-2 border border-green-300 rounded-md shadow-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white text-slate-900 text-sm font-mono uppercase"
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
/>
|
||||
>
|
||||
<option value="" disabled>Selecciona un RFC</option>
|
||||
{importadores.map(imp => (
|
||||
<option key={imp.rfc} value={imp.rfc}>{imp.rfc}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-green-600 mt-1">Formato: 12-13 caracteres (ABCD123456ABC)</p>
|
||||
</div>
|
||||
<input type="hidden" name="is_importador" value="true" />
|
||||
|
||||
Reference in New Issue
Block a user