16 Commits

Author SHA1 Message Date
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
3f01709952 Fix: se ajusta campo de fuente del pedimento detalle. 2025-12-16 07:24:40 -07: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
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
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
0c4a48a60b Merge pull request 'repoCumplimiento' (#6) from repoCumplimiento into main
Reviewed-on: #6
2025-10-22 03:35:28 +00:00
539954eb41 Merge pull request 'Cambios en admin' (#5) from admin into main
Reviewed-on: #5
2025-10-16 01:42:15 +00: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
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
9 changed files with 5080 additions and 704 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",

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

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

@@ -94,11 +94,9 @@ export default function Reports() {
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/table-summary/${params ? `?${params}` : ''}`;
console.log('Cumplimiento Report Request:', url);
try {
const res = await fetchWithAuth(url);
const res = await fetchWithAuth(url);
const data = await res.json();
console.log('Cumplimiento Report Response:', data);
if (!res.ok) throw new Error('Error al generar el reporte');
alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.');
} catch (err) {
@@ -123,6 +121,47 @@ export default function Reports() {
};
const [summaryData, setSummaryData] = useState(null);
const initialFiltersControlPedimento = {
pedimento_app: '',
fecha_pago__gte: '',
fecha_pago__lte: '',
organizacion_id: organizacionId || '',
};
// control_pedimento
const [filtersControlPedimento, setFiltersControlPedimento] = useState(initialFiltersControlPedimento);
const handleFilterChangeControlPedimento = (e) => {
setFiltersControlPedimento({ ...filtersControlPedimento, [e.target.name]: e.target.value });
};
const handleGenerarReporteControlPedimento = async () => {
// if (!organizacionId ) {
// alert('No se pudo obtener el organizacion_id. Intenta de nuevo más tarde.');
// return;
// }
// Build query params from filtersCumplimiento and add organizacion_id
const paramsObj = { ...filtersControlPedimento };
if(paramsObj.organizacion_id == ''){
alert('No se pudo obtener el organizacion_id. Selecciona tu organizacion para intenta de nuevo.');
return;
}
const params = Object.entries(paramsObj)
.filter(([_, v]) => v)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/control-pedimento/${params ? `?${params}` : ''}`;
try {
const res = await fetchWithAuth(url);
const data = await res.json();
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.');
}
};
// Fetch summary data for dashboard/cards
const fetchSummary = async () => {
try {
@@ -156,6 +195,233 @@ export default function Reports() {
const [showTour, setShowTour] = useState(false);
const [tourStep, setTourStep] = useState(0);
const [organizaciones, setOrganizaciones] = useState([]);
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();
}, []);
const [globalFilters, setGlobalFilters] = useState({
rfc: '',
fecha_pago_desde: '',
fecha_pago_hasta: '',
organizacion: '',
patente: '',
pedimento: ''
});
const renderGlobalFilters = () => (
<div className="mb-6">
<div className="bg-white rounded-xl shadow-lg overflow-hidden border border-blue-100">
<div className="p-4 border-b border-blue-50">
<div className="flex items-center gap-3">
<div className="h-10 w-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
</div>
<div>
<h3 className="font-bold text-gray-900">Filtros globales</h3>
<p className="text-sm text-gray-500">Filtros aplicables a todos los modelos</p>
</div>
</div>
</div>
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Filtro por RFC */}
<div className="group">
<label className="block text-sm font-medium text-gray-700 mb-1">
RFC
</label>
<div className="relative rounded-lg shadow-sm">
<input
type="text"
value={globalFilters.rfc || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
rfc: e.target.value
}))}
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"
placeholder="Ej: ABC123456789"
/>
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
{/* Filtro por Fecha Pago Desde */}
<div className="group">
<label className="block text-sm font-medium text-gray-700 mb-1">
Fecha Pago Desde
</label>
<div className="relative rounded-lg shadow-sm">
<input
type="date"
value={globalFilters.fecha_pago_desde || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
fecha_pago_desde: e.target.value
}))}
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"
/>
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
{/* Filtro por Fecha Pago Hasta */}
<div className="group">
<label className="block text-sm font-medium text-gray-700 mb-1">
Fecha Pago Hasta
</label>
<div className="relative rounded-lg shadow-sm">
<input
type="date"
value={globalFilters.fecha_pago_hasta || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
fecha_pago_hasta: e.target.value
}))}
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"
/>
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
</div>
{/* Filtro por Patente */}
<div className="group">
<label className="block text-sm font-medium text-gray-700 mb-1">
Patente
</label>
<div className="relative rounded-lg shadow-sm">
<input
type="text"
value={globalFilters.patente || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
patente: e.target.value
}))}
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"
placeholder="Ej: 1234"
/>
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
{/* Filtro por Pedimento */}
<div className="group">
<label className="block text-sm font-medium text-gray-700 mb-1">
Pedimento
</label>
<div className="relative rounded-lg shadow-sm">
<input
type="text"
value={globalFilters.pedimento || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
pedimento: e.target.value
}))}
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"
placeholder="Ej: 1234567"
/>
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
{/* Filtro por Organización */}
<div className="group">
<label className="block text-sm font-medium text-gray-700 mb-1">
Organización
</label>
<div className="relative rounded-lg shadow-sm">
<select
value={globalFilters.organizacion || ''}
onChange={(e) => setGlobalFilters(prev => ({
...prev,
organizacion: e.target.value
}))}
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} {/* Usar el campo 'nombre' que sí existe */}
</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>
{/* Botón para limpiar filtros globales */}
<div className="mt-4 flex justify-end">
<button
onClick={() => setGlobalFilters({
rfc: '',
fecha_pago_desde: '',
fecha_pago_hasta: '',
organizacion: '',
patente: '',
pedimento: ''
})}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200
focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
>
Limpiar filtros globales
</button>
</div>
</div>
</div>
</div>
);
// Estado para formato de exportación personalizado
const [showFormatSelector, setShowFormatSelector] = useState(false);
@@ -172,11 +438,27 @@ export default function Reports() {
const initialModels = activeTab === 'pedimentos' ? pedimentosModels : datastageModels;
const defaultModel = initialModels[0] || { model: '', fields: [], filters: {} };
// esquema para el resto
// Estado para modelo seleccionado
const [selectedModel, setSelectedModel] = useState(defaultModel.model);
// Estado para campos seleccionados
const [selectedFields, setSelectedFields] = useState(defaultModel.fields);
// esquema para el nuevo
const [modoMultiple, setModoMultiple] = useState(false);
const [selectedModels, setSelectedModels] = useState(defaultModel.model);
const [selectedFieldsDataStage, setSelectedFieldsDataStage] = useState([]);
const [modelFieldsMap, setModelFieldsMap] = useState({});
const isModoMultiple = Object.keys(modelFieldsMap).length > 1;
useEffect(() => {
if (selectedModel) {
// Cargar campos previamente seleccionados para este modelo
const camposGuardados = modelFieldsMap[selectedModel] || [];
setSelectedFieldsDataStage(camposGuardados);
}
}, [selectedModel, modelFieldsMap]);
// Estado para campo seleccionado en lista disponible
const [availableSelected, setAvailableSelected] = useState(null);
@@ -324,6 +606,7 @@ export default function Reports() {
useEffect(() => {
fetchSummary();
}, []);
// Función para manejar la exportación del modelo
const handleExportModel = async () => {
if (selectedFields.length === 0) {
@@ -429,6 +712,353 @@ export default function Reports() {
}
};
// Modificar la función addField para actualizar el mapeo
const addFieldDataStage = (field) => {
const nuevosCampos = [...selectedFieldsDataStage, field];
// Actualizar estado local
setSelectedFieldsDataStage(nuevosCampos);
// Actualizar mapeo global
setModelFieldsMap(prev => ({
...prev,
[selectedModel]: nuevosCampos
}));
};
// Modificar la función removeField para actualizar el mapeo
const removeFieldDataStage = (field) => {
const nuevosCampos = selectedFieldsDataStage.filter(f => f !== field);
// Actualizar estado local
setSelectedFieldsDataStage(nuevosCampos);
// Actualizar mapeo global
setModelFieldsMap(prev => ({
...prev,
[selectedModel]: nuevosCampos
}));
};
// Modificar includeAllFields
const includeAllFieldsDataStage = () => {
const currentModel = datastageModels.find(m => m.model === selectedModel);
if (!currentModel) return;
setSelectedFieldsDataStage([...currentModel.fields]);
setModelFieldsMap(prev => ({
...prev,
[selectedModel]: [...currentModel.fields]
}));
};
// Modificar removeAllFields
const removeAllFieldsDataStage = () => {
setSelectedFieldsDataStage([]);
setModelFieldsMap(prev => ({
...prev,
[selectedModel]: []
}));
};
// Nueva función específica para DataStage múltiple
const handleExportDataStage = async () => {
// Verificar que haya al menos un modelo con campos seleccionados
const modelosConCampos = Object.entries(modelFieldsMap)
.filter(([_, campos]) => campos.length > 0)
.map(([modelo]) => modelo);
if (modelosConCampos.length === 0) {
alert('Por favor selecciona al menos un campo en algún modelo');
return;
}
setIsExporting(true);
try {
const progressDiv = document.createElement('div');
progressDiv.className = 'fixed bottom-4 right-4 bg-white p-4 rounded-lg shadow-xl border border-blue-100';
progressDiv.innerHTML = `
<div class="flex items-center gap-3">
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
<p class="text-sm text-gray-600">Preparando exportación DataStage...</p>
</div>
`;
document.body.appendChild(progressDiv);
// DETECCIÓN AUTOMÁTICA DEL MODO
const modo = modelosConCampos.length > 1 ? 'multiple' : 'simple';
const exportData = {
modo: modo,
format: exportFormat,
globalFilters: globalFilters
};
if (modo === 'simple') {
// MODO SIMPLE: solo un modelo con campos
const modeloUnico = modelosConCampos[0];
const modelData = datastageModels.find(m => m.model === modeloUnico);
exportData.model = modeloUnico;
exportData.fields = modelFieldsMap[modeloUnico];
} else {
// MODO MÚLTIPLE: varios modelos con campos
exportData.models = modelosConCampos.map(modelo => {
const modelData = datastageModels.find(m => m.model === modelo);
return {
model: modelo,
name: modelData?.name || modelo,
fields: modelFieldsMap[modelo]
};
});
}
// Resto del código de exportación...
progressDiv.innerHTML = `
<div class="flex items-center gap-3">
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-green-500"></div>
<p class="text-sm text-gray-600">Generando archivo DataStage...</p>
</div>
`;
const response = await fetchWithAuth(`${API_URL}/reports/exportmodel/datastage/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(exportData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || errorData.message || 'Error al exportar DataStage');
}
const blob = await response.blob();
const contentDisposition = response.headers.get('Content-Disposition');
let fileName = '';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+?)"/) ||
contentDisposition.match(/filename=([^;]+)/);
if (filenameMatch) {
fileName = filenameMatch[1].replace(/"/g, '');
}
}
if (!fileName) {
const isZip = blob.type === 'application/zip';
const isExcel = blob.type.includes('spreadsheetml');
if (isZip) {
fileName = modo === 'multiple'
? `datastage_reports_${new Date().toISOString().split('T')[0]}.zip`
: `datastage_${modelosConCampos[0]}_particionado_${new Date().toISOString().split('T')[0]}.zip`;
} else if (isExcel) {
fileName = `datastage_${modelosConCampos[0]}_${new Date().toISOString().split('T')[0]}.xlsx`;
} else {
fileName = `datastage_${modelosConCampos[0]}_${new Date().toISOString().split('T')[0]}.csv`;
}
}
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
progressDiv.innerHTML = `
<div class="flex items-center gap-3 text-green-600">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<p class="text-sm">¡Exportación completada! (Modo ${modo})</p>
</div>
`;
setTimeout(() => {
progressDiv.style.opacity = '0';
progressDiv.style.transform = 'translateY(100%)';
setTimeout(() => progressDiv.remove(), 300);
}, 2000);
showMessage(`¡Archivo ${fileName} descargado exitosamente! (${modo === 'multiple' ? 'Múltiple' : 'Simple'})`, 'success');
} catch (error) {
console.error('❌ ERROR AL EXPORTAR DATASTAGE:', error);
showMessage(error.message || 'Error al exportar DataStage. Por favor intente nuevamente.', 'error');
} finally {
setIsExporting(false);
const progressDiv = document.querySelector('.fixed.bottom-4.right-4');
if (progressDiv) progressDiv.remove();
}
};
const renderFieldsDataStage = () => {
const currentModel = datastageModels.find(m => m.model === selectedModel);
const availableFields = currentModel ? currentModel.fields : [];
// 🔥 CORREGIR: Siempre usar selectedFieldsDataStage para el modelo actual
const selectedFieldsForModel = selectedFieldsDataStage;
const formatFieldName = (field) => {
return field.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
return (
<div className="mb-6">
<div className="bg-white rounded-xl shadow-lg overflow-hidden border border-blue-100">
<div className="p-4 border-b border-blue-50">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<div>
<h3 className="font-bold text-gray-900">
{modoMultiple ? `Campos de ${selectedModel}` : 'Campos del reporte'}
</h3>
<p className="text-sm text-gray-500">
{modoMultiple
? `Selecciona campos para ${selectedModel}`
: 'Selecciona los campos a incluir'
}
</p>
</div>
</div>
<span className="inline-flex items-center px-3 py-1 rounded-full bg-blue-50 text-blue-700 text-sm font-medium">
{selectedFieldsForModel.length} seleccionados
</span>
</div>
{/* Panel de campos */}
<div className="flex gap-4 flex-col lg:flex-row">
{/* Panel izquierdo - Campos disponibles */}
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700">Campos disponibles</h4>
<button
onClick={includeAllFieldsDataStage}
disabled={availableFields.length === selectedFieldsForModel.length}
className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
Incluir todos
</button>
</div>
<div className="bg-gray-50 rounded-lg p-2 max-h-80 overflow-y-auto custom-scrollbar">
{availableFields.filter(field => !selectedFieldsForModel.includes(field)).length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-400">
<svg className="w-12 h-12 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p className="text-sm">Todos los campos están incluidos</p>
</div>
) : (
<div className="grid grid-cols-1 gap-1">
{availableFields
.filter(field => !selectedFieldsForModel.includes(field))
.map(field => (
<div
key={field}
onClick={() => addFieldDataStage(field)}
className="group flex items-center justify-between p-2 rounded-md cursor-pointer transition-all duration-200 hover:bg-gray-100"
>
<span className="text-sm font-medium">{formatFieldName(field)}</span>
<button
onClick={(e) => {
e.stopPropagation();
addFieldDataStage(field);
}}
className="opacity-0 group-hover:opacity-100 p-1 rounded-md hover:bg-blue-100 text-blue-600 transition-all duration-200"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
))}
</div>
)}
</div>
</div>
{/* Panel derecho - Campos seleccionados */}
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700">
{modoMultiple ? `Campos incluidos en ${selectedModel}` : 'Campos incluidos'}
</h4>
<button
onClick={removeAllFieldsDataStage}
disabled={selectedFieldsForModel.length === 0}
className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Quitar todos
</button>
</div>
<div className="bg-gray-50 rounded-lg p-2 max-h-80 overflow-y-auto custom-scrollbar">
{selectedFieldsForModel.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-400">
<svg className="w-12 h-12 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-sm">No hay campos seleccionados</p>
</div>
) : (
<div className="grid grid-cols-1 gap-1">
{selectedFieldsForModel.map(field => (
<div
key={field}
className="group flex items-center justify-between p-2 rounded-md cursor-pointer transition-all duration-200 hover:bg-gray-100"
onClick={() => removeFieldDataStage(field)}
>
<span className="text-sm font-medium">
{formatFieldName(field)}
{modoMultiple && (
<span className="text-xs text-gray-500 ml-2">({selectedModel})</span>
)}
</span>
<button
onClick={(e) => {
e.stopPropagation();
removeFieldDataStage(field);
}}
className="opacity-0 group-hover:opacity-100 p-1 rounded-md hover:bg-red-100 text-red-600 transition-all duration-200"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
const renderFields = () => {
const formatFieldName = (field) => {
return field.split('_')
@@ -752,6 +1382,118 @@ export default function Reports() {
</button>
</div>
),
control_pedimentos: (
<div className="p-6">
<h2 className="text-xl font-bold mb-2 text-blue-900">Generar reporte de Control de Requerimiento</h2>
<p className="mb-4 text-gray-700">Aquí puedes generar y descargar el reporte de Control de Requerimiento.</p>
{/* Filtros replicados */}
<div className="max-w-7xl mx-auto mt-6 mb-4 px-4">
<form onSubmit={(e) => {
e.preventDefault();
fetchSummary();
}} className="bg-white rounded-lg shadow-sm border border-slate-200 p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
{Object.keys(initialFiltersControlPedimento).map((key) => (
<div key={key}>
<label className="block text-xs font-medium text-slate-600 mb-1" htmlFor={key}>
{key.replace(/_/g, ' ').replace('gte', 'desde').replace('lte', 'hasta')}
</label>
{key === 'organizacion_id' ? (
<select
name={key}
id={key}
value={filtersControlPedimento[key]}
onChange={handleFilterChangeControlPedimento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Seleccionar organización</option>
{organizaciones.results && organizaciones.results.map(org => (
<option key={org.id} value={org.id}>
{org.nombre} {/* Usar el campo 'nombre' que sí existe */}
</option>
))}
</select>
) : (
<input
type={key.includes('fecha') ? 'date' : 'text'}
name={key}
id={key}
value={filtersControlPedimento[key]}
onChange={handleFilterChangeControlPedimento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
)}
</div>
))}
</div>
<div className="flex justify-end">
<div className="flex gap-2">
<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={handleGenerarReporteControlPedimento}
>
Generar Reporte
</button>
</div>
</div>
</form>
</div>
{/* Aquí va la lógica y UI específica para Cumplimiento */}
{/* 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.filter(r => r.report_type === 'control_pedimento').length > 0 ? (
reports
.filter(r => r.report_type === 'control_pedimento')
.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">
{reports.length > 0 ? 'No hay reportes de control de pedimento' : 'No hay reportes disponibles'}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
),
datastage: (
<div className="p-6 bg-white/95 rounded-2xl shadow-xl border border-blue-100">
<div className="relative mb-8">
@@ -827,14 +1569,43 @@ export default function Reports() {
</div>
<div className="mb-6">
<h3 className="text-md font-bold text-blue-800 mb-3">Campos</h3>
{renderFields()}
{renderFieldsDataStage()}
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-blue-800">Busqueda por modelo: </span>
<span className={`text-sm font-bold ${isModoMultiple ? 'text-purple-600' : 'text-green-600'}`}>
{isModoMultiple ? 'MÚLTIPLES MODELOS' : 'SINGULAR'}
</span>
</div>
<div className="text-xs text-blue-600">
{Object.keys(modelFieldsMap).filter(model => modelFieldsMap[model]?.length > 0).length} modelo(s) con campos
</div>
</div>
{/* Mostrar modelos activos */}
{Object.keys(modelFieldsMap).filter(model => modelFieldsMap[model]?.length > 0).length > 0 && (
<div className="mt-2 text-xs text-blue-700">
<strong>Modelos activos:</strong> {Object.keys(modelFieldsMap)
.filter(model => modelFieldsMap[model]?.length > 0)
.map(model => `${model} (${modelFieldsMap[model].length} campos)`)
.join(', ')}
</div>
)}
</div>
</div>
<div className="mb-6">
<h3 className="text-md font-bold text-blue-800 mb-3">Filtros Globales</h3>
{renderGlobalFilters()}
</div>
{/* <div className="mb-6">
<h3 className="text-md font-bold text-blue-800 mb-3">Filtros</h3>
{renderFilters()}
</div>
</div> */}
<button
onClick={handleExportModel}
onClick={handleExportDataStage}
disabled={isExporting}
className={`group relative w-full py-3 text-lg font-semibold ${isExporting
? 'bg-gray-400 cursor-not-allowed'
@@ -936,31 +1707,35 @@ export default function Reports() {
</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>
))
{reports.filter(r => r.report_type === 'cumplimiento').length > 0 ? (
reports
.filter(r => r.report_type === 'cumplimiento')
.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>
<td colSpan={6} className="px-4 py-2 text-center text-slate-400">
{reports.length > 0 ? 'No hay reportes de cumplimiento' : 'No hay reportes disponibles'}
</td>
</tr>
)}
</tbody>
@@ -1105,6 +1880,22 @@ export default function Reports() {
<span>Pedimentos cargados</span>
</div>
</button>
<button
className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${activeTab === 'control_pedimentos'
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'
: 'text-gray-700 hover:bg-blue-50/80'
}`}
onClick={() => setActiveTab('control_pedimentos')}
>
<div className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>Control de Pedimentos</span>
</div>
</button>
<button
className={`flex-1 py-3 px-4 text-sm font-semibold rounded-xl focus:outline-none transition-all duration-200 ${activeTab === 'datastage'
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/20 scale-[1.02]'