Mejora 1: Implementado sistema de notificaciones global con Snackbar y modal de confirmación.
This commit is contained in:
@@ -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({
|
||||||
@@ -66,7 +67,7 @@ const darkTheme = createTheme({
|
|||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
},
|
},
|
||||||
// Chip 'Manual'
|
// Chip 'Manual'
|
||||||
colorInfo: {
|
colorInfo: {
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.2)', // blue-500 con opacidad
|
backgroundColor: 'rgba(59, 130, 246, 0.2)', // blue-500 con opacidad
|
||||||
color: '#60a5fa',
|
color: '#60a5fa',
|
||||||
},
|
},
|
||||||
@@ -99,9 +100,11 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Layout>
|
<NotificationProvider>
|
||||||
<Dashboard />
|
<Layout>
|
||||||
</Layout>
|
<Dashboard />
|
||||||
|
</Layout>
|
||||||
|
</NotificationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
46
frontend/src/components/ConfirmationModal.tsx
Normal file
46
frontend/src/components/ConfirmationModal.tsx
Normal 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;
|
||||||
@@ -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."
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
51
frontend/src/contexts/NotificationContext.tsx
Normal file
51
frontend/src/contexts/NotificationContext.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
frontend/src/hooks/useNotification.ts
Normal file
8
frontend/src/hooks/useNotification.ts
Normal 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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user