30 Commits

Author SHA1 Message Date
Dulce
d46ea97340 modificacion reportes 2025-12-16 08:13:00 -07:00
5580c3cc91 Merge pull request 'fix: Se agrega deteccion de pestaña seleccionada para mostrarla en el modal de documentos.' (#12) from T2025-10-152 into main
Reviewed-on: #12
2025-12-12 22:00:39 +00:00
bb59d1e487 Merge pull request 'fix/landingpage-ui-adjustment' (#11) from fix/landingpage-ui-adjustment into main
Reviewed-on: #11
2025-12-12 22:00:01 +00:00
769a1fd4e8 fix: Se agrega deteccion de pestaña seleccionada para mostrarla en el modal de documentos. 2025-12-10 11:18:19 -07:00
06ec641691 feat/Landingpage 2025-12-09 16:22:46 -07:00
a91ef2f11a style: update landing page 2025-12-08 10:17:23 -07:00
70f0b38e93 fix/Se modifica vista principal de detalle de pedimento y se ajusta la visualizacion de los documentos respectivos a cada pestaña. 2025-12-05 08:22:45 -07:00
d8c23dcf09 Remove .env from Git tracking 2025-11-26 08:59:23 -07:00
97a39a6d37 Merge pull request 'Subo archivo JSX sin afectar rama principal' (#10) from fixed/subir-expediente-jsx into main
Reviewed-on: #10
2025-11-26 15:52:56 +00:00
756815983d Subo archivo JSX sin afectar rama principal 2025-11-26 08:48:50 -07:00
33ca76c054 Merge pull request 'retirar debug mode' (#9) from reportes into main
Reviewed-on: #9
2025-11-26 14:48:22 +00:00
Dulce
6acb55c563 retirar debug mode 2025-11-25 16:25:57 -07:00
bdabc94974 Merge pull request 'reportes update' (#8) from reportes into main
Reviewed-on: #8
2025-11-25 22:24:39 +00:00
Dulce
5387eb25cf reportes update 2025-11-25 13:25:29 -07:00
06c5d32ae0 Merge pull request 'prod' (#7) from repoCumplimiento into main
Reviewed-on: #7
2025-10-22 03:41:10 +00:00
fd4fe5dc2b prod 2025-10-21 21:40:33 -06:00
0c4a48a60b Merge pull request 'repoCumplimiento' (#6) from repoCumplimiento into main
Reviewed-on: #6
2025-10-22 03:35:28 +00:00
f845629b81 Cumplimiento a prod 2025-10-21 21:35:03 -06:00
5e50d6bac0 Reporte de cumplimiento 2025-10-21 21:32:37 -06:00
539954eb41 Merge pull request 'Cambios en admin' (#5) from admin into main
Reviewed-on: #5
2025-10-16 01:42:15 +00:00
e69fca99c0 Cambios en admin 2025-10-15 19:41:21 -06:00
63ab45856f Merge pull request 'feat: Implementar subida avanzada de expedientes con compresión automática' (#4) from feature/document-upload-updated into main
Reviewed-on: #4
2025-10-15 14:21:16 +00:00
e371af3706 Merge pull request 'Modificaciones a pedimento Detail' (#3) from PedimentoDetail into main
Reviewed-on: #3
2025-10-15 00:07:47 +00:00
3f640307f8 Modificaciones a pedimento Detail 2025-10-14 18:05:02 -06:00
4660ed59a7 feat: Implementar subida avanzada de expedientes con compresión automática
-  Agregar selección múltiple de carpetas con validación de nomenclatura
-  Implementar compresión automática de carpetas usando JSZip
-  Permitir selección múltiple de archivos ZIP y RAR
- 🎨 Mejorar UI del modal con scroll y gestión de archivos individual
- 🔧 Agregar barras de progreso para compresión y subida
- 🔧 Corregir endpoint a bulk-create y formato FormData
- 🔧 Resolver warnings de React con webkitdirectory
- 📦 Instalar JSZip como dependencia

Funcionalidades nuevas:
- Modal responsivo con altura máxima y scroll interno
- Compresión automática de carpetas antes de envío
- Interfaz consistente para carpetas, ZIP y RAR
- Eliminar archivos/carpetas individualmente
- Contador visual de archivos seleccionados
- Validación flexible de nomenclatura de expedientes
2025-10-14 14:07:17 -05:00
791bd2f87e Merge pull request 'refactor: centralize download functions into utils' (#2) from feature/download-utils-clean into main
Reviewed-on: #2
2025-10-13 19:38:33 +00:00
5f4a797c3c refactor: centralize download functions into utils
- Create src/utils/downloadUtils.js with downloadFile and downloadBulkZip
- Remove duplicated download functions from PedimentoDetail.jsx
- Remove duplicated download functions from Documents.jsx
- Add proper imports to use centralized functions
- Improve code reusability and maintainability
- Ensure consistent download behavior across components
2025-10-13 14:25:01 -05:00
5c8380a772 se agrego referencia a pedimento 2025-10-12 08:04:10 -06:00
03a9f793b1 se modifico auditor y pagina de procesos 2025-10-12 07:52:06 -06:00
c924aab9c1 Merge pull request 'feat: Add checkbox selection and bulk operations' (#1) from feature/checkbox-bulk-operations into main
Reviewed-on: #1
2025-10-10 01:38:42 +00:00
19 changed files with 5327 additions and 2923 deletions

5
.env
View File

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

@@ -23,3 +23,4 @@ dist-ssr
*.sln
*.sw?
.env
*.bak

95
package-lock.json generated
View File

@@ -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": {

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

View 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;
}
}

View File

@@ -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;
organizacion: string;
organizacion_name: string;
estado: number;
tipo_procesamiento: number;
export interface Task {
task_id: string;
timestamp: string;
message: string;
status: string;
pedimento: string;
organizacion: 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
View 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);
};

View File

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

View File

@@ -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,6 +17,85 @@ 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;
@@ -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,8 +298,14 @@ 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">
@@ -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,13 +631,13 @@ 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>
@@ -413,7 +660,7 @@ 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>
@@ -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>

View File

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

View File

@@ -13,6 +13,7 @@ export default function LandingAnimated() {
message: ''
});
const currentYear = new Date().getFullYear();
const observerRef = useRef(null);
const sectionsRef = useRef({});
@@ -101,7 +102,7 @@ export default function LandingAnimated() {
// Estadísticas animadas
const stats = [
{ number: '500+', label: 'Agentes Aduanales', icon: '🏢' },
{ number: '350+', label: 'Clientes', icon: '🏢' },
{ number: '15,000+', label: 'Pedimentos Procesados', icon: '📋' },
{ number: '99.9%', label: 'Uptime Garantizado', icon: '⚡' },
{ number: '24/7', label: 'Soporte Especializado', icon: '🛡️' }
@@ -115,24 +116,31 @@ export default function LandingAnimated() {
? 'bg-white/95 backdrop-blur-md shadow-lg border-b border-gray-200'
: 'bg-transparent'
}`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="flex items-center justify-between py-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<h1 className="text-2xl font-bold">
<span
className="bg-clip-text text-transparent"
className="text-transparent bg-clip-text"
style={{
background: `linear-gradient(to right, #1B2A41, #4DA6FF)`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent'
color: isScrolled
? 'transparent'
: '#FFFFFF',
background: isScrolled
? 'linear-gradient(to right, #1B2A41, #4DA6FF)'
: 'none',
WebkitBackgroundClip: isScrolled ? 'text' : 'unset',
WebkitTextFillColor: isScrolled ? 'transparent' : '#FFFFFF',
backgroundClip: isScrolled ? 'text' : 'unset'
}}
>
EFC
</span>
</h1>
</div>
<nav className="hidden md:flex ml-10 space-x-8">
<nav className="hidden ml-10 space-x-8 md:flex">
{[
{ id: 'inicio', label: 'Inicio' },
{ id: 'estadisticas', label: 'Confianza' },
@@ -141,36 +149,52 @@ export default function LandingAnimated() {
{ id: 'precios', label: 'Precios' },
{ id: 'contacto', label: 'Contacto' }
].map((item) => (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={`relative text-sm font-medium transition-all duration-300 hover:scale-105 group`}
style={{
color: activeSection === item.id
? '#1B2A41'
: isScrolled
? '#333333'
: 'white'
}}
onMouseEnter={(e) => {
if (activeSection !== item.id) {
e.target.style.color = isScrolled ? '#1B2A41' : '#4DA6FF';
}
}}
onMouseLeave={(e) => {
if (activeSection !== item.id) {
e.target.style.color = isScrolled ? '#333333' : 'white';
}
}}
>
{item.label}
<span
className={`absolute -bottom-1 left-0 h-0.5 transition-all duration-300 ${
activeSection === item.id ? 'w-full' : 'w-0 group-hover:w-full'
}`}
style={{ backgroundColor: '#1B2A41' }}
></span>
</button>
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={`relative text-sm font-medium transition-all duration-300 hover:scale-105 group`}
style={{
color: item.id === 'inicio'
? (isScrolled
? (activeSection === 'inicio' ? '#1B2A41' : '#333333')
: (activeSection === 'inicio' ? 'white' : 'white'))
: (activeSection === item.id
? '#1B2A41'
: isScrolled
? '#333333'
: 'white')
}}
onMouseEnter={(e) => {
if (activeSection !== item.id) {
if (item.id === 'inicio') {
e.target.style.color = isScrolled ? '#1B2A41' : '#4DA6FF';
} else {
e.target.style.color = isScrolled ? '#1B2A41' : '#4DA6FF';
}
}
}}
onMouseLeave={(e) => {
if (activeSection !== item.id) {
if (item.id === 'inicio') {
e.target.style.color = isScrolled ? '#333333' : 'white';
} else {
e.target.style.color = isScrolled ? '#333333' : 'white';
}
}
}}
>
{item.label}
<span
className={`absolute -bottom-1 left-0 h-0.5 transition-all duration-300 ${
activeSection === item.id ? 'w-full' : 'w-0 group-hover:w-full'
}`}
style={{
backgroundColor: item.id === 'inicio' && !isScrolled && activeSection === 'inicio'
? 'white'
: '#1B2A41'
}}
></span>
</button>
))}
</nav>
</div>
@@ -196,21 +220,33 @@ export default function LandingAnimated() {
</header>
{/* Hero Section con efectos de gradiente animado */}
<section id="inicio" className="relative min-h-screen flex items-center overflow-hidden">
{/* Background con gradientes animados */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(135deg, #1B2A41 0%, #263549 50%, #1976D2 100%)'
}}
>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
<section id="inicio" className="relative flex items-center min-h-screen overflow-hidden">
{/* Background con imagen */}
<div className="absolute inset-0">
{/* Imagen de fondo */}
<div
className="absolute inset-0 bg-center bg-no-repeat bg-cover"
style={{
backgroundImage: 'url("images/empresaria001.webp")', // Cambia esta ruta
backgroundColor: '#1B2A41', // Color de respaldo
}}
>
{/* Overlay para oscurecer la imagen y mejorar legibilidad */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(to bottom, rgba(27, 42, 65, 0.85), rgba(27, 42, 65, 0.7))',
}}
></div>
</div>
{/* Efectos adicionales */}
<div className="absolute inset-0 opacity-20">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent animate-pulse"></div>
</div>
</div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 text-center">
<div className="relative px-4 py-32 mx-auto text-center max-w-7xl sm:px-6 lg:px-8">
<div className="space-y-8">
<h1
data-animate="hero-title"
@@ -222,7 +258,7 @@ export default function LandingAnimated() {
>
<span className="block">
<span
className="bg-clip-text text-transparent"
className="text-transparent bg-clip-text"
style={{
background: 'linear-gradient(to right, white, #64B5F6)',
WebkitBackgroundClip: 'text',
@@ -233,7 +269,7 @@ export default function LandingAnimated() {
</span>
</span>
<span
className="block text-3xl sm:text-4xl md:text-5xl mt-4 bg-clip-text text-transparent"
className="block mt-4 text-3xl text-transparent sm:text-4xl md:text-5xl bg-clip-text"
style={{
background: 'linear-gradient(to right, #64B5F6, white)',
WebkitBackgroundClip: 'text',
@@ -243,7 +279,7 @@ export default function LandingAnimated() {
Para Agentes Aduanales
</span>
<span
className="block text-3xl sm:text-4xl md:text-5xl bg-clip-text text-transparent"
className="block text-3xl text-transparent sm:text-4xl md:text-5xl bg-clip-text"
style={{
background: 'linear-gradient(to right, white, #64B5F6)',
WebkitBackgroundClip: 'text',
@@ -264,8 +300,8 @@ export default function LandingAnimated() {
style={{ color: '#64B5F6' }}
>
La plataforma líder desarrollada por
<span className="font-bold text-white"> @AduanaSoft</span> para
<span className="font-semibold" style={{ color: '#FF9800' }}> digitalizar y optimizar</span>
<span className="font-bold text-white"> Aduanasoft®</span> para
<span className="font-semibold" style={{ color: '#FFFFFF' }}> digitalizar y optimizar</span>
{' '}todos tus procesos de comercio exterior con tecnología de vanguardia
</p>
@@ -279,7 +315,7 @@ export default function LandingAnimated() {
>
<Link
to="/login"
className="group inline-flex items-center px-8 py-4 text-lg font-semibold rounded-full transition-all duration-300 shadow-2xl hover:shadow-3xl transform hover:-translate-y-1 hover:scale-105"
className="inline-flex items-center px-8 py-4 text-lg font-semibold transition-all duration-300 transform rounded-full shadow-2xl group hover:shadow-3xl hover:-translate-y-1 hover:scale-105"
style={{
color: '#1B2A41',
background: 'linear-gradient(to right, white, #F2F4F7)'
@@ -292,15 +328,15 @@ export default function LandingAnimated() {
}}
>
<span>Comenzar Ahora</span>
<svg className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-5 h-5 ml-2 transition-transform duration-200 group-hover:translate-x-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</Link>
<button
onClick={() => scrollToSection('caracteristicas')}
className="group inline-flex items-center px-8 py-4 text-lg font-semibold rounded-full text-white bg-transparent border-2 border-white/30 hover:border-white hover:bg-white/10 transition-all duration-300 backdrop-blur-sm"
className="inline-flex items-center px-8 py-4 text-lg font-semibold text-white transition-all duration-300 bg-transparent border-2 rounded-full group border-white/30 hover:border-white hover:bg-white/10 backdrop-blur-sm"
>
<svg className="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M19 10a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Ver Demo</span>
@@ -308,11 +344,11 @@ export default function LandingAnimated() {
</div>
{/* Floating cards con efectos */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
<div className="grid max-w-4xl grid-cols-1 gap-6 mx-auto md:grid-cols-3">
{[
{ icon: '🚀', title: 'Rápido', desc: 'Procesamiento instantáneo' },
{ icon: '🔒', title: 'Seguro', desc: 'Cifrado de nivel bancario' },
{ icon: '📊', title: 'Inteligente', desc: 'IA para optimización' }
{ icon: '📊', title: 'Inteligente', desc: 'IA para optimización' },
].map((feature, index) => (
<div
key={index}
@@ -324,8 +360,8 @@ export default function LandingAnimated() {
}`}
style={{ transitionDelay: `${700 + index * 200}ms` }}
>
<div className="text-4xl mb-3">{feature.icon}</div>
<h3 className="text-white font-semibold text-lg mb-2">{feature.title}</h3>
<div className="mb-3 text-4xl">{feature.icon}</div>
<h3 className="mb-2 text-lg font-semibold text-white">{feature.title}</h3>
<p className="text-sm" style={{ color: '#64B5F6' }}>{feature.desc}</p>
</div>
))}
@@ -334,10 +370,10 @@ export default function LandingAnimated() {
</div>
{/* Scroll indicator animado */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
<div className="absolute transform -translate-x-1/2 bottom-8 left-1/2 animate-bounce">
<button
onClick={() => scrollToSection('estadisticas')}
className="text-white/70 hover:text-white transition-colors duration-200"
className="transition-colors duration-200 text-white/70 hover:text-white"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
@@ -348,7 +384,7 @@ export default function LandingAnimated() {
{/* Sección de Estadísticas y Confianza */}
<section id="estadisticas" className="py-20" style={{ background: 'linear-gradient(to right, #F2F4F7, white)' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div
data-animate="stats-header"
className={`text-center mb-16 transition-all duration-1000 ${
@@ -357,17 +393,17 @@ export default function LandingAnimated() {
: 'opacity-0 translate-y-10'
}`}
>
<h2 className="text-4xl font-extrabold mb-4" style={{ color: '#333333' }}>
Más de <span style={{ color: '#1B2A41' }}>500 empresas</span> confían en nosotros
<h2 className="mb-4 text-4xl font-extrabold" style={{ color: '#333333' }}>
Más de <span style={{ color: '#1B2A41' }}>350 empresas</span> confían en nosotros
</h2>
<p className="text-xl max-w-3xl mx-auto" style={{ color: '#7A7A7A' }}>
Desarrollado por <span className="font-bold" style={{ color: '#1B2A41' }}>@AduanaSoft</span>,
líderes en tecnología aduanal con más de 10 años de experiencia
<p className="max-w-3xl mx-auto text-xl" style={{ color: '#7A7A7A' }}>
Desarrollado por <span className="font-bold" style={{ color: '#1B2A41' }}>Aduanasoft®</span>,
líderes en tecnología aduanal con más de 29 años de experiencia
</p>
</div>
{/* Stats con animaciones */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
<div className="grid grid-cols-2 gap-8 mb-16 lg:grid-cols-4">
{stats.map((stat, index) => (
<div
key={index}
@@ -379,8 +415,8 @@ export default function LandingAnimated() {
}`}
style={{ transitionDelay: `${index * 200}ms` }}
>
<div className="text-4xl mb-4 animate-pulse">{stat.icon}</div>
<div className="text-3xl font-bold mb-2" style={{ color: '#1B2A41' }}>{stat.number}</div>
<div className="mb-4 text-4xl animate-pulse">{stat.icon}</div>
<div className="mb-2 text-3xl font-bold" style={{ color: '#1B2A41' }}>{stat.number}</div>
<div className="font-medium" style={{ color: '#7A7A7A' }}>{stat.label}</div>
</div>
))}
@@ -396,18 +432,18 @@ export default function LandingAnimated() {
}`}
style={{ background: 'linear-gradient(to right, #1B2A41, #263549)' }}
>
<div className="grid md:grid-cols-2 gap-8 items-center">
<div className="grid items-center gap-8 md:grid-cols-2">
<div>
<h3 className="text-3xl font-bold mb-6">Acerca de AduanaSoft</h3>
<h3 className="mb-6 text-3xl font-bold">Acerca de Aduanasoft</h3>
<div className="space-y-4 text-indigo-100">
{[
"10+ años especializados en software aduanal",
"29+ años especializados en software aduanal",
"Equipo experto en comercio exterior y tecnología",
"Certificación SAT y cumplimiento normativo total",
"Soporte 24/7 con especialistas aduanales"
].map((item, idx) => (
<div key={idx} className="flex items-start space-x-3">
<div className="flex-shrink-0 w-6 h-6 bg-white/20 rounded-full flex items-center justify-center mt-1">
<div className="flex items-center justify-center flex-shrink-0 w-6 h-6 mt-1 rounded-full bg-white/20">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
@@ -418,9 +454,9 @@ export default function LandingAnimated() {
</div>
</div>
<div className="text-center">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20">
<div className="text-6xl mb-4">🏆</div>
<h4 className="text-2xl font-bold mb-2">Líder del Mercado</h4>
<div className="p-8 border bg-white/10 backdrop-blur-md rounded-2xl border-white/20">
<div className="mb-4 text-6xl">🏆</div>
<h4 className="mb-2 text-2xl font-bold">Líder del Mercado</h4>
<p className="text-indigo-100">
Reconocidos como la mejor solución tecnológica para agentes aduanales en México
</p>
@@ -433,7 +469,7 @@ export default function LandingAnimated() {
{/* Características con efectos interactivos */}
<section id="caracteristicas" className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div
data-animate="features-header"
className={`text-center mb-16 transition-all duration-1000 ${
@@ -442,7 +478,7 @@ export default function LandingAnimated() {
: 'opacity-0 translate-y-10'
}`}
>
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
<h2 className="mb-4 text-4xl font-extrabold text-gray-900">
Soluciones Especializadas para Comercio Exterior
</h2>
<p className="text-xl text-gray-600">
@@ -450,7 +486,7 @@ export default function LandingAnimated() {
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{[
{
icon: '📋',
@@ -492,7 +528,7 @@ export default function LandingAnimated() {
}`}>
{feature.icon}
</div>
<h3 className="text-2xl font-bold mb-4 transition-colors duration-300" style={{
<h3 className="mb-4 text-2xl font-bold transition-colors duration-300" style={{
color: visibleElements.has(`feature-${index}`) ? '#1B2A41' : '#333333'
}}>
{feature.title}
@@ -503,7 +539,7 @@ export default function LandingAnimated() {
<ul className="space-y-2">
{feature.features.map((item, idx) => (
<li key={idx} className="flex items-center text-sm" style={{ color: '#7A7A7A' }}>
<svg className="w-4 h-4 mr-2 flex-shrink-0" style={{ color: '#2E7D32' }} fill="currentColor" viewBox="0 0 20 20">
<svg className="flex-shrink-0 w-4 h-4 mr-2" style={{ color: '#2E7D32' }} fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
{item}
@@ -514,7 +550,7 @@ export default function LandingAnimated() {
<div className="p-4 transition-colors duration-300" style={{
background: 'linear-gradient(to right, #F2F4F7, #FFFFFF)'
}}>
<button className="font-semibold text-sm transition-colors duration-200" style={{
<button className="text-sm font-semibold transition-colors duration-200" style={{
color: '#1B2A41'
}}>
Conocer más
@@ -528,7 +564,7 @@ export default function LandingAnimated() {
{/* Precios */}
<section id="precios" className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div
data-animate="pricing-header"
className={`text-center mb-16 transition-all duration-1000 ${
@@ -537,7 +573,7 @@ export default function LandingAnimated() {
: 'opacity-0 translate-y-10'
}`}
>
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
<h2 className="mb-4 text-4xl font-extrabold text-gray-900">
Planes diseñados para tu crecimiento
</h2>
<p className="text-xl text-gray-600">
@@ -545,7 +581,7 @@ export default function LandingAnimated() {
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{[
{
name: 'Starter',
@@ -614,8 +650,8 @@ export default function LandingAnimated() {
}}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<span className="text-white px-4 py-2 rounded-full text-sm font-semibold animate-pulse" style={{
<div className="absolute transform -translate-x-1/2 -top-4 left-1/2">
<span className="px-4 py-2 text-sm font-semibold text-white rounded-full animate-pulse" style={{
background: 'linear-gradient(to right, #1B2A41, #263549)'
}}>
Más Popular
@@ -623,8 +659,8 @@ export default function LandingAnimated() {
</div>
)}
<div className="text-center mb-8">
<h3 className="text-2xl font-bold mb-2" style={{ color: '#333333' }}>{plan.name}</h3>
<div className="mb-8 text-center">
<h3 className="mb-2 text-2xl font-bold" style={{ color: '#333333' }}>{plan.name}</h3>
<p className="mb-4" style={{ color: '#7A7A7A' }}>{plan.description}</p>
{/*
<div className="mb-4">
@@ -634,10 +670,10 @@ export default function LandingAnimated() {
*/}
</div>
<ul className="space-y-3 mb-8">
<ul className="mb-8 space-y-3">
{plan.features.map((feature, idx) => (
<li key={idx} className="flex items-center">
<svg className="w-5 h-5 text-green-500 mr-3" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-5 h-5 mr-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span className="text-gray-700">{feature}</span>
@@ -662,32 +698,50 @@ export default function LandingAnimated() {
</section>
{/* Contacto */}
<section id="contacto" className="relative py-20 overflow-hidden" style={{
background: 'linear-gradient(135deg, #1B2A41 0%, #263549 50%, #1B2A41 100%)'
}}>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%234DA6FF' fillOpacity='0.3'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
}}></div>
</div>
<section id="contacto" className="relative py-20 overflow-hidden">
{/* Background con imagen */}
<div className="absolute inset-0">
{/* Misma imagen de fondo que la sección inicio */}
<div
className="absolute inset-0 bg-center bg-no-repeat bg-cover"
style={{
backgroundImage: 'url("images/sistemagestion002.webp")',
backgroundColor: '#1B2A41',
}}
>
{/* Overlay más oscuro para mejor legibilidad en formulario */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(to bottom, rgba(27, 42, 65, 0.85), rgba(27, 42, 65, 0.7))',
}}
></div>
</div>
{/* Floating elements */}
<div className="absolute top-10 left-10 w-20 h-20 rounded-full opacity-20" style={{ backgroundColor: '#4DA6FF' }}></div>
<div className="absolute bottom-10 right-10 w-32 h-32 rounded-full opacity-15" style={{ backgroundColor: '#F57C00' }}></div>
<div className="absolute top-1/2 left-1/4 w-16 h-16 rounded-full opacity-10" style={{ backgroundColor: '#2E7D32' }}></div>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%234DA6FF' fillOpacity='0.3'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
}}></div>
</div>
</div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-4xl md:text-5xl font-extrabold text-white mb-6">
¿Listo para <span style={{ color: '#4DA6FF' }}>transformar</span> tu operación aduanal?
</h2>
<p className="text-xl md:text-2xl max-w-3xl mx-auto" style={{ color: '#64B5F6' }}>
Únete a más de 500 empresas que ya optimizaron sus procesos con EFC
</p>
</div>
{/* Floating elements */}
{/* <div className="absolute w-20 h-20 rounded-full top-10 left-10 opacity-20" style={{ backgroundColor: '#4DA6FF' }}></div>
<div className="absolute w-32 h-32 rounded-full bottom-10 right-10 opacity-15" style={{ backgroundColor: '#F57C00' }}></div>
<div className="absolute w-16 h-16 rounded-full top-1/2 left-1/4 opacity-10" style={{ backgroundColor: '#2E7D32' }}></div> */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
<div className="relative px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="mb-16 text-center">
<h2 className="mb-6 text-4xl font-extrabold text-white md:text-5xl">
¿Listo para <span style={{ color: '#4DA6FF' }}>transformar</span> tu operación aduanal?
</h2>
<p className="max-w-3xl mx-auto text-xl md:text-2xl" style={{ color: '#64B5F6' }}>
Únete a más de 350 empresas que ya optimizaron sus procesos con EFC
</p>
</div>
<div className="grid items-start grid-cols-1 gap-12 lg:grid-cols-2">
<div
data-animate="contact-info"
className={`transition-all duration-1000 ${
@@ -697,9 +751,9 @@ export default function LandingAnimated() {
}`}
>
{/* Card de información de contacto */}
<div className="bg-white/10 backdrop-blur-md rounded-3xl p-8 border border-white/20">
<div className="p-8 border bg-white/10 backdrop-blur-md rounded-3xl border-white/20">
<div className="mb-8">
<h3 className="text-2xl font-bold text-white mb-4">
<h3 className="mb-4 text-2xl font-bold text-white">
Hablemos de tu proyecto
</h3>
<p className="text-lg" style={{ color: '#64B5F6' }}>
@@ -728,15 +782,15 @@ export default function LandingAnimated() {
subtitle: 'Visitas con cita previa'
}
].map((contact, idx) => (
<div key={idx} className="flex items-start space-x-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-all duration-300">
<div className="w-14 h-14 flex items-center justify-center rounded-full" style={{ backgroundColor: '#4DA6FF' }}>
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div key={idx} className="flex items-start p-4 space-x-4 transition-all duration-300 rounded-xl bg-white/5 hover:bg-white/10">
<div className="flex items-center justify-center rounded-full w-14 h-14" style={{ backgroundColor: '#4DA6FF' }}>
<svg className="text-white w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={contact.icon} />
</svg>
</div>
<div className="flex-1">
<h4 className="font-bold text-white text-lg">{contact.title}</h4>
<p className="font-semibold mb-1" style={{ color: '#4DA6FF' }}>{contact.info}</p>
<h4 className="text-lg font-bold text-white">{contact.title}</h4>
<p className="mb-1 font-semibold" style={{ color: '#4DA6FF' }}>{contact.info}</p>
<p className="text-sm" style={{ color: '#64B5F6' }}>{contact.subtitle}</p>
</div>
</div>
@@ -744,10 +798,10 @@ export default function LandingAnimated() {
</div>
{/* Botón adicional para WhatsApp */}
<div className="mt-8 pt-6 border-t border-white/20">
<div className="pt-6 mt-8 border-t border-white/20">
<a
href="#"
className="flex items-center justify-center w-full py-4 px-6 rounded-xl font-semibold text-white transition-all duration-300 transform hover:scale-105"
className="flex items-center justify-center w-full px-6 py-4 font-semibold text-white transition-all duration-300 transform rounded-xl hover:scale-105"
style={{
background: 'linear-gradient(45deg, #25D366, #128C7E)'
}}
@@ -769,10 +823,10 @@ export default function LandingAnimated() {
: 'opacity-0 translate-x-10'
}`}
>
<h3 className="text-2xl font-bold mb-6" style={{ color: '#333333' }}>Solicita una demostración</h3>
<h3 className="mb-6 text-2xl font-bold" style={{ color: '#333333' }}>Solicita una demostración</h3>
<form onSubmit={handleContactSubmit} className="space-y-4">
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
<label className="block mb-2 text-sm font-semibold" style={{ color: '#333333' }}>
Nombre completo
</label>
<input
@@ -780,7 +834,7 @@ export default function LandingAnimated() {
required
value={contactForm.name}
onChange={(e) => setContactForm({...contactForm, name: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
className="w-full px-4 py-3 transition-all duration-200 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
style={{
focusRingColor: '#4DA6FF',
outline: 'none'
@@ -792,7 +846,7 @@ export default function LandingAnimated() {
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
<label className="block mb-2 text-sm font-semibold" style={{ color: '#333333' }}>
Email corporativo
</label>
<input
@@ -800,7 +854,7 @@ export default function LandingAnimated() {
required
value={contactForm.email}
onChange={(e) => setContactForm({...contactForm, email: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
className="w-full px-4 py-3 transition-all duration-200 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
placeholder="tu@empresa.com"
@@ -808,7 +862,7 @@ export default function LandingAnimated() {
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
<label className="block mb-2 text-sm font-semibold" style={{ color: '#333333' }}>
Empresa
</label>
<input
@@ -816,7 +870,7 @@ export default function LandingAnimated() {
required
value={contactForm.company}
onChange={(e) => setContactForm({...contactForm, company: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
className="w-full px-4 py-3 transition-all duration-200 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
placeholder="Nombre de tu empresa"
@@ -824,14 +878,14 @@ export default function LandingAnimated() {
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
<label className="block mb-2 text-sm font-semibold" style={{ color: '#333333' }}>
Mensaje
</label>
<textarea
rows="4"
value={contactForm.message}
onChange={(e) => setContactForm({...contactForm, message: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
className="w-full px-4 py-3 transition-all duration-200 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
placeholder="Cuéntanos sobre tu operación aduanal..."
@@ -860,11 +914,11 @@ export default function LandingAnimated() {
</section>
{/* Footer */}
<footer className="text-white py-12" style={{ backgroundColor: '#1B2A41' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<footer className="py-12 text-white" style={{ backgroundColor: '#1B2A41' }}>
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="grid grid-cols-1 gap-8 md:grid-cols-4">
<div className="col-span-1 md:col-span-2">
<h3 className="text-2xl font-bold mb-4">
<h3 className="mb-4 text-2xl font-bold">
<span style={{
background: 'linear-gradient(to right, #4DA6FF, #64B5F6)',
WebkitBackgroundClip: 'text',
@@ -873,13 +927,13 @@ export default function LandingAnimated() {
EFC
</span>
</h3>
<p className="mb-2 max-w-md" style={{ color: '#7A7A7A' }}>
{/* <p className="max-w-md mb-2" style={{ color: '#7A7A7A' }}>
Uso correcto <span className="font-semibold" style={{ color: '#4DA6FF' }}>Aduanasoft®</span>
</p>
<ul className="mb-4 text-sm text-gray-300 space-y-1">
</p> */}
{/* <ul className="mb-4 space-y-1 text-sm text-gray-300">
<li><span className="font-bold text-white">+350</span> clientes en todo el país</li>
<li><span className="font-bold text-white">29 años</span> de experiencia</li>
</ul>
</ul> */}
<div className="flex space-x-4">
<a href="https://www.facebook.com/AduanaSoftMX" target="_blank" rel="noopener noreferrer" className="transition-colors duration-200" style={{ color: '#7A7A7A' }} onMouseEnter={e => e.target.style.color = 'white'} onMouseLeave={e => e.target.style.color = '#7A7A7A'} title="Facebook">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M22.675 0h-21.35C.595 0 0 .592 0 1.326v21.348C0 23.408.595 24 1.325 24h11.495v-9.294H9.692v-3.622h3.128V8.413c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.797.143v3.24l-1.918.001c-1.504 0-1.797.715-1.797 1.763v2.313h3.587l-.467 3.622h-3.12V24h6.116C23.406 24 24 23.408 24 22.674V1.326C24 .592 23.406 0 22.675 0"/></svg>
@@ -903,29 +957,29 @@ export default function LandingAnimated() {
</div>
<div>
<h4 className="font-semibold mb-4">Producto</h4>
<h4 className="mb-4 font-semibold">Producto</h4>
<ul className="space-y-2 text-gray-400">
<li><button onClick={() => scrollToSection('caracteristicas')} className="hover:text-white transition-colors duration-200">Características</button></li>
<li><button onClick={() => scrollToSection('precios')} className="hover:text-white transition-colors duration-200">Precios</button></li>
<li><a href="#" className="hover:text-white transition-colors duration-200">Integraciónes</a></li>
<li><a href="#" className="hover:text-white transition-colors duration-200">API</a></li>
<li><button onClick={() => scrollToSection('caracteristicas')} className="transition-colors duration-200 hover:text-white">Características</button></li>
<li><button onClick={() => scrollToSection('precios')} className="transition-colors duration-200 hover:text-white">Precios</button></li>
<li><a href="#" className="transition-colors duration-200 hover:text-white">Integraciónes</a></li>
<li><a href="#" className="transition-colors duration-200 hover:text-white">API</a></li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-4">Soporte</h4>
<h4 className="mb-4 font-semibold">Soporte</h4>
<ul className="space-y-2 text-gray-400">
<li><button onClick={() => scrollToSection('contacto')} className="hover:text-white transition-colors duration-200">Contacto</button></li>
<li><a href="#" className="hover:text-white transition-colors duration-200">Documentación</a></li>
<li><a href="#" className="hover:text-white transition-colors duration-200">Centro de Ayuda</a></li>
<li><a href="#" className="hover:text-white transition-colors duration-200">Status</a></li>
<li><button onClick={() => scrollToSection('contacto')} className="transition-colors duration-200 hover:text-white">Contacto</button></li>
<li><a href="#" className="transition-colors duration-200 hover:text-white">Documentación</a></li>
<li><a href="#" className="transition-colors duration-200 hover:text-white">Centro de Ayuda</a></li>
<li><a href="#" className="transition-colors duration-200 hover:text-white">Status</a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<div className="pt-8 mt-8 text-center text-gray-400 border-t border-gray-800">
<p>
&copy; 2025 EFC by <span className="font-semibold text-indigo-400">@AduanaSoft</span>.
&copy; {currentYear} EFC by <span className="font-semibold text-white">Aduanasoft®</span>.
Todos los derechos reservados. | Solución especializada para Agentes Aduanales e Importadores.
</p>
</div>

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

View File

@@ -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,26 +72,30 @@ 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">
@@ -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>
)}

View 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');
}
};