Mejora 2: Implementada edición en línea para el texto de los titulares.

This commit is contained in:
2025-10-29 12:26:38 -03:00
parent 267aaab91f
commit bca56e7722
2 changed files with 74 additions and 54 deletions

View File

@@ -1,5 +1,8 @@
import { useEffect, useState, useCallback } from 'react'; 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 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';
@@ -7,26 +10,25 @@ 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 { useNotification } from '../hooks/useNotification';
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'; import ConfirmationModal from './ConfirmationModal';
import type { ActualizarTitularPayload } from '../services/apiService';
const Dashboard = () => { const Dashboard = () => {
const [titulares, setTitulares] = useState<Titular[]>([]); const [titulares, setTitulares] = useState<Titular[]>([]);
const [config, setConfig] = useState<Configuracion | null>(null); const [config, setConfig] = useState<Configuracion | null>(null);
const [addModalOpen, setAddModalOpen] = useState(false); const [addModalOpen, setAddModalOpen] = useState(false);
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false); 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 [confirmState, setConfirmState] = useState<{ open: boolean; onConfirm: (() => void) | null }>({ open: false, onConfirm: null });
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const [titularAEditar, setTitularAEditar] = useState<Titular | null>(null);
const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => { const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => {
setTitulares(titularesActualizados); setTitulares(titularesActualizados);
}, []); }, []);
@@ -36,12 +38,9 @@ const Dashboard = () => {
]); ]);
useEffect(() => { useEffect(() => {
// Obtenemos la configuración persistente
const fetchConfig = api.obtenerConfiguracion(); const fetchConfig = api.obtenerConfiguracion();
// Obtenemos el estado inicial del switch (que siempre será 'false')
const fetchEstado = api.getEstadoProceso(); const fetchEstado = api.getEstadoProceso();
// Cuando ambas promesas se resuelvan, construimos el estado inicial
Promise.all([fetchConfig, fetchEstado]) Promise.all([fetchConfig, fetchEstado])
.then(([configData, estadoData]) => { .then(([configData, estadoData]) => {
setConfig({ setConfig({
@@ -54,16 +53,39 @@ const Dashboard = () => {
api.obtenerTitulares().then(setTitulares); api.obtenerTitulares().then(setTitulares);
}, []); }, []);
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 });
}
};
setConfirmState({ open: true, onConfirm });
};
const handleSaveEdit = async (id: number, payload: ActualizarTitularPayload) => {
try {
await api.actualizarTitular(id, payload);
showNotification('Titular actualizado', 'success');
} catch (err) {
showNotification('Error al guardar los cambios', 'error');
console.error("Error al guardar cambios:", err);
}
};
const handleSwitchChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleSwitchChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (!config) return; if (!config) return;
const isChecked = event.target.checked; const isChecked = event.target.checked;
setConfig({ ...config, scrapingActivo: isChecked }); setConfig({ ...config, scrapingActivo: isChecked });
try { try {
// Llamamos al nuevo endpoint para cambiar solo el estado
await api.setEstadoProceso(isChecked); await api.setEstadoProceso(isChecked);
} catch (err) { } catch (err) {
console.error("Error al cambiar estado del proceso", err); console.error("Error al cambiar estado del proceso", err);
// Revertir en caso de error
setConfig({ ...config, scrapingActivo: !isChecked }); setConfig({ ...config, scrapingActivo: !isChecked });
} }
}; };
@@ -79,21 +101,6 @@ const Dashboard = () => {
} }
}; };
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) => { const handleAdd = async (texto: string) => {
try { try {
await api.crearTitularManual(texto); await api.crearTitularManual(texto);
@@ -129,16 +136,6 @@ 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);
}
};
return ( return (
<> <>
<Box <Box
@@ -196,6 +193,7 @@ const Dashboard = () => {
onReorder={handleReorder} onReorder={handleReorder}
onDelete={handleDelete} onDelete={handleDelete}
onEdit={(titular) => setTitularAEditar(titular)} onEdit={(titular) => setTitularAEditar(titular)}
onSave={handleSaveEdit}
/> />
<AddTitularModal <AddTitularModal
@@ -203,13 +201,6 @@ const Dashboard = () => {
onClose={() => setAddModalOpen(false)} onClose={() => setAddModalOpen(false)}
onAdd={handleAdd} onAdd={handleAdd}
/> />
<EditarTitularModal
open={titularAEditar !== null}
onClose={() => setTitularAEditar(null)}
onSave={handleSaveEdit}
titular={titularAEditar}
/>
<ConfirmationModal <ConfirmationModal
open={confirmState.open} open={confirmState.open}
onClose={() => setConfirmState({ open: false, onConfirm: null })} onClose={() => setConfirmState({ open: false, onConfirm: null })}
@@ -217,6 +208,12 @@ const Dashboard = () => {
title="Confirmar Eliminación" title="Confirmar Eliminación"
message="¿Estás seguro de que quieres eliminar este titular? Esta acción no se puede deshacer." message="¿Estás seguro de que quieres eliminar este titular? Esta acción no se puede deshacer."
/> />
<EditarTitularModal
open={titularAEditar !== null}
onClose={() => setTitularAEditar(null)}
onSave={(id, texto, viñeta) => handleSaveEdit(id, { texto, viñeta: viñeta || null })}
titular={titularAEditar}
/>
</> </>
); );
}; };

View File

@@ -1,8 +1,7 @@
// frontend/src/components/TablaTitulares.tsx // frontend/src/components/TablaTitulares.tsx
import { import { useState } from 'react';
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography, Link import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography, Link, TextField } from '@mui/material';
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import DragHandleIcon from '@mui/icons-material/DragHandle'; import DragHandleIcon from '@mui/icons-material/DragHandle';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
@@ -10,19 +9,27 @@ import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type D
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import type { Titular } from '../types'; import type { Titular } from '../types';
import type { ActualizarTitularPayload } from '../services/apiService';
interface SortableRowProps { interface SortableRowProps {
titular: Titular; titular: Titular;
onDelete: (id: number) => void; onDelete: (id: number) => void;
onSave: (id: number, payload: ActualizarTitularPayload) => void;
onEdit: (titular: Titular) => void; onEdit: (titular: Titular) => void;
} }
const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => { const SortableRow = ({ titular, onDelete, onSave, onEdit }: SortableRowProps) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id }); const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id });
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(titular.texto);
const style = { const style = { transform: CSS.Transform.toString(transform), transition };
transform: CSS.Transform.toString(transform),
transition, const handleSave = () => {
if (editText.trim() && editText.trim() !== titular.texto) {
onSave(titular.id, { texto: editText.trim(), viñeta: titular.viñeta });
}
setIsEditing(false);
}; };
const getChipColor = (tipo: Titular['tipo']): "success" | "warning" | "info" => { const getChipColor = (tipo: Titular['tipo']): "success" | "warning" | "info" => {
@@ -30,7 +37,7 @@ const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => {
if (tipo === 'Manual') return 'info'; if (tipo === 'Manual') return 'info';
return 'success'; return 'success';
}; };
const formatFuente = (fuente: string | null) => { const formatFuente = (fuente: string | null) => {
if (!fuente) return 'N/A'; if (!fuente) return 'N/A';
try { try {
@@ -46,7 +53,22 @@ const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => {
<TableCell sx={{ cursor: 'grab', verticalAlign: 'middle' }} {...listeners}> <TableCell sx={{ cursor: 'grab', verticalAlign: 'middle' }} {...listeners}>
<DragHandleIcon sx={{ color: 'text.secondary' }} /> <DragHandleIcon sx={{ color: 'text.secondary' }} />
</TableCell> </TableCell>
<TableCell sx={{ verticalAlign: 'middle' }}>{titular.texto}</TableCell> <TableCell sx={{ verticalAlign: 'middle' }} onClick={() => setIsEditing(true)}>
{isEditing ? (
<TextField
fullWidth
autoFocus
variant="standard"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }}
onClick={(e) => e.stopPropagation()}
/>
) : (
<Typography variant="body2">{titular.texto}</Typography>
)}
</TableCell>
<TableCell sx={{ verticalAlign: 'middle' }}> <TableCell sx={{ verticalAlign: 'middle' }}>
<Chip label={titular.tipo || 'Scraped'} color={getChipColor(titular.tipo)} size="small" /> <Chip label={titular.tipo || 'Scraped'} color={getChipColor(titular.tipo)} size="small" />
</TableCell> </TableCell>
@@ -76,9 +98,10 @@ interface TablaTitularesProps {
onReorder: (titulares: Titular[]) => void; onReorder: (titulares: Titular[]) => void;
onDelete: (id: number) => void; onDelete: (id: number) => void;
onEdit: (titular: Titular) => void; onEdit: (titular: Titular) => void;
onSave: (id: number, payload: ActualizarTitularPayload) => void;
} }
const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit }: TablaTitularesProps) => { const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit, onSave }: TablaTitularesProps) => {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }));
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = (event: DragEndEvent) => {
@@ -115,7 +138,7 @@ const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit }: TablaTitular
</TableHead> </TableHead>
<TableBody> <TableBody>
{titulares.map((titular) => ( {titulares.map((titular) => (
<SortableRow key={titular.id} titular={titular} onDelete={onDelete} onEdit={onEdit} /> <SortableRow key={titular.id} titular={titular} onDelete={onDelete} onEdit={onEdit} onSave={onSave} />
))} ))}
</TableBody> </TableBody>
</Table> </Table>