Versión 1.0: Aplicación funcionalmente completa con todas las características principales implementadas.
This commit is contained in:
		| @@ -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} | ||||
|       /> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user