Versión 1.0: Aplicación funcionalmente completa con todas las características principales implementadas.

This commit is contained in:
2025-10-29 11:36:20 -03:00
parent 5b3dede4d5
commit 3fbb254ac3
19 changed files with 587 additions and 250 deletions

View File

@@ -1,53 +1,76 @@
// frontend/src/components/Dashboard.tsx
import { useEffect, useState, useCallback } from 'react';
import { Box, Button, Typography, Stack, Chip, CircularProgress } from '@mui/material';
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 } from '../types';
import type { Titular, Configuracion } from '../types';
import * as api from '../services/apiService';
import { useSignalR } from '../hooks/useSignalR';
import FormularioConfiguracion from './FormularioConfiguracion';
import TablaTitulares from './TablaTitulares';
import AddTitularModal from './AddTitularModal';
import EditarTitularModal from './EditarTitularModal';
import { PowerSwitch } from './PowerSwitch';
const Dashboard = () => {
const [titulares, setTitulares] = useState<Titular[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [config, setConfig] = useState<Configuracion | null>(null);
const [addModalOpen, setAddModalOpen] = useState(false);
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
const [titularAEditar, setTitularAEditar] = useState<Titular | null>(null);
// Usamos useCallback para que la función de callback no se recree en cada render,
// evitando que el useEffect del hook se ejecute innecesariamente.
const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => {
console.log("Datos recibidos desde SignalR:", titularesActualizados);
setTitulares(titularesActualizados);
}, []); // El array vacío significa que esta función nunca cambiará
}, []);
// Usamos nuestro hook y le pasamos el evento que nos interesa escuchar
const { connectionStatus } = useSignalR([
{ eventName: 'TitularesActualizados', callback: onTitularesActualizados }
]);
// La carga inicial de datos sigue siendo necesaria por si el componente se monta
// antes de que llegue la primera notificación de SignalR.
useEffect(() => {
api.obtenerTitulares()
.then(setTitulares)
.catch(error => console.error("Error al cargar titulares:", error));
// Obtenemos la configuración persistente
const fetchConfig = api.obtenerConfiguracion();
// Obtenemos el estado inicial del switch (que siempre será 'false')
const fetchEstado = api.getEstadoProceso();
// Cuando ambas promesas se resuelvan, construimos el estado inicial
Promise.all([fetchConfig, fetchEstado])
.then(([configData, estadoData]) => {
setConfig({
...configData,
scrapingActivo: estadoData.activo
});
})
.catch(error => console.error("Error al cargar datos iniciales:", error));
api.obtenerTitulares().then(setTitulares);
}, []);
const handleSwitchChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (!config) return;
const isChecked = event.target.checked;
setConfig({ ...config, scrapingActivo: isChecked });
try {
// Llamamos al nuevo endpoint para cambiar solo el estado
await api.setEstadoProceso(isChecked);
} catch (err) {
console.error("Error al cambiar estado del proceso", err);
// Revertir en caso de error
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);
// Ya no necesitamos hacer nada más, SignalR notificará a todos los clientes.
} catch (err) {
console.error("Error al reordenar:", err);
// En caso de error, volvemos a pedir los datos para no tener un estado inconsistente.
api.obtenerTitulares().then(setTitulares);
}
};
@@ -56,7 +79,6 @@ const Dashboard = () => {
if (window.confirm('¿Estás seguro de que quieres eliminar este titular?')) {
try {
await api.eliminarTitular(id);
// SignalR se encargará de actualizar el estado.
} catch (err) {
console.error("Error al eliminar:", err);
}
@@ -66,7 +88,6 @@ const Dashboard = () => {
const handleAdd = async (texto: string) => {
try {
await api.crearTitularManual(texto);
// SignalR se encargará de actualizar el estado.
} catch (err) {
console.error("Error al añadir titular:", err);
}
@@ -88,10 +109,8 @@ const Dashboard = () => {
setIsGeneratingCsv(true);
try {
await api.generarCsvManual();
// Opcional: mostrar una notificación de éxito
} catch (error) {
console.error("Error al generar CSV manualmente", error);
// Opcional: mostrar una notificación de error
console.error("Error al generar CSV manually", error);
} finally {
setIsGeneratingCsv(false);
}
@@ -100,7 +119,6 @@ const Dashboard = () => {
const handleSaveEdit = async (id: number, texto: string, viñeta: string) => {
try {
await api.actualizarTitular(id, { texto, viñeta: viñeta || null });
// SignalR se encargará de actualizar la UI
} catch (err) {
console.error("Error al guardar cambios:", err);
}
@@ -108,29 +126,56 @@ const Dashboard = () => {
return (
<>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<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="h4" component="h1">
Titulares Dashboard
<Typography variant="h5" component="h2" sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
Estado del Servidor {getStatusChip()}
</Typography>
{getStatusChip()}
{config ? (
<PowerSwitch
checked={config.scrapingActivo}
onChange={handleSwitchChange}
label={config.scrapingActivo ? "Proceso ON" : "Proceso OFF"}
/>
) : <CircularProgress size={24} />}
</Stack>
<Stack direction="row" spacing={2}>
<Stack direction="row" spacing={2} sx={{ width: { xs: '100%', sm: 'auto' } }}>
<Button
variant="outlined"
startIcon={isGeneratingCsv ? <CircularProgress size={20} /> : <SyncIcon />}
onClick={handleGenerateCsv}
disabled={isGeneratingCsv}
variant="contained" color="success"
startIcon={isGeneratingCsv ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />}
onClick={handleGenerateCsv} disabled={isGeneratingCsv}
>
{isGeneratingCsv ? 'Generando...' : 'Generate CSV'}
Regenerar CSV
</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setModalOpen(true)}>
Add Manual
<Button
variant="contained" color="primary"
startIcon={<AddIcon />}
onClick={() => setAddModalOpen(true)}
>
Titular Manual
</Button>
</Stack>
</Box>
<FormularioConfiguracion />
<Accordion defaultExpanded={false} sx={{ mb: 3 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6">Configuración</Typography>
</AccordionSummary>
<AccordionDetails>
<FormularioConfiguracion config={config} setConfig={setConfig} />
</AccordionDetails>
</Accordion>
<TablaTitulares
titulares={titulares}
onReorder={handleReorder}
@@ -139,8 +184,8 @@ const Dashboard = () => {
/>
<AddTitularModal
open={modalOpen}
onClose={() => setModalOpen(false)}
open={addModalOpen}
onClose={() => setAddModalOpen(false)}
onAdd={handleAdd}
/>