46 Commits

Author SHA1 Message Date
b29c586524 fix: se agrega como para seleccion de organizaciones para filtrar los procesos por organizacion y ejecutar los procesos por el filtro de organizacion establecido. 2026-02-03 16:41:06 -07:00
f9ece2923f fix: se crea boton para ejecutar los procesos de consulta a ventanilla unica. 2026-02-03 10:23:00 -07:00
96e0b4eea2 Merge pull request 'fix: se agrega nueva pestaña para visualizar los archivos de error que devuelve vucem en detalle pedimento.' (#19) from T2025-09-004 into main
Reviewed-on: #19
2026-01-29 18:09:37 +00:00
7c40275381 fix: se agrega nueva pestaña para visualizar los archivos de error que devuelve vucem en detalle pedimento. 2026-01-29 08:07:52 -07:00
407597c959 Merge pull request 'fix: se ajusta mensaje de error cuando se sube una carpeta, zip o rar con archivos.' (#18) from fix-T2025-09-007 into main
Reviewed-on: #18
2026-01-27 17:03:44 +00:00
07facb8ae2 fix: se ajusta mensaje de error cuando se sube una carpeta, zip o rar con archivos. 2026-01-26 15:54:20 -07:00
0d60156b7f Merge pull request 'actualizar el formulario de vucem para permitir agregar organizacion' (#17) from vucem-formulario into main
Reviewed-on: #17
2026-01-16 16:55:45 +00:00
Dulce
5a0e9a8259 actualizar el formulario de vucem para permitir agregar organizacion 2026-01-16 09:54:04 -07:00
a36178df1b Merge pull request 'se instala libreia de iconos y se modifica vista para habilitar selccion multiple y carga de archivos.' (#16) from T2025-10-152-001 into main
Reviewed-on: #16
2026-01-07 22:07:45 +00:00
50716e363d se instala libreia de iconos y se modifica vista para habilitar selccion multiple y carga de archivos. 2026-01-07 14:56:00 -07:00
4b66b4944a Merge pull request 'Fix--Auditor' (#15) from Fix--Auditor into main
Reviewed-on: #15
2026-01-07 17:36:16 +00:00
4414923d04 Nuevos Ajustes en la vista de modal de Peticiones y Respuestas 2026-01-02 07:39:28 -07:00
a78ed6f51b Fix: Se ajusta vista para ver los archivos de peticion y error en cada uno de las consultas realizadas a VU, asi como agregar codigo que manda llamar al backend que ya estaba hecho para auditar cada seccion de pedimento completo, partidas, etc. 2025-12-31 08:37:05 -07:00
dfddb418d0 Merge pull request 'modificacion reportes' (#14) from correccion-reportes into main
Reviewed-on: #14
2025-12-16 15:14:24 +00:00
Dulce
d46ea97340 modificacion reportes 2025-12-16 08:13:00 -07:00
29e0f1a68d Merge pull request 'Fix: se ajusta campo de fuente del pedimento detalle.' (#13) from T2025-10-021 into main
Reviewed-on: #13
2025-12-16 14:33:24 +00:00
3f01709952 Fix: se ajusta campo de fuente del pedimento detalle. 2025-12-16 07:24:40 -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
20 changed files with 8891 additions and 3070 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

105
package-lock.json generated
View File

@@ -14,6 +14,8 @@
"@tanstack/react-query": "^5.62.7",
"chart.js": "^4.5.0",
"highlight.js": "^11.11.1",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0",
@@ -1706,6 +1708,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 +2428,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 +2443,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 +2508,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 +2557,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",
@@ -2557,6 +2604,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.562.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -2703,6 +2759,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 +2981,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 +3097,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 +3152,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 +3171,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 +3831,6 @@
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {

View File

@@ -16,6 +16,8 @@
"@tanstack/react-query": "^5.62.7",
"chart.js": "^4.5.0",
"highlight.js": "^11.11.1",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"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';
import { fetchWithAuth, postWithAuth } 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,79 @@ export async function fetchProcesamientoPedimentos(
}
});
const res = await fetchWithAuth(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`);
console.log('Params:', 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;
}
}
// Interfaz para la respuesta del comando
export interface ComandoResponse {
message?: string;
error?: string;
}
// Interfaz para los parámetros de ejecución
export interface EjecutarComandoParams {
procesamiento?: string;
organizacionid?: string;
todos?: boolean;
}
// API para ejecutar comando de procesamiento
export async function ejecutarComando(
params: EjecutarComandoParams
): Promise<ComandoResponse> {
try {
const API_URL = (import.meta as any).env.VITE_EFC_API_URL;
console.log('API_URL:', API_URL);
// Preparar los datos para la petición POST
const requestData: any = {};
if (params.procesamiento !== undefined) {
requestData.procesamiento = params.procesamiento;
}
if (params.organizacionid !== undefined) {
requestData.organizacionid = params.organizacionid;
}
if (params.todos !== undefined) {
requestData.todos = params.todos;
}
// const res = await fetchWithAuth(`${API_URL}/customs/procesamientopedimentos-ejecutar-comando/`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(requestData),
// });
const res = await postWithAuth(`${API_URL}/customs/procesamientopedimentos-ejecutar-comando/`, requestData);
if (!res.ok) {
// Intentar obtener el mensaje de error del servidor
try {
const errorData = await res.json();
throw new Error(errorData.message || errorData.error || `Error ${res.status}: ${res.statusText}`);
} catch {
throw new Error(`Error ${res.status}: ${res.statusText}`);
}
}
return await res.json();
} catch (error) {
// console.error('Error in ejecutarComando:', 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"

File diff suppressed because it is too large Load Diff

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>
{/* 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>
<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>
<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>
{/* 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>
{/* 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="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 grid-cols-1 lg:grid-cols-2 gap-12 items-start">
<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,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>
)}

View File

@@ -202,10 +202,13 @@ function RelacionarImportadoresModal({ open, onClose, vucem }) {
}
import React, { useEffect, useState } from 'react';
import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth, putFormDataWithAuth, postFormDataWithAuth, patchWithAuth } from '../fetchWithAuth';
import { useUser } from '../context/UserContext';
const API_URL = import.meta.env.VITE_EFC_API_URL;
export default function Vucem() {
// Estado para modal de relacionar importadores
const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true';
const [showRelacionarModal, setShowRelacionarModal] = useState(false);
const [selectedVucem, setSelectedVucem] = useState(null);
const [vucemList, setVucemList] = useState([]);
@@ -216,10 +219,13 @@ export default function Vucem() {
const [editVucem, setEditVucem] = useState(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteVucem, setDeleteVucem] = useState(null);
const [organizaciones, setOrganizaciones] = useState([]);
const { user: currentUser } = useUser();
// Estado para formulario de creación/edición
const initialForm = {
usuario: '',
organizacion: '',
password: '',
patente: '',
efirma: '',
@@ -260,6 +266,22 @@ export default function Vucem() {
}));
};
useEffect(() => {
const fetchOrganizaciones = async () => {
try {
const url = `${import.meta.env.VITE_EFC_API_URL}/organization/organizaciones/`;
const res = await fetchWithAuth(url); // ← USA fetchWithAuth
if (!res.ok) throw new Error('Error al obtener las organizaciones');
const data = await res.json();
setOrganizaciones(data);
} catch (err) {
console.error('Error fetching organizaciones:', err);
setOrganizaciones([]); // ← Asegurar que siempre sea un array
}
};
fetchOrganizaciones();
}, []);
// Funciones para alternar visibilidad en modal de edición
const toggleEditPasswordVisibility = () => {
setShowEditPassword(!showEditPassword);
@@ -358,6 +380,7 @@ export default function Vucem() {
const res = await fetchWithAuth(`${API_URL}/vucem/vucem/`);
if (!res.ok) throw new Error('Error al cargar Ventanilla Unica');
const data = await res.json();
// console.log('data > ', data);
setVucemList(data);
setError(null);
} catch (err) {
@@ -1272,12 +1295,13 @@ export default function Vucem() {
formData.append('password', form.password);
formData.append('patente', form.patente);
formData.append('efirma', form.efirma);
if (form.key) formData.append('key', form.key);
if (form.cer) formData.append('cer', form.cer);
formData.append('is_importador', form.is_importador);
formData.append('is_active', form.is_active);
formData.append('acusecove', form.acusecove);
formData.append('acuseedocument', form.acuseedocument);
formData.append('is_active', form.is_active);
formData.append('organizacion_id', form.organizacion);
if (form.key) formData.append('key', form.key);
if (form.cer) formData.append('cer', form.cer);
try {
const res = await postFormDataWithAuth(`${API_URL}/vucem/vucem/`, formData);
if (!res.ok) throw new Error('Error al crear');
@@ -1553,6 +1577,54 @@ export default function Vucem() {
</div>
</div>
{/* Seccion de organizacion para super_users */}
{currentUser.is_superuser ? (
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<div className="flex items-center mb-3 pb-2 border-b border-slate-300">
<div className="bg-amber-600 rounded-lg p-2 mr-3 shadow-sm">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<h4 className="text-sm font-semibold text-slate-800">Configuracion de Organizaciones</h4>
<p className="text-xs text-slate-600">Asignar la Organizacion a la que pertenece.</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-1 gap-4">
<div className="space-y-1">
<label className="block text-xs font-semibold text-slate-700">
Seleccionar Organizacion <span className="text-red-600">*</span>
</label>
<div className="relative rounded-lg shadow-sm">
<select
name="organizacion"
value={form.organizacion || ''}
onChange={handleInputChange}
className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm
transition-all duration-200 bg-white appearance-none"
>
<option value="">Todas las organizaciones</option>
{organizaciones.results && organizaciones.results.map(org => (
<option key={org.id} value={org.id}>
{org.nombre}
</option>
))}
</select>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg className="h-5 w-5 text-gray-400 group-focus-within:text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200"></div>
)}
{/* Botones de acción */}
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 pt-4 border-t border-slate-200">
<button

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