Feat: Edición y Manejo de Titulares, entre otros.
This commit is contained in:
@@ -11,11 +11,13 @@ import { useSignalR } from '../hooks/useSignalR';
|
||||
import FormularioConfiguracion from './FormularioConfiguracion';
|
||||
import TablaTitulares from './TablaTitulares';
|
||||
import AddTitularModal from './AddTitularModal';
|
||||
import EditarTitularModal from './EditarTitularModal';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [titulares, setTitulares] = useState<Titular[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
|
||||
const [titularAEditar, setTitularAEditar] = useState<Titular | null>(null);
|
||||
|
||||
// Usamos useCallback para que la función de callback no se recree en cada render,
|
||||
// evitando que el useEffect del hook se ejecute innecesariamente.
|
||||
@@ -95,6 +97,15 @@ const Dashboard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (id: number, texto: string, viñeta: string) => {
|
||||
try {
|
||||
await api.actualizarTitular(id, { texto, viñeta: viñeta || null });
|
||||
// SignalR se encargará de actualizar la UI
|
||||
} catch (err) {
|
||||
console.error("Error al guardar cambios:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
@@ -120,8 +131,25 @@ const Dashboard = () => {
|
||||
</Box>
|
||||
|
||||
<FormularioConfiguracion />
|
||||
<TablaTitulares titulares={titulares} onReorder={handleReorder} onDelete={handleDelete} />
|
||||
<AddTitularModal open={modalOpen} onClose={() => setModalOpen(false)} onAdd={handleAdd} />
|
||||
<TablaTitulares
|
||||
titulares={titulares}
|
||||
onReorder={handleReorder}
|
||||
onDelete={handleDelete}
|
||||
onEdit={(titular) => setTitularAEditar(titular)}
|
||||
/>
|
||||
|
||||
<AddTitularModal
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
|
||||
<EditarTitularModal
|
||||
open={titularAEditar !== null}
|
||||
onClose={() => setTitularAEditar(null)}
|
||||
onSave={handleSaveEdit}
|
||||
titular={titularAEditar}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
66
frontend/src/components/EditarTitularModal.tsx
Normal file
66
frontend/src/components/EditarTitularModal.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// frontend/src/components/EditarTitularModal.tsx
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button } from '@mui/material';
|
||||
import type { Titular } from '../types';
|
||||
|
||||
const style = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (id: number, texto: string, viñeta: string) => void;
|
||||
titular: Titular | null;
|
||||
}
|
||||
|
||||
const EditarTitularModal = ({ open, onClose, onSave, titular }: Props) => {
|
||||
const [texto, setTexto] = useState('');
|
||||
const [viñeta, setViñeta] = useState('');
|
||||
|
||||
// Este efecto actualiza el estado del formulario cuando se selecciona un titular para editar
|
||||
useEffect(() => {
|
||||
if (titular) {
|
||||
setTexto(titular.texto);
|
||||
setViñeta(titular.viñeta ?? '•'); // Default a '•' si es nulo
|
||||
}
|
||||
}, [titular]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (titular && texto.trim()) {
|
||||
onSave(titular.id, texto.trim(), viñeta.trim());
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={style}>
|
||||
<Typography variant="h6" component="h2">Editar Titular</Typography>
|
||||
<TextField
|
||||
fullWidth autoFocus margin="normal" label="Texto del titular"
|
||||
value={texto} onChange={(e) => setTexto(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth margin="normal" label="Viñeta (ej: •, !, o dejar vacío)"
|
||||
value={viñeta} onChange={(e) => setViñeta(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||
/>
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={onClose} sx={{ mr: 1 }}>Cancelar</Button>
|
||||
<Button variant="contained" onClick={handleSave}>Guardar Cambios</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditarTitularModal;
|
||||
@@ -9,14 +9,16 @@ import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type D
|
||||
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type { Titular } from '../types';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
// La prop `onDelete` se añade para comunicar el evento al componente padre
|
||||
interface SortableRowProps {
|
||||
titular: Titular;
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (titular: Titular) => void;
|
||||
}
|
||||
|
||||
const SortableRow = ({ titular, onDelete }: SortableRowProps) => {
|
||||
const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id });
|
||||
|
||||
const style = {
|
||||
@@ -42,7 +44,9 @@ const SortableRow = ({ titular, onDelete }: SortableRowProps) => {
|
||||
</TableCell>
|
||||
<TableCell>{titular.fuente}</TableCell>
|
||||
<TableCell>
|
||||
{/* Usamos un stopPropagation para que el clic no active el arrastre */}
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(titular); }}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(titular.id); }}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
@@ -55,9 +59,10 @@ interface TablaTitularesProps {
|
||||
titulares: Titular[];
|
||||
onReorder: (titulares: Titular[]) => void;
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (titular: Titular) => void;
|
||||
}
|
||||
|
||||
const TablaTitulares = ({ titulares, onReorder, onDelete }: TablaTitularesProps) => {
|
||||
const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit }: TablaTitularesProps) => {
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })); // Evita activar el drag con un simple clic
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
@@ -95,7 +100,7 @@ const TablaTitulares = ({ titulares, onReorder, onDelete }: TablaTitularesProps)
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{titulares.map((titular) => (
|
||||
<SortableRow key={titular.id} titular={titular} onDelete={onDelete} />
|
||||
<SortableRow key={titular.id} titular={titular} onDelete={onDelete} onEdit={onEdit} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -10,6 +10,11 @@ const apiClient = axios.create({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
export interface ActualizarTitularPayload {
|
||||
texto: string;
|
||||
viñeta: string | null;
|
||||
}
|
||||
|
||||
export const obtenerTitulares = async (): Promise<Titular[]> => {
|
||||
const response = await apiClient.get('/titulares');
|
||||
return response.data;
|
||||
@@ -43,4 +48,8 @@ export const guardarConfiguracion = async (config: Configuracion): Promise<void>
|
||||
|
||||
export const generarCsvManual = async (): Promise<void> => {
|
||||
await apiClient.post('/acciones/generar-csv');
|
||||
};
|
||||
|
||||
export const actualizarTitular = async (id: number, payload: ActualizarTitularPayload): Promise<void> => {
|
||||
await apiClient.put(`/titulares/${id}`, payload);
|
||||
};
|
||||
@@ -10,11 +10,12 @@ export interface Titular {
|
||||
fechaCreacion: string;
|
||||
tipo: 'Scraped' | 'Edited' | 'Manual' | null;
|
||||
fuente: string | null;
|
||||
viñeta: string | null;
|
||||
}
|
||||
|
||||
export interface Configuracion {
|
||||
rutaCsv: string;
|
||||
intervaloMinutos: number;
|
||||
cantidadTitularesAScrapear: number;
|
||||
limiteTotalEnDb: number;
|
||||
//limiteTotalEnDb: number;
|
||||
}
|
||||
Reference in New Issue
Block a user