Files
frontend/src/pages/Datastage.jsx
2025-09-01 16:38:47 -06:00

727 lines
52 KiB
JavaScript

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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
<div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md">
<h2 className="text-lg font-bold mb-4">Registros cargados</h2>
<div className="overflow-x-auto max-h-96">
<table className="min-w-full border border-gray-300 text-sm">
<thead>
<tr className="bg-gray-100">
<th className="px-2 py-1 border">Registro</th>
<th className="px-2 py-1 border">Cantidad</th>
</tr>
</thead>
<tbody> {(registros && typeof registros === 'object' ? Object.entries(registros) : []).map(([registro, cantidad]) => (
<tr key={registro}>
<td className="px-2 py-1 border">{registro}</td>
<td className="px-2 py-1 border text-right">{cantidad}</td>
</tr>
))}
</tbody>
</table>
</div>
<button
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={onClose}
>
Cerrar
</button>
</div>
</div>
);
}
// 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 [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 = () => {
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 (
<div className="min-h-screen p-4 sm:p-6 bg-gradient-to-br from-gray-50 to-blue-50">
<div ref={focusKeeperRef} tabIndex={-1} style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden', outline: 'none' }} aria-hidden="true"></div>
<div className="max-w-7xl mx-auto">
{/* Header decorativo */}
<div className={
"mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 border border-blue-200 p-6 sm:p-8 flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6" +
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' } : undefined}>
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<ellipse cx="12" cy="7" rx="8" ry="3" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4 7v10c0 1.657 3.582 3 8 3s8-1.343 8-3V7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4 17c0 1.657 3.582 3 8 3s8-1.343 8-3" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="flex-1">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
Datastage
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs font-semibold px-3 py-1 rounded-full animate-fade-in">{datastages.count}</span>
</h1>
<p className="text-sm sm:text-base lg:text-lg text-blue-100 font-medium">Gestiona y visualiza la información de Datastage.</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#1e40af" stopOpacity="0.15" />
<stop offset="1" stopColor="#1e3a8a" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
</div>
</div>
{/* Animación personalizada para el icono y contador */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
}
@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }
.animate-fadein-slideup { animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards; }
`}</style>
{/* Botón flotante para crear */}
<div className="flex justify-end mb-4">
<button onClick={openCreateModal} className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg font-semibold shadow hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-400 transition-all">
<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="M12 4v16m8-8H4" />
</svg>
Nuevo Datastage
</button>
</div>
{/* Lista */}
{/* Responsive: tabla en desktop, tarjetas en móvil/tablet */}
<div className="bg-white shadow-lg rounded-xl border border-gray-200">
<div className="px-4 sm:px-6 lg:px-8 pt-6 sm:pt-8 pb-2 border-b border-gray-200">
<h2 className="text-xl sm:text-2xl font-extrabold text-blue-800 tracking-tight mb-1">Lista de Datastages</h2>
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
</div>
{/* Tabla para pantallas grandes */}
<div className="hidden lg:block overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden text-xs bg-white">
<thead className="bg-slate-100">
<tr>
<th className="border px-2 py-2 whitespace-nowrap">ID</th>
<th className="border px-2 py-2 whitespace-nowrap">Archivo</th>
<th className="border px-2 py-2 whitespace-nowrap">Contribuyente</th>
<th className="border px-2 py-2 whitespace-nowrap">Procesado</th>
<th className="border px-2 py-2 whitespace-nowrap">Creado</th>
<th className="border px-2 py-2 whitespace-nowrap">Actualizado</th>
<th className="border px-2 py-2 whitespace-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
{(Array.isArray(datastages.results) ? datastages.results : []).map(item => (
<tr key={item.id} className="hover:bg-slate-50 transition-colors">
<td className="border px-2 py-2 text-center">{item.id}</td>
<td className="border px-2 py-2 max-w-xs truncate">
{item.archivo ? (
<span className="flex items-center gap-1 text-xs text-gray-700 truncate font-mono">
{(() => {
try {
const url = new URL(item.archivo);
return decodeURIComponent(url.pathname.split('/').pop() || '');
} catch {
return '';
}
})()}
<button
type="button"
className="inline-flex items-center justify-center w-6 h-6 rounded bg-blue-100 border border-blue-200 text-blue-700 hover:bg-blue-200 hover:border-blue-300 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400 ml-1"
title="Descargar archivo"
onClick={() => downloadDatastageFile(
item.id,
(() => {
try {
const url = new URL(item.archivo);
return decodeURIComponent(url.pathname.split('/').pop() || '');
} catch {
return '';
}
})()
)}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5 5-5M12 15V3" />
</svg>
</button>
</span>
) : (
<span className="text-gray-400">Sin archivo</span>
)}
</td>
<td className="border px-2 py-2">{item.contribuyente}</td>
<td className="border px-2 py-2 text-center">
<span className={item.procesado ? 'bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs' : 'bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs'}>
{item.procesado ? 'Sí' : 'No'}
</span>
</td>
<td className="border px-2 py-2 whitespace-nowrap">{item.created_at ? new Date(item.created_at).toLocaleString() : ''}</td>
<td className="border px-2 py-2 whitespace-nowrap">{item.updated_at ? new Date(item.updated_at).toLocaleString() : ''}</td>
<td className="border px-2 py-2 space-x-2 text-center">
<button onClick={() => handleSelect(item.id)}
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 hover:border-blue-300 transition-all duration-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
title="Ver detalle"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
</button>
<button onClick={() => openEditModal(item)}
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-yellow-50 border border-yellow-200 text-yellow-700 hover:bg-yellow-100 hover:border-yellow-300 transition-all duration-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
title="Editar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536M9 13l6-6 3.536 3.536a2 2 0 010 2.828l-7.072 7.072a2 2 0 01-2.828 0l-3.536-3.536a2 2 0 010-2.828l7.072-7.072z" /></svg>
</button>
<button onClick={() => openDeleteModal(item.id)}
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-red-50 border border-red-200 text-red-700 hover:bg-red-100 hover:border-red-300 transition-all duration-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-red-400"
title="Eliminar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<button
onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal)}
className={`inline-flex items-center justify-center w-9 h-9 rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 ${item.procesado ? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50' : 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'}`}
title={item.procesado ? 'Ya procesado' : 'Procesar'}
disabled={item.procesado}
>
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
{/* Paginación */}
{datastages.count > itemsPerPage && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 px-4 py-4 border-t border-gray-200">
<div className="text-xs text-gray-700">
Mostrando <span className="font-medium">{((currentPage - 1) * itemsPerPage) + 1}</span> a <span className="font-medium">{Math.min(currentPage * itemsPerPage, datastages.count)}</span> de <span className="font-medium">{datastages.count}</span> registros
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-2 py-1 rounded border text-xs bg-white hover:bg-blue-50 disabled:opacity-50"
>Anterior</button>
{Array.from({ length: Math.ceil(datastages.count / itemsPerPage) }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setCurrentPage(pageNum)}
className={`px-2 py-1 rounded text-xs border ${currentPage === pageNum ? 'bg-blue-600 text-white' : 'bg-white hover:bg-blue-50'}`}
>{pageNum}</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(Math.ceil(datastages.count / itemsPerPage), p + 1))}
disabled={currentPage === Math.ceil(datastages.count / itemsPerPage)}
className="px-2 py-1 rounded border text-xs bg-white hover:bg-blue-50 disabled:opacity-50"
>Siguiente</button>
<select
value={itemsPerPage}
onChange={e => { setItemsPerPage(Number(e.target.value)); setCurrentPage(1); }}
className="ml-2 px-2 py-1 border rounded text-xs"
>
{[10, 20, 50, 100].map(n => (<option key={n} value={n}>{n} por página</option>))}
</select>
</div>
</div>
)}
</div>
{/* Tarjetas para móvil/tablet */}
<div className="lg:hidden space-y-4 p-2">
{datastages.results.length === 0 ? (
<div className="text-center text-gray-400 py-8">No hay datastages disponibles</div>
) : (
(Array.isArray(datastages.results) ? datastages.results : []).map(item => (
<div key={item.id} className="bg-white rounded-xl shadow border border-gray-200 p-4 flex flex-col gap-2">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-blue-800">#{item.id}</span>
<span className={item.procesado ? 'bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs' : 'bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs'}>
{item.procesado ? 'Procesado' : 'Pendiente'}
</span>
</div>
<div className="flex items-center gap-1 text-xs text-gray-700 break-all font-mono mb-1">
{item.archivo ? (
<span className="flex items-center gap-1">
{(() => { try { const url = new URL(item.archivo); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })()}
<button
type="button"
className="inline-flex items-center justify-center w-6 h-6 rounded bg-blue-100 border border-blue-200 text-blue-700 hover:bg-blue-200 hover:border-blue-300 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400 ml-1"
title="Descargar archivo"
onClick={() => downloadDatastageFile(
item.id,
(() => { try { const url = new URL(item.archivo); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })()
)}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5 5-5M12 15V3" />
</svg>
</button>
</span>
) : <span className="text-gray-400">Sin archivo</span>}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="font-semibold">Contribuyente:</span> {item.contribuyente}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="font-semibold">Creado:</span> {item.created_at ? new Date(item.created_at).toLocaleString() : ''}
</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="font-semibold">Actualizado:</span> {item.updated_at ? new Date(item.updated_at).toLocaleString() : ''}
</div>
<div className="flex gap-2 mt-2">
<button onClick={() => handleSelect(item.id)}
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 hover:border-blue-300 transition-all duration-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
title="Ver detalle"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
</button>
<button onClick={() => openEditModal(item)}
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-yellow-50 border border-yellow-200 text-yellow-700 hover:bg-yellow-100 hover:border-yellow-300 transition-all duration-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-yellow-400"
title="Editar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536M9 13l6-6 3.536 3.536a2 2 0 010 2.828l-7.072 7.072a2 2 0 01-2.828 0l-3.536-3.536a2 2 0 010-2.828l7.072-7.072z" /></svg>
</button>
<button onClick={() => openDeleteModal(item.id)}
className="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-red-50 border border-red-200 text-red-700 hover:bg-red-100 hover:border-red-300 transition-all duration-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-red-400"
title="Eliminar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<button
onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal)}
className={`inline-flex items-center justify-center w-9 h-9 rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 ${item.procesado ? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50' : 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'}`}
title={item.procesado ? 'Ya procesado' : 'Procesar'}
disabled={item.procesado}
>
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</button>
</div>
</div>
))
)}
</div>
</div>
{/* Modales */}
{/* Modal de creación - estilo Users/Importers */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
<form onSubmit={handleCreate} className="relative mx-auto w-full max-w-xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
{/* Header */}
<div className="bg-gradient-to-r from-blue-700 to-blue-900 rounded-t-2xl p-4 text-white border-b-2 border-blue-500">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="bg-blue-500 bg-opacity-30 rounded-xl p-2 border border-blue-400 border-opacity-30">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold tracking-wide">Nuevo Datastage</h3>
<p className="text-blue-200 text-xs font-medium">Carga un archivo .zip y asigna un contribuyente</p>
</div>
</div>
<button type="button" onClick={() => setShowCreateModal(false)} className="text-blue-100 hover:text-white hover:bg-blue-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-blue-500 border-opacity-30">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-4 ">
{error && <div className="text-red-500 mb-2">{error}</div>}
<div>
<label className="block text-xs font-semibold text-slate-700 mb-1">Archivo (.zip)</label>
<input type="file" accept=".zip" className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} required />
</div>
<div>
<label className="block text-xs font-semibold text-slate-700 mb-1">Contribuyente</label>
<input className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
</div>
<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 type="button" onClick={() => setShowCreateModal(false)} className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200">Cancelar</button>
<button type="submit" className="w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-700 to-blue-900 hover:from-blue-800 hover:to-blue-950 focus:ring-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Crear</button>
</div>
</div>
</form>
</div>
)}
{/* Modal de edición - estilo Users/Importers */}
{showEditModal && (
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
<form onSubmit={handleEdit} className="relative mx-auto w-full max-w-xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
{/* Header */}
<div className="bg-gradient-to-r from-yellow-600 to-yellow-800 rounded-t-2xl p-4 text-white border-b-2 border-yellow-400">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="bg-yellow-500 bg-opacity-30 rounded-xl p-2 border border-yellow-400 border-opacity-30">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536M9 13l6-6 3.536 3.536a2 2 0 010 2.828l-7.072 7.072a2 2 0 01-2.828 0l-3.536-3.536a2 2 0 010-2.828l7.072-7.072z" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold tracking-wide">Editar Datastage</h3>
<p className="text-yellow-200 text-xs font-medium">Actualiza el archivo o el contribuyente</p>
</div>
</div>
<button type="button" onClick={handleCancelEdit} className="text-yellow-100 hover:text-white hover:bg-yellow-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-yellow-400 border-opacity-30">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{error && <div className="text-red-500 mb-2">{error}</div>}
<div>
<label className="block text-xs font-semibold text-slate-700 mb-1">Archivo (.zip)</label>
<input type="file" accept=".zip" className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} />
</div>
<div>
<label className="block text-xs font-semibold text-slate-700 mb-1">Contribuyente</label>
<input className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
</div>
<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 type="button" onClick={handleCancelEdit} className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200">Cancelar</button>
<button type="submit" className="w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-yellow-600 to-yellow-800 hover:from-yellow-700 hover:to-yellow-900 focus:ring-yellow-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Actualizar</button>
</div>
</div>
</form>
</div>
)}
{/* Modal de eliminación - estilo Users/Importers */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
<div className="relative mx-auto w-full max-w-md bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
{/* Header */}
<div className="bg-gradient-to-r from-red-700 to-red-900 rounded-t-2xl p-4 text-white border-b-2 border-red-500">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="bg-red-500 bg-opacity-30 rounded-xl p-2 border border-red-400 border-opacity-30">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold tracking-wide">Eliminar Datastage</h3>
<p className="text-red-200 text-xs font-medium">Esta acción no se puede deshacer.</p>
</div>
</div>
<button type="button" onClick={() => setShowDeleteModal(false)} className="text-red-100 hover:text-white hover:bg-red-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-red-500 border-opacity-30">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Content */}
<div className="p-6 text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">¿Eliminar este datastage?</h3>
<p className="text-sm text-gray-500 mb-4">¿Seguro que deseas eliminar este datastage? Esta acción no se puede deshacer.</p>
<div className="flex justify-center space-x-3 pt-4">
<button type="button" onClick={() => setShowDeleteModal(false)} className="px-6 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200">Cancelar</button>
<button type="button" onClick={handleDelete} className="px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-red-700 to-red-900 hover:from-red-800 hover:to-red-950 focus:ring-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Eliminar</button>
</div>
</div>
</div>
</div>
)}
{/* Modal de detalle */}
{showDetailModal && selected && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
<div className="bg-white rounded-xl shadow-2xl border border-blue-200 p-8 max-w-sm w-full flex flex-col animate-fade-in">
<h3 className="text-lg font-bold mb-2 text-blue-900">Detalle de Datastage #{selected.id}</h3>
<div className="mb-1"><b>Archivo:</b> {selected.archivo ? <a href={selected.archivo} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline break-all">Descargar</a> : <span className="text-gray-400">Sin archivo</span>}</div>
<div className="mb-1"><b>Contribuyente:</b> {selected.contribuyente}</div>
<div className="mb-1"><b>Procesado:</b> <span className={selected.procesado ? 'bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs' : 'bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs'}>{selected.procesado ? 'Sí' : 'No'}</span></div>
<div className="mb-1"><b>Organización:</b> {selected.organizacion}</div>
<div className="mb-1"><b>Creado:</b> {selected.created_at ? new Date(selected.created_at).toLocaleString() : ''}</div>
<div className="mb-1"><b>Actualizado:</b> {selected.updated_at ? new Date(selected.updated_at).toLocaleString() : ''}</div>
<button onClick={() => setShowDetailModal(false)} className="mt-2 bg-gray-300 px-3 py-1 rounded hover:bg-gray-400 transition">Cerrar</button>
</div>
</div>
)}
{/* Modal de registros cargados */}
<RegistrosCargadosModal open={showRegistrosModal} onClose={() => setShowRegistrosModal(false)} registros={registrosCargados} />
{/* Modal de éxito */}
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success} />
</div>
</div>
);
}