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

{(registros && typeof registros === 'object' ? Object.entries(registros) : []).map(([registro, cantidad]) => ( ))}
Registro Cantidad
{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) }); const data = await res.json(); if (res.status === 200) { // PATCH para marcar como procesado en backend await patchProcesadoTrue(item); setDatastages(prev => ({ ...prev, results: Array.isArray(prev.results) ? prev.results.map(d => d.id === item.id ? { ...d, procesado: true } : d) : [] })); // Mostrar el mensaje con task_id y detail si existen if (data && data.task_id && data.detail) { setSuccess(`Procesamiento iniciado.\nTask ID: ${data.task_id}\n${data.detail}`); } else { setSuccess('Procesado correctamente'); } // El modal de éxito se debe mostrar en el componente principal después de setSuccess // No se llama aquí if (data && data.registros_cargados) { setRegistrosCargados(data.registros_cargados); setShowRegistrosModal(true); } } else { setError(data && data.detail ? data.detail : '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); // datastages will hold the full API response object (with .results and .count) const [datastages, setDatastages] = useState({ results: [], count: 0 }); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [form, setForm] = useState({ archivo: null, contribuyente: '' }); const [importadores, setImportadores] = useState([]); 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]); // Mostrar modal de éxito cuando cambia el mensaje de success useEffect(() => { if (success) setShowSuccessModal(true); }, [success]); // Cargar lista const load = async (page = currentPage, pageSize = itemsPerPage) => { setLoading(true); setError(null); try { const data = await fetchDatastages(page, pageSize); if (data && Array.isArray(data.results)) { setDatastages(data); } else if (Array.isArray(data)) { setDatastages({ results: data, count: data.length }); } else { setDatastages({ results: [], count: 0 }); } } catch (e) { setError(e.message); setDatastages({ results: [], count: 0 }); } setLoading(false); }; useEffect(() => { load(currentPage, itemsPerPage); }, [currentPage, itemsPerPage]); // 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 = async () => { setForm({ archivo: null, contribuyente: '' }); setEditingId(null); // Fetch importadores try { const res = await fetchWithAuth(`${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, { method: 'GET' }); const data = await res.json(); if (Array.isArray(data)) { setImportadores(data); } else { setImportadores([]); } } catch { setImportadores([]); } 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.count}

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 */} {/* Responsive: tabla en desktop, tarjetas en móvil/tablet */}

Lista de Datastages

{/* Tabla para pantallas grandes */}
{(Array.isArray(datastages.results) ? datastages.results : []).map(item => ( ))}
ID Archivo Contribuyente Procesado Creado Actualizado Acciones
{item.id} {item.download_url ? ( {(() => { try { const url = new URL(item.download_url); 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() : ''}
{/* Paginación */} {datastages.count > itemsPerPage && (
Mostrando {((currentPage - 1) * itemsPerPage) + 1} a {Math.min(currentPage * itemsPerPage, datastages.count)} de {datastages.count} registros
{Array.from({ length: Math.ceil(datastages.count / itemsPerPage) }, (_, i) => i + 1).map(pageNum => ( ))}
)}
{/* Tarjetas para móvil/tablet */}
{datastages.results.length === 0 ? (
No hay datastages disponibles
) : ( (Array.isArray(datastages.results) ? datastages.results : []).map(item => (
#{item.id} {item.procesado ? 'Procesado' : 'Pendiente'}
{item.download_url ? ( {(() => { try { const url = new URL(item.download_url); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })()} ) : Sin archivo}
Contribuyente: {item.contribuyente}
Creado: {item.created_at ? new Date(item.created_at).toLocaleString() : ''}
Actualizado: {item.updated_at ? new Date(item.updated_at).toLocaleString() : ''}
)) )}
{/* Modales */} {/* Modal de creación - estilo Users/Importers */} {showCreateModal && (
{/* Header formal con gradiente */}

Nuevo Datastage

Carga un archivo .zip y asigna un contribuyente

{/* Content */}
{error && (
{error}
)} {/* Sección de Archivo */}

Archivo de Datos

Selecciona el archivo ZIP a procesar

setForm(f => ({ ...f, archivo: e.target.files[0] }))} required className="w-full px-4 py-2.5 border-2 border-slate-300 border-dashed rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 text-sm hover:border-blue-500 cursor-pointer file:mr-4 file:py-1 file:px-4 file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 file:rounded-full hover:file:bg-blue-100" />

Formato soportado: .ZIP (máximo 100MB)

{/* Sección de Contribuyente */}

Datos del Contribuyente

Selecciona el RFC del contribuyente

Selecciona el RFC del contribuyente asociado a este datastage

{/* Botones de acción */}
)} {/* Modal de edición - estilo Users/Importers */} {showEditModal && (
{/* Header */}

Editar Datastage

Actualiza el archivo o el contribuyente

{/* Content */}
{error &&
{error}
}
setForm(f => ({ ...f, archivo: e.target.files[0] }))} />
setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
)} {/* Modal de eliminación - estilo Users/Importers */} {showDeleteModal && (
{/* Header */}

Eliminar Datastage

Esta acción no se puede deshacer.

{/* Content */}

¿Eliminar este datastage?

¿Seguro que deseas eliminar este datastage? Esta acción no se puede deshacer.

)} {/* Modal de detalle */} {showDetailModal && selected && (

Detalle de Datastage #{selected.id}

{/*
Archivo: {selected.download_url ? 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} />
); }