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 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 (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Layout>
<Dashboard />
</Layout>
<NotificationProvider>
<Layout>
<Dashboard />
</Layout>
</NotificationProvider>
</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 {
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<Titular[]>([]);
@@ -23,6 +22,11 @@ const Dashboard = () => {
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
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[]) => {
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}
/>
<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);
};