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