Mejora 1: Implementado sistema de notificaciones global con Snackbar y modal de confirmación.

This commit is contained in:
2025-10-29 11:47:47 -03:00
parent 3fbb254ac3
commit 267aaab91f
5 changed files with 142 additions and 12 deletions

View File

@@ -2,6 +2,7 @@
import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box } from '@mui/material'; import { ThemeProvider, createTheme, CssBaseline, AppBar, Toolbar, Typography, Container, Box } from '@mui/material';
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';
import { NotificationProvider } from './contexts/NotificationContext';
// Paleta de colores ajustada para coincidir con la nueva imagen (inspirada en Tailwind) // Paleta de colores ajustada para coincidir con la nueva imagen (inspirada en Tailwind)
const darkTheme = createTheme({ const darkTheme = createTheme({
@@ -99,9 +100,11 @@ function App() {
return ( return (
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
<CssBaseline /> <CssBaseline />
<NotificationProvider>
<Layout> <Layout>
<Dashboard /> <Dashboard />
</Layout> </Layout>
</NotificationProvider>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -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 (
<Modal open={open} onClose={onClose}>
<Box sx={style}>
<Typography variant="h6" component="h2" gutterBottom>
{title}
</Typography>
<Typography sx={{ mb: 3, color: 'text.secondary' }}>
{message}
</Typography>
<Stack direction="row" spacing={2} justifyContent="flex-end">
<Button onClick={onClose}>Cancelar</Button>
<Button variant="contained" color="error" onClick={onConfirm}>
Confirmar
</Button>
</Stack>
</Box>
</Modal>
);
};
export default ConfirmationModal;

View File

@@ -1,8 +1,5 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { import { Box, Button, Stack, Chip, CircularProgress, Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/material';
Box, Button, Stack, Chip, CircularProgress,
Accordion, AccordionSummary, AccordionDetails, Typography
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import SyncIcon from '@mui/icons-material/Sync'; import SyncIcon from '@mui/icons-material/Sync';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 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 type { Titular, Configuracion } from '../types';
import * as api from '../services/apiService'; import * as api from '../services/apiService';
import { useSignalR } from '../hooks/useSignalR'; import { useSignalR } from '../hooks/useSignalR';
import { useNotification } from '../hooks/useNotification'; // <-- Importar hook de notificación
import FormularioConfiguracion from './FormularioConfiguracion'; import FormularioConfiguracion from './FormularioConfiguracion';
import TablaTitulares from './TablaTitulares'; import TablaTitulares from './TablaTitulares';
import AddTitularModal from './AddTitularModal'; import AddTitularModal from './AddTitularModal';
import EditarTitularModal from './EditarTitularModal'; import EditarTitularModal from './EditarTitularModal';
import { PowerSwitch } from './PowerSwitch'; import { PowerSwitch } from './PowerSwitch';
import ConfirmationModal from './ConfirmationModal';
const Dashboard = () => { const Dashboard = () => {
const [titulares, setTitulares] = useState<Titular[]>([]); const [titulares, setTitulares] = useState<Titular[]>([]);
@@ -23,6 +22,11 @@ const Dashboard = () => {
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false); const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
const [titularAEditar, setTitularAEditar] = useState<Titular | null>(null); const [titularAEditar, setTitularAEditar] = useState<Titular | null>(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[]) => { const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => {
setTitulares(titularesActualizados); setTitulares(titularesActualizados);
}, []); }, []);
@@ -75,20 +79,27 @@ const Dashboard = () => {
} }
}; };
const handleDelete = async (id: number) => { const handleDelete = (id: number) => {
if (window.confirm('¿Estás seguro de que quieres eliminar este titular?')) { const onConfirm = async () => {
try { try {
await api.eliminarTitular(id); await api.eliminarTitular(id);
showNotification('Titular eliminado correctamente', 'success');
} catch (err) { } catch (err) {
showNotification('Error al eliminar el titular', 'error');
console.error("Error al eliminar:", err); console.error("Error al eliminar:", err);
} finally {
setConfirmState({ open: false, onConfirm: null }); // Cierra el modal
} }
} };
setConfirmState({ open: true, onConfirm });
}; };
const handleAdd = async (texto: string) => { const handleAdd = async (texto: string) => {
try { try {
await api.crearTitularManual(texto); await api.crearTitularManual(texto);
showNotification('Titular manual añadido', 'success');
} catch (err) { } catch (err) {
showNotification('Error al añadir el titular', 'error');
console.error("Error al añadir titular:", err); console.error("Error al añadir titular:", err);
} }
}; };
@@ -109,8 +120,10 @@ const Dashboard = () => {
setIsGeneratingCsv(true); setIsGeneratingCsv(true);
try { try {
await api.generarCsvManual(); await api.generarCsvManual();
showNotification('CSV generado manualmente', 'success');
} catch (error) { } 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 { } finally {
setIsGeneratingCsv(false); setIsGeneratingCsv(false);
} }
@@ -119,7 +132,9 @@ const Dashboard = () => {
const handleSaveEdit = async (id: number, texto: string, viñeta: string) => { const handleSaveEdit = async (id: number, texto: string, viñeta: string) => {
try { try {
await api.actualizarTitular(id, { texto, viñeta: viñeta || null }); await api.actualizarTitular(id, { texto, viñeta: viñeta || null });
showNotification('Titular actualizado', 'success');
} catch (err) { } catch (err) {
showNotification('Error al guardar los cambios', 'error');
console.error("Error al guardar cambios:", err); console.error("Error al guardar cambios:", err);
} }
}; };
@@ -195,6 +210,13 @@ const Dashboard = () => {
onSave={handleSaveEdit} onSave={handleSaveEdit}
titular={titularAEditar} titular={titularAEditar}
/> />
<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."
/>
</> </>
); );
}; };

View File

@@ -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<NotificationContextType>({
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<AlertColor>('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 (
<NotificationContext.Provider value={{ showNotification }}>
{children}
<Snackbar
open={open}
autoHideDuration={6000} // La notificación desaparece después de 6 segundos
onClose={handleClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} // Posición
>
<Alert onClose={handleClose} severity={severity} sx={{ width: '100%' }}>
{message}
</Alert>
</Snackbar>
</NotificationContext.Provider>
);
};

View File

@@ -0,0 +1,8 @@
// frontend/src/hooks/useNotification.ts
import { useContext } from 'react';
import { NotificationContext } from '../contexts/NotificationContext';
export const useNotification = () => {
return useContext(NotificationContext);
};