From 267aaab91f798016e1c5b9e35cac9df1399fea3a Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 29 Oct 2025 11:47:47 -0300 Subject: [PATCH] =?UTF-8?q?Mejora=201:=20Implementado=20sistema=20de=20not?= =?UTF-8?q?ificaciones=20global=20con=20Snackbar=20y=20modal=20de=20confir?= =?UTF-8?q?maci=C3=B3n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 11 ++-- frontend/src/components/ConfirmationModal.tsx | 46 +++++++++++++++++ frontend/src/components/Dashboard.tsx | 38 +++++++++++--- frontend/src/contexts/NotificationContext.tsx | 51 +++++++++++++++++++ frontend/src/hooks/useNotification.ts | 8 +++ 5 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/ConfirmationModal.tsx create mode 100644 frontend/src/contexts/NotificationContext.tsx create mode 100644 frontend/src/hooks/useNotification.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c029535..8d4c73a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box } from '@mui/material'; import Dashboard from './components/Dashboard'; +import { NotificationProvider } from './contexts/NotificationContext'; // Paleta de colores ajustada para coincidir con la nueva imagen (inspirada en Tailwind) const darkTheme = createTheme({ @@ -66,7 +67,7 @@ const darkTheme = createTheme({ color: '#f59e0b', }, // Chip 'Manual' - colorInfo: { + colorInfo: { backgroundColor: 'rgba(59, 130, 246, 0.2)', // blue-500 con opacidad color: '#60a5fa', }, @@ -99,9 +100,11 @@ function App() { return ( - - - + + + + + ); } diff --git a/frontend/src/components/ConfirmationModal.tsx b/frontend/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..45b2d09 --- /dev/null +++ b/frontend/src/components/ConfirmationModal.tsx @@ -0,0 +1,46 @@ +// frontend/src/components/ConfirmationModal.tsx + +import { Modal, Box, Typography, Button, Stack } from '@mui/material'; + +const style = { + position: 'absolute' as 'absolute', + top: '40%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + boxShadow: 24, + p: 4, + borderRadius: 1, +}; + +interface Props { + open: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; +} + +const ConfirmationModal = ({ open, onClose, onConfirm, title, message }: Props) => { + return ( + + + + {title} + + + {message} + + + + + + + + ); +}; + +export default ConfirmationModal; \ No newline at end of file diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 0c1270c..601590d 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,8 +1,5 @@ import { useEffect, useState, useCallback } from 'react'; -import { - Box, Button, Stack, Chip, CircularProgress, - Accordion, AccordionSummary, AccordionDetails, Typography -} 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'; @@ -10,11 +7,13 @@ 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'; // <-- Importar hook de notificación import FormularioConfiguracion from './FormularioConfiguracion'; import TablaTitulares from './TablaTitulares'; import AddTitularModal from './AddTitularModal'; import EditarTitularModal from './EditarTitularModal'; import { PowerSwitch } from './PowerSwitch'; +import ConfirmationModal from './ConfirmationModal'; const Dashboard = () => { const [titulares, setTitulares] = useState([]); @@ -23,6 +22,11 @@ const Dashboard = () => { const [isGeneratingCsv, setIsGeneratingCsv] = useState(false); const [titularAEditar, setTitularAEditar] = useState(null); + // Estado para el modal de confirmación + const [confirmState, setConfirmState] = useState<{ open: boolean; onConfirm: (() => void) | null }>({ open: false, onConfirm: null }); + + const { showNotification } = useNotification(); + const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => { setTitulares(titularesActualizados); }, []); @@ -75,20 +79,27 @@ const Dashboard = () => { } }; - const handleDelete = async (id: number) => { - if (window.confirm('¿Estás seguro de que quieres eliminar este titular?')) { + 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 }); // Cierra el modal } - } + }; + setConfirmState({ open: true, onConfirm }); }; 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); } }; @@ -109,8 +120,10 @@ const Dashboard = () => { setIsGeneratingCsv(true); try { await api.generarCsvManual(); + showNotification('CSV generado manualmente', 'success'); } catch (error) { - console.error("Error al generar CSV manually", error); + showNotification('Error al generar el CSV', 'error'); + console.error("Error al generar CSV manualmente", error); } finally { setIsGeneratingCsv(false); } @@ -119,7 +132,9 @@ const Dashboard = () => { const handleSaveEdit = async (id: number, texto: string, viñeta: string) => { try { await api.actualizarTitular(id, { texto, viñeta: viñeta || null }); + showNotification('Titular actualizado', 'success'); } catch (err) { + showNotification('Error al guardar los cambios', 'error'); console.error("Error al guardar cambios:", err); } }; @@ -195,6 +210,13 @@ const Dashboard = () => { onSave={handleSaveEdit} titular={titularAEditar} /> + 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." + /> ); }; diff --git a/frontend/src/contexts/NotificationContext.tsx b/frontend/src/contexts/NotificationContext.tsx new file mode 100644 index 0000000..560621e --- /dev/null +++ b/frontend/src/contexts/NotificationContext.tsx @@ -0,0 +1,51 @@ +// frontend/src/contexts/NotificationContext.tsx + +import { createContext, useState, useCallback, type ReactNode } from 'react'; +import { Snackbar, Alert, type AlertColor } from '@mui/material'; + +// Definimos la forma de la función que nuestro contexto expondrá +interface NotificationContextType { + showNotification: (message: string, severity?: AlertColor) => void; +} + +// Creamos el contexto con un valor por defecto (una función vacía para evitar errores) +export const NotificationContext = createContext({ + showNotification: () => { }, +}); + +// Este es el componente Proveedor que envolverá nuestra aplicación +export const NotificationProvider = ({ children }: { children: ReactNode }) => { + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(''); + const [severity, setSeverity] = useState('info'); + + const handleClose = (_event?: React.SyntheticEvent | Event, reason?: string) => { + if (reason === 'clickaway') { + return; + } + setOpen(false); + }; + + // Usamos useCallback para que la referencia a esta función no cambie en cada render + const showNotification = useCallback((msg: string, sev: AlertColor = 'info') => { + setMessage(msg); + setSeverity(sev); + setOpen(true); + }, []); + + return ( + + {children} + + + {message} + + + + ); +}; \ No newline at end of file diff --git a/frontend/src/hooks/useNotification.ts b/frontend/src/hooks/useNotification.ts new file mode 100644 index 0000000..8295edb --- /dev/null +++ b/frontend/src/hooks/useNotification.ts @@ -0,0 +1,8 @@ +// frontend/src/hooks/useNotification.ts + +import { useContext } from 'react'; +import { NotificationContext } from '../contexts/NotificationContext'; + +export const useNotification = () => { + return useContext(NotificationContext); +}; \ No newline at end of file