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 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>
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
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."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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