From e86547d10d09b0e11320cc3048057a3c846d02e2 Mon Sep 17 00:00:00 2001 From: Kevin Rosales Date: Thu, 14 Aug 2025 16:35:09 -0600 Subject: [PATCH] Se agrego datastage --- .env | 4 +- src/App.jsx | 7 + src/api/datastage.js | 39 +++ src/components/ConfirmModal.jsx | 33 +++ src/components/Sidebar.jsx | 12 + src/fetchWithAuth.js | 13 +- src/pages/Datastage.jsx | 476 ++++++++++++++++++++++++++++++++ src/pages/Documents.jsx | 5 +- 8 files changed, 579 insertions(+), 10 deletions(-) create mode 100644 src/api/datastage.js create mode 100644 src/components/ConfirmModal.jsx create mode 100644 src/pages/Datastage.jsx diff --git a/.env b/.env index bae8a6f..4694ca2 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ VITE_DEBUG_MODE=false -VITE_EFC_API_URL=http://192.168.1.195:8000/api/v1 -VITE_EFC_MICROSERVICE_URL=http://192.168.1.195:8001/api/v1 +VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1 +VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservicio/api/v1 diff --git a/src/App.jsx b/src/App.jsx index 00b1f61..79ef5d5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ import Documents from './pages/Documents'; +import Datastage from './pages/Datastage'; import Agenda from './pages/Agenda'; import Vucem from './pages/Vucem'; import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom'; @@ -97,6 +98,12 @@ function AppContent() { } /> + {/* Ruta para Datastage */} + + + + } /> {/* Ruta para agenda */} diff --git a/src/api/datastage.js b/src/api/datastage.js new file mode 100644 index 0000000..b15329d --- /dev/null +++ b/src/api/datastage.js @@ -0,0 +1,39 @@ + +import.meta.env; +import { getWithAuth, postWithAuth, patchWithAuth, deleteWithAuth } from '../fetchWithAuth'; +const API_BASE = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/`; + +export async function fetchDatastages() { + const res = await getWithAuth(API_BASE); + if (!res.ok) throw new Error('Error al obtener datastages'); + const data = await res.json(); + // Si la respuesta es paginada, devolver results + if (data && Array.isArray(data.results)) return data.results; + // Si la respuesta no es un array, devolver array vacío + if (!Array.isArray(data)) return []; + return data; +} + +export async function fetchDatastageDetail(id) { + const res = await getWithAuth(`${API_BASE}${id}/`); + if (!res.ok) throw new Error('Error al obtener detalle'); + return res.json(); +} + +export async function createDatastage(data) { + const res = await postWithAuth(API_BASE, data); + if (!res.ok) throw new Error('Error al crear datastage'); + return res.json(); +} + +export async function updateDatastage(id, data) { + const res = await patchWithAuth(`${API_BASE}${id}/`, data); + if (!res.ok) throw new Error('Error al actualizar datastage'); + return res.json(); +} + +export async function deleteDatastage(id) { + const res = await deleteWithAuth(`${API_BASE}${id}/`); + if (!res.ok) throw new Error('Error al eliminar datastage'); + return true; +} diff --git a/src/components/ConfirmModal.jsx b/src/components/ConfirmModal.jsx new file mode 100644 index 0000000..02ec47a --- /dev/null +++ b/src/components/ConfirmModal.jsx @@ -0,0 +1,33 @@ +import React from 'react'; + +export default function ConfirmModal({ open, onClose, onConfirm, message = '¿Estás seguro?', confirmText = 'Confirmar', cancelText = 'Cancelar' }) { + if (!open) return null; + return ( +
+
+ + + +

{message}

+
+ + +
+
+ +
+ ); +} diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 3a9436d..c6a1998 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -140,6 +140,18 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) { ) + }, + { + name: 'Datastage', + path: '/datastage', + icon: ( + + + + + + ), + onClick: () => navigate('/datastage') } ] }, diff --git a/src/fetchWithAuth.js b/src/fetchWithAuth.js index 5c98fb2..565a2c0 100644 --- a/src/fetchWithAuth.js +++ b/src/fetchWithAuth.js @@ -86,14 +86,17 @@ const refreshToken = async () => { // Función principal para hacer peticiones con manejo automático de tokens export const fetchWithAuth = async (url, options = {}) => { + // Obtener el token actual let token = localStorage.getItem('access'); - + // Configurar headers por defecto - const defaultHeaders = { - 'Content-Type': 'application/json', - ...(token && { 'Authorization': `Bearer ${token}` }) - }; + let defaultHeaders = {}; + if (token) defaultHeaders['Authorization'] = `Bearer ${token}`; + // Solo poner Content-Type si no es GET o si hay body + if ((options.method && options.method.toUpperCase() !== 'GET') || options.body) { + defaultHeaders['Content-Type'] = 'application/json'; + } // Combinar headers const finalOptions = { diff --git a/src/pages/Datastage.jsx b/src/pages/Datastage.jsx new file mode 100644 index 0000000..ef4a985 --- /dev/null +++ b/src/pages/Datastage.jsx @@ -0,0 +1,476 @@ + + +import React, { useEffect, useState, useLayoutEffect, useRef } from 'react'; +import SuccessModal from '../components/SuccessModal.jsx'; +import ConfirmModal from '../components/ConfirmModal.jsx'; +import { + fetchDatastages, + fetchDatastageDetail, + createDatastage, + updateDatastage, + deleteDatastage +} from '../api/datastage'; +import { postFormDataWithAuth, patchFormDataWithAuth } from '../fetchWithAuth'; +// PATCH para actualizar solo el campo procesado +async function patchProcesadoTrue(item) { + const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${item.id}/`; + const body = { procesado: true }; + return fetchWithAuth(url, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); +} +import { fetchWithAuth } from '../fetchWithAuth'; + +// Modal para mostrar registros cargados +function RegistrosCargadosModal({ open, onClose, registros }) { + if (!open || !registros) return null; + return ( +
+
+

Registros cargados

+
+ + + + + + + + + {Object.entries(registros).map(([registro, cantidad]) => ( + + + + + ))} + +
RegistroCantidad
{registro}{cantidad}
+
+ +
+
+ ); +} + +// Procesar datastage (adaptado para mostrar registros cargados) +async function procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal) { + try { + const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${item.id}/procesar/`; + const body = { + organizacion: item.organizacion, + contribuyente: item.contribuyente, + procesado: item.procesado + }; + const res = await fetchWithAuth(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (res.status === 200) { + // PATCH para marcar como procesado en backend + await patchProcesadoTrue(item); + setDatastages(prev => prev.map(d => d.id === item.id ? { ...d, procesado: true } : d)); + setSuccess('Procesado correctamente'); + const data = await res.json(); + if (data && data.registros_cargados) { + setRegistrosCargados(data.registros_cargados); + setShowRegistrosModal(true); + } + } else { + setError('No se pudo procesar el datastage'); + } + } catch (e) { + setError('No se pudo procesar el datastage'); + } +} +// Descarga autenticada de archivos datastage +function downloadDatastageFile(id, filename) { + const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${id}/download-datastage/`; + fetchWithAuth(url, { method: 'GET' }) + .then(async res => { + if (!res.ok) throw new Error('Error al descargar archivo'); + const blob = await res.blob(); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = filename || `datastage_${id}.zip`; + document.body.appendChild(link); + link.click(); + link.remove(); + }) + .catch(() => alert('No se pudo descargar el archivo.')); +} + +export default function Datastage() { + const focusKeeperRef = useRef(null); + const [datastages, setDatastages] = useState([]); + const [selected, setSelected] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [form, setForm] = useState({ archivo: null, contribuyente: '' }); + const [editingId, setEditingId] = useState(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showDetailModal, setShowDetailModal] = useState(false); + const [success, setSuccess] = useState(''); + const [showSuccessModal, setShowSuccessModal] = useState(false); + // Estado para mostrar registros cargados + const [showRegistrosModal, setShowRegistrosModal] = useState(false); + const [registrosCargados, setRegistrosCargados] = useState(null); + const [deleteId, setDeleteId] = useState(null); + // Animación header + const [showAnimation, setShowAnimation] = useState(false); + const [hasAnimated, setHasAnimated] = useState(false); + useLayoutEffect(() => { setShowAnimation(true); }, []); + useEffect(() => { if (showAnimation && !hasAnimated) setTimeout(() => setHasAnimated(true), 800); }, [showAnimation, hasAnimated]); + + // Cargar lista + const load = async () => { + setLoading(true); + setError(null); + try { + const data = await fetchDatastages(); + setDatastages(data); + } catch (e) { + setError(e.message); + } + setLoading(false); + }; + useEffect(() => { load(); }, []); + + // Ver detalle + const handleSelect = async (id) => { + setLoading(true); + setError(null); + try { + const detail = await fetchDatastageDetail(id); + setSelected(detail); + setShowDetailModal(true); + } catch (e) { + setError(e.message); + } + setLoading(false); + }; + + // Crear + const handleCreate = async (e) => { + e.preventDefault(); + setLoading(true); + setError(null); + try { + const fd = new FormData(); + fd.append('contribuyente', form.contribuyente); + if (form.archivo) fd.append('archivo', form.archivo); + await postFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/`, fd); + setForm({ archivo: null, contribuyente: '' }); + setShowCreateModal(false); + setSuccess('Datastage creado exitosamente'); + setShowSuccessModal(true); + load(); + } catch (e) { + setError(e.message); + } + setLoading(false); + }; + + // Editar + const handleEdit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(null); + try { + const fd = new FormData(); + fd.append('contribuyente', form.contribuyente); + if (form.archivo) fd.append('archivo', form.archivo); + await patchFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${editingId}/`, fd); + setForm({ archivo: null, contribuyente: '' }); + setEditingId(null); + setShowEditModal(false); + setSuccess('Datastage actualizado exitosamente'); + setShowSuccessModal(true); + load(); + } catch (e) { + setError(e.message); + } + setLoading(false); + }; + + // Eliminar + const handleDelete = async () => { + if (!deleteId) return; + setLoading(true); + setError(null); + try { + await deleteDatastage(deleteId); + if (selected && selected.id === deleteId) setSelected(null); + setShowDeleteModal(false); + setSuccess('Datastage eliminado exitosamente'); + setShowSuccessModal(true); + load(); + } catch (e) { + setError(e.message); + } + setLoading(false); + }; + + // Abrir modal de edición + const openEditModal = (item) => { + setForm({ archivo: null, contribuyente: item.contribuyente }); + setEditingId(item.id); + setShowEditModal(true); + }; + + // Abrir modal de creación + const openCreateModal = () => { + setForm({ archivo: null, contribuyente: '' }); + setEditingId(null); + setShowCreateModal(true); + }; + + // Abrir modal de eliminación + const openDeleteModal = (id) => { + setDeleteId(id); + setShowDeleteModal(true); + }; + + // Cancelar edición + const handleCancelEdit = () => { + setForm({ archivo: null, contribuyente: '' }); + setEditingId(null); + setShowEditModal(false); + }; + + return ( +
+ +
+ {/* Header decorativo */} +
+
+ + + + + +
+
+

+ Datastage + {datastages.length} +

+

Gestiona y visualiza la información de Datastage.

+
+ {/* Efecto decorativo de fondo */} +
+ + + + + + + + + +
+
+ {/* Animación personalizada para el icono y contador */} + + + {/* Botón flotante para crear */} +
+ +
+ + {/* Lista */} +
+
+

Lista de Datastages

+
+
+
+ + + + + + + + + + + + + + {datastages.map(item => ( + + + + + + + + + + ))} + +
IDArchivoContribuyenteProcesadoCreadoActualizadoAcciones
{item.id} + {item.archivo ? ( + <> + + {(() => { + try { + const url = new URL(item.archivo); + return decodeURIComponent(url.pathname.split('/').pop() || ''); + } catch { + return ''; + } + })()} + + + + ) : ( + Sin archivo + )} + {item.contribuyente} + + {item.procesado ? 'Sí' : 'No'} + + {item.created_at ? new Date(item.created_at).toLocaleString() : ''}{item.updated_at ? new Date(item.updated_at).toLocaleString() : ''} + + + + +
+
+
+ + {/* Modales */} + {/* Modal de creación */} + {showCreateModal && ( +
+
+

Nuevo Datastage

+ {error &&
{error}
} + + setForm(f => ({ ...f, archivo: e.target.files[0] }))} required /> + + setForm(f => ({ ...f, contribuyente: e.target.value }))} required /> +
+ + +
+
+
+ )} + + {/* Modal de edición */} + {showEditModal && ( +
+
+

Editar Datastage

+ {error &&
{error}
} + + setForm(f => ({ ...f, archivo: e.target.files[0] }))} /> + + setForm(f => ({ ...f, contribuyente: e.target.value }))} required /> +
+ + +
+
+
+ )} + + {/* Modal de confirmación para eliminar */} + setShowDeleteModal(false)} onConfirm={handleDelete} message="¿Seguro que deseas eliminar este datastage?" confirmText="Eliminar" cancelText="Cancelar" /> + + {/* Modal de detalle */} + {showDetailModal && selected && ( +
+
+

Detalle de Datastage #{selected.id}

+
Archivo: {selected.archivo ? Descargar : Sin archivo}
+
Contribuyente: {selected.contribuyente}
+
Procesado: {selected.procesado ? 'Sí' : 'No'}
+
Organización: {selected.organizacion}
+
Creado: {selected.created_at ? new Date(selected.created_at).toLocaleString() : ''}
+
Actualizado: {selected.updated_at ? new Date(selected.updated_at).toLocaleString() : ''}
+ +
+
+ )} + + + {/* Modal de registros cargados */} + setShowRegistrosModal(false)} registros={registrosCargados} /> + + {/* Modal de éxito */} + setShowSuccessModal(false)} message={success} /> + +
+
+ ); +} + diff --git a/src/pages/Documents.jsx b/src/pages/Documents.jsx index 44cd4ba..03eabc7 100644 --- a/src/pages/Documents.jsx +++ b/src/pages/Documents.jsx @@ -364,7 +364,6 @@ export default function Documents() { - @@ -474,12 +473,12 @@ export default function Documents() { switch (String(doc.document_type)) { case '1': return 'Pedimento Partida'; case '2': return 'Pedimento Completo'; - case '3': return 'Pedimento Remesas'; + case '3': return 'Pedimento Remesas'; case '4': return 'Pedimento Acuse'; case '5': return 'Pedimento EDocument'; case '6': return 'Estado Pedimento'; case '7': return 'Acuse Cove'; - case '8': return 'Cove'; + default: return doc.document_type || ''; } })()