235 lines
8.2 KiB
TypeScript
235 lines
8.2 KiB
TypeScript
// frontend/src/components/Dashboard.tsx
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
import { Box, Button, Stack, Chip, CircularProgress, Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/material';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import SyncIcon from '@mui/icons-material/Sync';
|
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
|
|
import type { Titular, Configuracion } from '../types';
|
|
import * as api from '../services/apiService';
|
|
import { useSignalR } from '../hooks/useSignalR';
|
|
import { useNotification } from '../hooks/useNotification';
|
|
import FormularioConfiguracion from './FormularioConfiguracion';
|
|
import TablaTitulares from './TablaTitulares';
|
|
import AddTitularModal from './AddTitularModal';
|
|
import EditarTitularModal from './EditarTitularModal';
|
|
import { PowerSwitch } from './PowerSwitch';
|
|
import ConfirmationModal from './ConfirmationModal';
|
|
import type { ActualizarTitularPayload } from '../services/apiService';
|
|
import { TableSkeleton } from './TableSkeleton';
|
|
|
|
const Dashboard = () => {
|
|
const [titulares, setTitulares] = useState<Titular[]>([]);
|
|
const [config, setConfig] = useState<Configuracion | null>(null);
|
|
const [addModalOpen, setAddModalOpen] = useState(false);
|
|
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
|
|
const [confirmState, setConfirmState] = useState<{ open: boolean; onConfirm: (() => void) | null }>({ open: false, onConfirm: null });
|
|
const { showNotification } = useNotification();
|
|
const [titularAEditar, setTitularAEditar] = useState<Titular | null>(null);
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => {
|
|
setTitulares(titularesActualizados);
|
|
}, []);
|
|
|
|
const { connectionStatus } = useSignalR([
|
|
{ eventName: 'TitularesActualizados', callback: onTitularesActualizados }
|
|
]);
|
|
|
|
useEffect(() => {
|
|
// 1. Cargamos la configuración persistente
|
|
const fetchConfig = api.obtenerConfiguracion();
|
|
|
|
// 2. Preguntamos al servidor por el estado ACTUAL del proceso
|
|
const fetchEstado = api.getEstadoProceso();
|
|
|
|
Promise.all([fetchConfig, fetchEstado])
|
|
.then(([configData, estadoData]) => {
|
|
// Construimos el estado de la UI para que REFLEJE el estado real del servidor
|
|
setConfig({
|
|
...configData,
|
|
scrapingActivo: estadoData.activo
|
|
});
|
|
})
|
|
.catch(error => console.error("Error al cargar datos iniciales:", error));
|
|
|
|
// La carga de titulares sigue igual
|
|
api.obtenerTitulares()
|
|
.then(setTitulares)
|
|
.catch(error => console.error("Error al cargar titulares:", error))
|
|
.finally(() => setIsLoading(false));
|
|
|
|
}, []); // El array vacío asegura que esto solo se ejecute una vez
|
|
|
|
const handleDelete = (id: number) => {
|
|
const onConfirm = async () => {
|
|
try {
|
|
await api.eliminarTitular(id);
|
|
showNotification('Titular eliminado correctamente', 'success');
|
|
} catch (err) {
|
|
showNotification('Error al eliminar el titular', 'error');
|
|
console.error("Error al eliminar:", err);
|
|
} finally {
|
|
setConfirmState({ open: false, onConfirm: null });
|
|
}
|
|
};
|
|
setConfirmState({ open: true, onConfirm });
|
|
};
|
|
|
|
const handleSaveEdit = async (id: number, payload: ActualizarTitularPayload) => {
|
|
try {
|
|
await api.actualizarTitular(id, payload);
|
|
showNotification('Titular actualizado', 'success');
|
|
} catch (err) {
|
|
showNotification('Error al guardar los cambios', 'error');
|
|
console.error("Error al guardar cambios:", err);
|
|
}
|
|
};
|
|
|
|
const handleSwitchChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (!config) return;
|
|
const isChecked = event.target.checked;
|
|
setConfig({ ...config, scrapingActivo: isChecked });
|
|
try {
|
|
await api.setEstadoProceso(isChecked);
|
|
} catch (err) {
|
|
console.error("Error al cambiar estado del proceso", err);
|
|
setConfig({ ...config, scrapingActivo: !isChecked });
|
|
}
|
|
};
|
|
|
|
const handleReorder = async (titularesReordenados: Titular[]) => {
|
|
setTitulares(titularesReordenados);
|
|
const payload = titularesReordenados.map((item, index) => ({ id: item.id, nuevoOrden: index }));
|
|
try {
|
|
await api.actualizarOrdenTitulares(payload);
|
|
} catch (err) {
|
|
console.error("Error al reordenar:", err);
|
|
api.obtenerTitulares().then(setTitulares);
|
|
}
|
|
};
|
|
|
|
const handleAdd = async (texto: string) => {
|
|
try {
|
|
await api.crearTitularManual(texto);
|
|
showNotification('Titular manual añadido', 'success');
|
|
} catch (err) {
|
|
showNotification('Error al añadir el titular', 'error');
|
|
console.error("Error al añadir titular:", err);
|
|
}
|
|
};
|
|
|
|
const getStatusChip = () => {
|
|
switch (connectionStatus) {
|
|
case 'Connected':
|
|
return <Chip label="Conectado" color="success" size="small" />;
|
|
case 'Reconnecting':
|
|
case 'Connecting':
|
|
return <Chip label="Conectando..." color="warning" size="small" />;
|
|
default:
|
|
return <Chip label="Desconectado" color="error" size="small" />;
|
|
}
|
|
}
|
|
|
|
const handleGenerateCsv = async () => {
|
|
setIsGeneratingCsv(true);
|
|
try {
|
|
await api.generarCsvManual();
|
|
showNotification('CSV generado manualmente', 'success');
|
|
} catch (error) {
|
|
showNotification('Error al generar el CSV', 'error');
|
|
console.error("Error al generar CSV manualmente", error);
|
|
} finally {
|
|
setIsGeneratingCsv(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: { xs: 'column', sm: 'row' },
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
gap: 2,
|
|
mb: 3,
|
|
}}
|
|
>
|
|
<Stack direction="row" spacing={2} alignItems="center">
|
|
<Typography variant="h5" component="h2" sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
Estado del Servidor {getStatusChip()}
|
|
</Typography>
|
|
{config ? (
|
|
<PowerSwitch
|
|
checked={config.scrapingActivo}
|
|
onChange={handleSwitchChange}
|
|
label={config.scrapingActivo ? "Proceso ON" : "Proceso OFF"}
|
|
/>
|
|
) : <CircularProgress size={24} />}
|
|
</Stack>
|
|
|
|
<Stack direction="row" spacing={2} sx={{ width: { xs: '100%', sm: 'auto' } }}>
|
|
<Button
|
|
variant="contained" color="success"
|
|
startIcon={isGeneratingCsv ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />}
|
|
onClick={handleGenerateCsv} disabled={isGeneratingCsv}
|
|
>
|
|
Regenerar CSV
|
|
</Button>
|
|
<Button
|
|
variant="contained" color="primary"
|
|
startIcon={<AddIcon />}
|
|
onClick={() => setAddModalOpen(true)}
|
|
>
|
|
Titular Manual
|
|
</Button>
|
|
</Stack>
|
|
</Box>
|
|
|
|
<Accordion defaultExpanded={false} sx={{ mb: 3 }}>
|
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
<Typography variant="h6">Configuración</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<FormularioConfiguracion config={config} setConfig={setConfig} />
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
|
|
{isLoading ? (
|
|
<TableSkeleton />
|
|
) : (
|
|
<TablaTitulares
|
|
titulares={titulares}
|
|
onReorder={handleReorder}
|
|
onDelete={handleDelete}
|
|
onEdit={(titular) => setTitularAEditar(titular)}
|
|
onSave={handleSaveEdit}
|
|
/>
|
|
)}
|
|
|
|
<AddTitularModal
|
|
open={addModalOpen}
|
|
onClose={() => setAddModalOpen(false)}
|
|
onAdd={handleAdd}
|
|
/>
|
|
<ConfirmationModal
|
|
open={confirmState.open}
|
|
onClose={() => setConfirmState({ open: false, onConfirm: null })}
|
|
onConfirm={() => confirmState.onConfirm?.()}
|
|
title="Confirmar Eliminación"
|
|
message="¿Estás seguro de que quieres eliminar este titular? Esta acción no se puede deshacer."
|
|
/>
|
|
<EditarTitularModal
|
|
open={titularAEditar !== null}
|
|
onClose={() => setTitularAEditar(null)}
|
|
onSave={(id, texto, viñeta) => handleSaveEdit(id, { texto, viñeta: viñeta || null })}
|
|
titular={titularAEditar}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Dashboard; |