Compare commits
24 Commits
feature/ch
...
T2025-10-1
| Author | SHA1 | Date | |
|---|---|---|---|
| 769a1fd4e8 | |||
| 70f0b38e93 | |||
| d8c23dcf09 | |||
| 756815983d | |||
| 33ca76c054 | |||
|
|
6acb55c563 | ||
| bdabc94974 | |||
|
|
5387eb25cf | ||
| 06c5d32ae0 | |||
| fd4fe5dc2b | |||
| 0c4a48a60b | |||
| f845629b81 | |||
| 5e50d6bac0 | |||
| 539954eb41 | |||
| e69fca99c0 | |||
| 63ab45856f | |||
| e371af3706 | |||
| 3f640307f8 | |||
| 4660ed59a7 | |||
| 791bd2f87e | |||
| 5f4a797c3c | |||
| 5c8380a772 | |||
| 03a9f793b1 | |||
| c924aab9c1 |
5
.env
5
.env
@@ -1,5 +0,0 @@
|
||||
VITE_DEBUG_MODE=true
|
||||
|
||||
VITE_EFC_API_URL=http://192.168.1.79:8000/api/v1
|
||||
VITE_EFC_MICROSERVICE_URL=http://192.168.1.79:8001/api/v1
|
||||
VITE_EFC_MICROSERVICE_URL_2=http://192.168.1.79:8001/api/v2
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
*.bak
|
||||
95
package-lock.json
generated
95
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"chart.js": "^4.5.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jszip": "^3.10.1",
|
||||
"react": "^19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@@ -1706,6 +1707,12 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2420,6 +2427,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
@@ -2429,6 +2442,12 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
@@ -2488,6 +2507,12 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
@@ -2531,6 +2556,27 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
@@ -2703,6 +2749,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"license": "MIT"
|
||||
@@ -2919,6 +2971,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
@@ -3029,6 +3087,21 @@
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"dev": true,
|
||||
@@ -3069,6 +3142,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
@@ -3082,11 +3161,26 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
@@ -3727,7 +3821,6 @@
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"chart.js": "^4.5.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jszip": "^3.10.1",
|
||||
"react": "^19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
||||
68
src/api/pedimentoCompleto.ts
Normal file
68
src/api/pedimentoCompleto.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// src\api\pedimentoCompleto.ts
|
||||
import { fetchWithAuth } from '../fetchWithAuth';
|
||||
|
||||
export interface PedimentoCompleto {
|
||||
id: string;
|
||||
organizacion: string;
|
||||
pedimento: string;
|
||||
pedimento_numero: string;
|
||||
archivo: string;
|
||||
document_type: number;
|
||||
size: number;
|
||||
extension: string;
|
||||
fuente: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PedimentoCompletoResponse {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: PedimentoCompleto[];
|
||||
}
|
||||
|
||||
export interface DocumentFilters {
|
||||
document_type?: string;
|
||||
archivo__icontains?: string;
|
||||
extension?: string;
|
||||
created_at__date?: string;
|
||||
ordering?: string;
|
||||
}
|
||||
|
||||
const API_URL = (import.meta as any).env.VITE_EFC_API_URL;
|
||||
|
||||
export async function fetchPedimentoCompleto(
|
||||
pedimentoId: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 10,
|
||||
filters: DocumentFilters = {}
|
||||
): Promise<PedimentoCompletoResponse> {
|
||||
try {
|
||||
// Construir URL con filtros
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
pedimento: pedimentoId
|
||||
});
|
||||
|
||||
// Agregar filtros si existen
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// const res = await fetchWithAuth(`${API_URL}/record/documents/?${params.toString()}`);
|
||||
const res = await fetchWithAuth(`${API_URL}/record/pedimento-documents/?${params.toString()}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('No autorizado o error en la petición');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
console.error('Error in fetchPedimentoCompleto:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,29 @@
|
||||
import { fetchWithAuth } from '../fetchWithAuth';
|
||||
|
||||
// Tipos para la respuesta y registros
|
||||
export interface ProcesamientoPedimento {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
export interface Task {
|
||||
task_id: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
status: string;
|
||||
pedimento: string;
|
||||
organizacion: string;
|
||||
organizacion_name: string;
|
||||
estado: number;
|
||||
tipo_procesamiento: number;
|
||||
pedimento: string;
|
||||
servicio: number;
|
||||
}
|
||||
|
||||
export interface ProcesamientoPedimentosResponse {
|
||||
export interface TasksResponse {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: ProcesamientoPedimento[];
|
||||
results: Task[];
|
||||
}
|
||||
|
||||
// API para customs/procesamientopedimentos/
|
||||
export async function fetchProcesamientoPedimentos(
|
||||
// API para tasks/tasks/
|
||||
export async function fetchTasks(
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ProcesamientoPedimentosResponse> {
|
||||
): Promise<TasksResponse> {
|
||||
try {
|
||||
const API_URL = (import.meta as any).env.VITE_EFC_API_URL;
|
||||
|
||||
@@ -41,15 +39,15 @@ export async function fetchProcesamientoPedimentos(
|
||||
}
|
||||
});
|
||||
|
||||
const res = await fetchWithAuth(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`);
|
||||
const res = await fetchWithAuth(`${API_URL}/tasks/tasks/?${params.toString()}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Error al obtener procesamiento de pedimentos');
|
||||
throw new Error('Error al obtener tareas');
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
console.error('Error in fetchProcesamientoPedimentos:', error);
|
||||
console.error('Error in fetchTasks:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
36
src/api/taskStatus.js
Normal file
36
src/api/taskStatus.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// Helper functions for task status display
|
||||
|
||||
export const getTaskStatusLabel = (status) => {
|
||||
const statuses = {
|
||||
'submitted': 'Enviado',
|
||||
'pending': 'Pendiente',
|
||||
'processing': 'Procesando',
|
||||
'completed': 'Completado',
|
||||
'failed': 'Error',
|
||||
'cancelled': 'Cancelado'
|
||||
};
|
||||
return statuses[status] || `Estado ${status}`;
|
||||
};
|
||||
|
||||
export const getTaskStatusColor = (status) => {
|
||||
const colors = {
|
||||
'submitted': 'bg-blue-100 text-blue-800',
|
||||
'pending': 'bg-yellow-100 text-yellow-800',
|
||||
'processing': 'bg-indigo-100 text-indigo-800',
|
||||
'completed': 'bg-green-100 text-green-800',
|
||||
'failed': 'bg-red-100 text-red-800',
|
||||
'cancelled': 'bg-gray-100 text-gray-800'
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
// Ayuda a determinar si el estado permite ciertas acciones
|
||||
export const isTaskActionable = (status) => {
|
||||
const nonActionableStatuses = ['processing', 'completed', 'cancelled'];
|
||||
return !nonActionableStatuses.includes(status);
|
||||
};
|
||||
|
||||
export const isTaskFinal = (status) => {
|
||||
const finalStatuses = ['completed', 'failed', 'cancelled'];
|
||||
return finalStatuses.includes(status);
|
||||
};
|
||||
@@ -312,72 +312,6 @@ export default function Admin() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Análisis de actividad de usuario */}
|
||||
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && !isGroup35 && (
|
||||
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.65s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/3 to-purple-500/3"></div>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-3 mb-4 sm:mb-6">
|
||||
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full p-3 shadow-lg">
|
||||
<svg className="h-6 w-6 sm:h-7 sm:w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">Actividad de Usuarios</h3>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
|
||||
<span className="ml-3 text-gray-500">Cargando...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-red-600 bg-red-50 p-4 rounded-xl border border-red-200">{error}</div>
|
||||
) : userActivity ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 sm:gap-8">
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-4 sm:p-6 border border-blue-100">
|
||||
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
Resumen de acciones
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(userActivity.actions_count).map(([action, count]) => (
|
||||
<div key={action} className="flex justify-between items-center bg-white rounded-xl p-3 shadow-sm border border-blue-100">
|
||||
<span className="capitalize text-gray-700 font-medium">{action}</span>
|
||||
<span className="font-mono text-blue-700 bg-blue-100 px-2 py-1 rounded-lg text-sm font-bold">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between items-center bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl p-3 shadow-lg font-semibold">
|
||||
<span>Total actividades</span>
|
||||
<span className="font-mono bg-white/20 px-2 py-1 rounded-lg">{userActivity.actividades_filtradas}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-2xl p-4 sm:p-6 border border-green-100">
|
||||
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
Top usuarios
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{userActivity.top_users.map((user, idx) => (
|
||||
<div key={user.username} className="flex justify-between items-center bg-white rounded-xl p-3 shadow-sm border border-green-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="bg-green-100 text-green-800 rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold">{idx + 1}</span>
|
||||
<span className="text-gray-700 font-medium">{user.username}</span>
|
||||
</div>
|
||||
<span className="font-mono text-green-700 bg-green-100 px-2 py-1 rounded-lg text-sm font-bold">{user.activity_count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabla de últimos documentos */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getWithAuth, postWithAuth } from '../fetchWithAuth';
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
@@ -15,13 +17,92 @@ function Auditor() {
|
||||
const [auditandoRemesas, setAuditandoRemesas] = useState(false);
|
||||
const [procesandoPedimento, setProcesandoPedimento] = useState(null);
|
||||
const [procesandoRemesa, setProcesandoRemesa] = useState(null);
|
||||
const [procesandoAcuse, setProcesandoAcuse] = useState(null);
|
||||
const [procesandoCove, setProcesandoCove] = useState(null);
|
||||
const [auditandoAcusesCove, setAuditandoAcusesCove] = useState(false);
|
||||
const [pedimentoFilter, setPedimentoFilter] = useState('');
|
||||
const [auditandoEDocuments, setAuditandoEDocuments] = useState(false);
|
||||
const [auditandoAcuses, setAuditandoAcuses] = useState(false);
|
||||
|
||||
// Handler para auditar acuses (general)
|
||||
const handleAuditarAcuses = async () => {
|
||||
if (auditandoAcuses) return;
|
||||
try {
|
||||
setAuditandoAcuses(true);
|
||||
const organizacionId = pedimentos[0]?.organizacion;
|
||||
if (!organizacionId) {
|
||||
throw new Error('No hay organización disponible para auditar');
|
||||
}
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-acuse/`, {
|
||||
organizacion_id: organizacionId
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al iniciar la auditoría de acuses');
|
||||
}
|
||||
alert('La auditoría de acuses se ha iniciado correctamente');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setAuditandoAcuses(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handler para auditar EDocuments (general)
|
||||
const handleAuditarEDocuments = async () => {
|
||||
if (auditandoEDocuments) return;
|
||||
try {
|
||||
setAuditandoEDocuments(true);
|
||||
const organizacionId = pedimentos[0]?.organizacion;
|
||||
if (!organizacionId) {
|
||||
throw new Error('No hay organización disponible para auditar');
|
||||
}
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-edocuments/`, {
|
||||
organizacion_id: organizacionId
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al iniciar la auditoría de EDocuments');
|
||||
}
|
||||
alert('La auditoría de EDocuments se ha iniciado correctamente');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setAuditandoEDocuments(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handler para auditar acuses cove (general)
|
||||
const handleAuditarAcusesCove = async () => {
|
||||
if (auditandoAcusesCove) return;
|
||||
try {
|
||||
setAuditandoAcusesCove(true);
|
||||
// Obtener el organizacion_id del primer pedimento
|
||||
const organizacionId = pedimentos[0]?.organizacion;
|
||||
if (!organizacionId) {
|
||||
throw new Error('No hay organización disponible para auditar');
|
||||
}
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-acuse-cove/`, {
|
||||
organizacion_id: organizacionId
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al iniciar la auditoría de acuses cove');
|
||||
}
|
||||
alert('La auditoría de acuses cove se ha iniciado correctamente');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setAuditandoAcusesCove(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuditarRemesaPedimento = async (pedimentoId) => {
|
||||
if (procesandoRemesa) return;
|
||||
|
||||
|
||||
try {
|
||||
setProcesandoRemesa(pedimentoId);
|
||||
|
||||
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-procesamiento-remesa/pedimento/`, {
|
||||
pedimento_id: pedimentoId
|
||||
});
|
||||
@@ -43,12 +124,12 @@ function Auditor() {
|
||||
|
||||
const handleAuditarRemesas = async () => {
|
||||
if (auditandoRemesas) return;
|
||||
|
||||
|
||||
try {
|
||||
setAuditandoRemesas(true);
|
||||
// Obtener el organizacion_id del primer pedimento
|
||||
const organizacionId = pedimentos[0]?.organizacion;
|
||||
|
||||
|
||||
if (!organizacionId) {
|
||||
throw new Error('No hay organización disponible para auditar');
|
||||
}
|
||||
@@ -74,10 +155,10 @@ function Auditor() {
|
||||
|
||||
const handleAuditarPartidasPedimento = async (pedimentoId) => {
|
||||
if (procesandoPedimento) return;
|
||||
|
||||
|
||||
try {
|
||||
setProcesandoPedimento(pedimentoId);
|
||||
|
||||
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/crear-partidas/pedimento/`, {
|
||||
pedimento_id: pedimentoId
|
||||
});
|
||||
@@ -99,12 +180,12 @@ function Auditor() {
|
||||
|
||||
const handleAuditarPartidas = async () => {
|
||||
if (auditandoPartidas) return;
|
||||
|
||||
|
||||
try {
|
||||
setAuditandoPartidas(true);
|
||||
// Obtener el organizacion_id del primer pedimento
|
||||
const organizacionId = pedimentos[0]?.organizacion;
|
||||
|
||||
|
||||
if (!organizacionId) {
|
||||
throw new Error('No hay organización disponible para auditar');
|
||||
}
|
||||
@@ -130,12 +211,12 @@ function Auditor() {
|
||||
|
||||
const handleAuditarTodos = async () => {
|
||||
if (auditando) return;
|
||||
|
||||
|
||||
try {
|
||||
setAuditando(true);
|
||||
// Obtener el organizacion_id del primer pedimento
|
||||
const organizacionId = pedimentos[0]?.organizacion;
|
||||
|
||||
|
||||
if (!organizacionId) {
|
||||
throw new Error('No hay organización disponible para auditar');
|
||||
}
|
||||
@@ -159,11 +240,54 @@ function Auditor() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuditarAcusePedimento = async (pedimentoId) => {
|
||||
if (procesandoAcuse) return;
|
||||
try {
|
||||
setProcesandoAcuse(pedimentoId);
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-acuse/pedimento/`, {
|
||||
pedimento_id: pedimentoId
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al auditar acuse del pedimento');
|
||||
}
|
||||
alert('El acuse del pedimento se está auditando');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setProcesandoAcuse(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuditarCovePedimento = async (pedimentoId) => {
|
||||
if (procesandoCove) return;
|
||||
try {
|
||||
setProcesandoCove(pedimentoId);
|
||||
const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-cove/pedimento/`, {
|
||||
pedimento_id: pedimentoId
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al auditar COVE del pedimento');
|
||||
}
|
||||
alert('El COVE del pedimento se está auditando');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert(error.message);
|
||||
} finally {
|
||||
setProcesandoCove(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPedimentos = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getWithAuth(`${API_URL}/customs/pedimentos/?page=${page}&page_size=${itemsPerPage}`);
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: itemsPerPage.toString(),
|
||||
...(pedimentoFilter && { pedimento_app: pedimentoFilter })
|
||||
});
|
||||
const response = await getWithAuth(`${API_URL}/customs/pedimentos/?${queryParams}`);
|
||||
if (!response.ok) throw new Error('Error al cargar los pedimentos');
|
||||
const data = await response.json();
|
||||
setPedimentos(data.results);
|
||||
@@ -174,14 +298,20 @@ function Auditor() {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPedimentos();
|
||||
}, [page, itemsPerPage]);
|
||||
|
||||
// Aplicar debounce al fetchPedimentos
|
||||
const timeoutId = setTimeout(() => {
|
||||
fetchPedimentos();
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [page, itemsPerPage, pedimentoFilter]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header mejorado y responsivo */}
|
||||
<div className="mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 animate-fadein-slideup opacity-0"
|
||||
<div className="mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 animate-fadein-slideup opacity-0"
|
||||
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
||||
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
|
||||
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -223,9 +353,9 @@ function Auditor() {
|
||||
</div>
|
||||
|
||||
{/* Contenido principal */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 lg:p-8 animate-fadein-slideup opacity-0"
|
||||
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 lg:p-8 animate-fadein-slideup opacity-0"
|
||||
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="relative">
|
||||
@@ -247,6 +377,7 @@ function Auditor() {
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-2xl p-4 sm:p-6 border border-gray-200 shadow-sm">
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -254,8 +385,91 @@ function Auditor() {
|
||||
</svg>
|
||||
Servicios de Auditoría
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={handleAuditarAcusesCove}
|
||||
disabled={auditandoAcusesCove || pedimentos.length === 0}
|
||||
className={`inline-flex items-center px-4 py-2 rounded-lg shadow-sm text-white transition-all duration-200
|
||||
${auditandoAcusesCove
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-pink-600 hover:bg-pink-700 hover:shadow-md transform hover:scale-105'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{auditandoAcusesCove ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Auditando Acuses Cove...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
|
||||
Auditar Acuses Cove
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAuditarEDocuments}
|
||||
disabled={auditandoEDocuments || pedimentos.length === 0}
|
||||
className={`inline-flex items-center px-4 py-2 rounded-lg shadow-sm text-white transition-all duration-200
|
||||
${auditandoEDocuments
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-yellow-600 hover:bg-yellow-700 hover:shadow-md transform hover:scale-105'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{auditandoEDocuments ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Auditando EDocuments...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
|
||||
Auditar EDocuments
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAuditarAcuses}
|
||||
disabled={auditandoAcuses || pedimentos.length === 0}
|
||||
className={`inline-flex items-center px-4 py-2 rounded-lg shadow-sm text-white transition-all duration-200
|
||||
${auditandoAcuses
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 hover:shadow-md transform hover:scale-105'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{auditandoAcuses ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Auditando Acuses...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
|
||||
Auditar Acuses
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAuditarTodos}
|
||||
disabled={auditando || auditandoPartidas || pedimentos.length === 0}
|
||||
@@ -342,6 +556,39 @@ function Auditor() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sección de Filtros */}
|
||||
<div className="mt-6 bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Contenedor para los filtros - esperando instrucciones */}
|
||||
<div className="col-span-full">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Filtros de Búsqueda
|
||||
</h3>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<label htmlFor="pedimento" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Número de Pedimento
|
||||
</label>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="pedimento"
|
||||
id="pedimento"
|
||||
value={pedimentoFilter}
|
||||
onChange={(e) => setPedimentoFilter(e.target.value)}
|
||||
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-3 pr-10 py-2 sm:text-sm border-gray-300 rounded-md"
|
||||
placeholder="Buscar por pedimento..."
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabla de pedimentos */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
@@ -384,23 +631,23 @@ function Auditor() {
|
||||
{pedimentos.map((pedimento) => (
|
||||
<tr key={pedimento.id} className="hover:bg-gray-50">
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900">
|
||||
{pedimento.pedimento_app}
|
||||
<Link to={`/expedientes/pedimento/${pedimento.id}`} className='hover:text-blue-500 hover:text-bold hover:text-underline'>{pedimento.pedimento_app}</Link>
|
||||
</td>
|
||||
{/* PC - Pedimento Completo */}
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110">
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
{/* RM - Remesas */}
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleAuditarRemesaPedimento(pedimento.id)}
|
||||
disabled={procesandoRemesa === pedimento.id}
|
||||
className={`inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200
|
||||
${procesandoRemesa === pedimento.id
|
||||
${procesandoRemesa === pedimento.id
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110'
|
||||
}
|
||||
@@ -413,18 +660,18 @@ function Auditor() {
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
{/* PT - Partidas */}
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleAuditarPartidasPedimento(pedimento.id)}
|
||||
disabled={procesandoPedimento === pedimento.id}
|
||||
className={`inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200
|
||||
${procesandoPedimento === pedimento.id
|
||||
${procesandoPedimento === pedimento.id
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110'
|
||||
}
|
||||
@@ -437,32 +684,60 @@ function Auditor() {
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
{/* AC - Acuse */}
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110">
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => handleAuditarAcusePedimento(pedimento.id)}
|
||||
disabled={procesandoAcuse === pedimento.id}
|
||||
className={`inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 ${procesandoAcuse === pedimento.id
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110'
|
||||
}`}
|
||||
>
|
||||
{procesandoAcuse === pedimento.id ? (
|
||||
<svg className="h-4 w-4 text-gray-600 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
{/* COVE */}
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110">
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => handleAuditarCovePedimento(pedimento.id)}
|
||||
disabled={procesandoCove === pedimento.id}
|
||||
className={`inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 ${procesandoCove === pedimento.id
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110'
|
||||
}`}
|
||||
>
|
||||
{procesandoCove === pedimento.id ? (
|
||||
<svg className="h-4 w-4 text-gray-600 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
{/* AC_COVE */}
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110">
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
@@ -470,7 +745,7 @@ function Auditor() {
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-center">
|
||||
<button className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white border border-gray-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 hover:scale-110">
|
||||
<svg className="h-4 w-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@@ -13,68 +13,11 @@ import { fetchPedimentoDocuments } from '../api/documentos.ts';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
// import { usePolling } from '../hooks/usePolling';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { downloadFile, downloadBulkZip } from '../utils/downloadUtils';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
// Descarga individual
|
||||
const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => {
|
||||
try {
|
||||
const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`);
|
||||
|
||||
if (!res.ok) {
|
||||
alert('No autorizado o error en la descarga');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
if (setSuccess) setSuccess('Descarga exitosa');
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
showMessage('Error al descargar el archivo', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Descarga masiva (bulk)
|
||||
const downloadBulkZip = async (ids, showMessage, setSuccess, nombreZip = 'documentos') => {
|
||||
if (!ids.length) {
|
||||
showMessage('Selecciona al menos un documento.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await postWithAuth(`${API_URL}/record/documents/bulk-download/`, {
|
||||
document_ids: ids,
|
||||
pedimento_nombre: nombreZip
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
showMessage('No autorizado o error en la descarga masiva', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${nombreZip || 'documentos'}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
if (setSuccess) setSuccess('Descarga(s) completada(s)');
|
||||
} catch (error) {
|
||||
console.error('Error in bulk download:', error);
|
||||
showMessage('Error en la descarga masiva', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
export default function Documents() {
|
||||
const focusKeeperRef = useRef(null);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import fetchWithAuth from '../fetchWithAuth';
|
||||
|
||||
const initialFilters = {
|
||||
pedimento_app: '',
|
||||
aduana: '',
|
||||
@@ -12,11 +11,48 @@ const initialFilters = {
|
||||
fecha_pago_lte: '',
|
||||
contribuyente__rfc: '',
|
||||
};
|
||||
|
||||
export default function TableroAlmacenamiento() {
|
||||
const [filters, setFilters] = useState(initialFilters);
|
||||
const [summary, setSummary] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [reports, setReports] = useState([]);
|
||||
|
||||
const handleGenerateReport = async () => {
|
||||
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/table-summary/${params ? `?${params}` : ''}`;
|
||||
const res = await fetchWithAuth(url, { method: 'POST' });
|
||||
if (!res.ok) throw new Error('Error al generar el reporte');
|
||||
alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.');
|
||||
} catch (err) {
|
||||
alert('No se pudo generar el reporte.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadReport = async (reportId) => {
|
||||
try {
|
||||
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-download/${reportId}/`;
|
||||
const res = await fetchWithAuth(url);
|
||||
if (!res.ok) throw new Error('Error al descargar el reporte');
|
||||
const blob = await res.blob();
|
||||
let filename = `reporte_${reportId}.csv`;
|
||||
const disposition = res.headers.get('Content-Disposition');
|
||||
if (disposition && disposition.includes('filename=')) {
|
||||
filename = disposition.split('filename=')[1].replace(/"/g, '').trim();
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (err) {
|
||||
alert('No se pudo descargar el reporte.');
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch summary data
|
||||
const fetchSummary = async () => {
|
||||
@@ -36,27 +72,31 @@ export default function TableroAlmacenamiento() {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Fetch initial data
|
||||
// Fetch report list from API
|
||||
useEffect(() => {
|
||||
const fetchReports = async () => {
|
||||
try {
|
||||
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-list/`;
|
||||
const res = await fetchWithAuth(url);
|
||||
if (!res.ok) throw new Error('Error al obtener el historial de reportes');
|
||||
const data = await res.json();
|
||||
setReports(data);
|
||||
} catch (err) {
|
||||
setReports([]);
|
||||
}
|
||||
};
|
||||
fetchReports();
|
||||
}, []);
|
||||
|
||||
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="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
|
||||
{/* Header */}
|
||||
@@ -97,12 +137,21 @@ export default function TableroAlmacenamiento() {
|
||||
))}
|
||||
</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 className="flex gap-2">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"
|
||||
onClick={() => alert('Generar reporte (implementación pendiente)')}
|
||||
>
|
||||
Generar Reporte
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -115,203 +164,60 @@ export default function TableroAlmacenamiento() {
|
||||
<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 className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
{/* ...Tarjetas existentes... */}
|
||||
{/* ...aquí van las Card como antes... */}
|
||||
{/* ...no se repite para brevedad... */}
|
||||
</div>
|
||||
{/* Tabla de reportes debajo de las tarjetas */}
|
||||
<div className="mt-10">
|
||||
<h2 className="text-lg font-bold text-slate-700 mb-4">Historial de Reportes</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white rounded-lg shadow border border-slate-200">
|
||||
<thead>
|
||||
<tr className="bg-slate-100">
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">ID</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Estado</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Creado</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Finalizado</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Error</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Descargar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.length > 0 ? (
|
||||
reports.map((r) => (
|
||||
<tr key={r.report_id}>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.report_id}</td>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.status}</td>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.created_at}</td>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.finished_at}</td>
|
||||
<td className="px-4 py-2 text-xs text-red-500">{r.error_message ? r.error_message : '-'}</td>
|
||||
<td className="px-4 py-2 text-xs">
|
||||
{r.status === 'ready' ? (
|
||||
<button
|
||||
className="text-blue-600 hover:underline"
|
||||
onClick={() => handleDownloadReport(r.report_id)}
|
||||
>
|
||||
Descargar
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-slate-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-2 text-center text-slate-400">No hay reportes disponibles.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
||||
{/* 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">asd</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>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-slate-500 py-12">No hay datos para mostrar.</div>
|
||||
)}
|
||||
|
||||
69
src/utils/downloadUtils.js
Normal file
69
src/utils/downloadUtils.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { fetchWithAuth } from '../fetchWithAuth';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
/**
|
||||
* Función auxiliar para descargas individuales
|
||||
* @param {string} id - ID del documento
|
||||
* @param {string} filename - Nombre del archivo (opcional)
|
||||
* @param {function} showMessage - Función para mostrar mensajes
|
||||
*/
|
||||
export const downloadFile = async (id, filename = 'archivo', showMessage) => {
|
||||
try {
|
||||
const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`);
|
||||
|
||||
if (!res.ok) {
|
||||
showMessage('Error en la descarga del archivo', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
if (error.message === 'SESSION_EXPIRED') {
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
} else {
|
||||
showMessage('Error al descargar el archivo', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Función auxiliar para descargas masivas en ZIP
|
||||
* @param {array} ids - Array de IDs de documentos
|
||||
* @param {function} showMessage - Función para mostrar mensajes
|
||||
* @param {string} pedimentoName - Nombre del pedimento para el archivo ZIP (opcional)
|
||||
*/
|
||||
export const downloadBulkZip = async (ids, showMessage, pedimentoName) => {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-download/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ document_ids: ids })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Error en la descarga');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `documentos_${pedimentoName || 'pedimento'}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
showMessage('Descarga iniciada exitosamente', 'success');
|
||||
} catch (error) {
|
||||
showMessage('Error en la descarga: ' + error.message, 'error');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user