From bca56e7722b502e7023d5f7a7f6f8098a4c40ed0 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 29 Oct 2025 12:26:38 -0300 Subject: [PATCH] =?UTF-8?q?Mejora=202:=20Implementada=20edici=C3=B3n=20en?= =?UTF-8?q?=20l=C3=ADnea=20para=20el=20texto=20de=20los=20titulares.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Dashboard.tsx | 83 +++++++++++----------- frontend/src/components/TablaTitulares.tsx | 45 +++++++++--- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 601590d..17b87f4 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,5 +1,8 @@ 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'; @@ -7,26 +10,25 @@ 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 { useNotification } from '../hooks/useNotification'; import FormularioConfiguracion from './FormularioConfiguracion'; import TablaTitulares from './TablaTitulares'; import AddTitularModal from './AddTitularModal'; import EditarTitularModal from './EditarTitularModal'; import { PowerSwitch } from './PowerSwitch'; import ConfirmationModal from './ConfirmationModal'; +import type { ActualizarTitularPayload } from '../services/apiService'; const Dashboard = () => { const [titulares, setTitulares] = useState([]); const [config, setConfig] = useState(null); const [addModalOpen, setAddModalOpen] = useState(false); 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 [titularAEditar, setTitularAEditar] = useState(null); + const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => { setTitulares(titularesActualizados); }, []); @@ -36,12 +38,9 @@ const Dashboard = () => { ]); useEffect(() => { - // Obtenemos la configuración persistente const fetchConfig = api.obtenerConfiguracion(); - // Obtenemos el estado inicial del switch (que siempre será 'false') const fetchEstado = api.getEstadoProceso(); - // Cuando ambas promesas se resuelvan, construimos el estado inicial Promise.all([fetchConfig, fetchEstado]) .then(([configData, estadoData]) => { setConfig({ @@ -54,16 +53,39 @@ const Dashboard = () => { 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) => { if (!config) return; const isChecked = event.target.checked; setConfig({ ...config, scrapingActivo: isChecked }); try { - // Llamamos al nuevo endpoint para cambiar solo el estado await api.setEstadoProceso(isChecked); } catch (err) { console.error("Error al cambiar estado del proceso", err); - // Revertir en caso de error 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) => { try { 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 ( <> { onReorder={handleReorder} onDelete={handleDelete} onEdit={(titular) => setTitularAEditar(titular)} + onSave={handleSaveEdit} /> { onClose={() => setAddModalOpen(false)} onAdd={handleAdd} /> - - setTitularAEditar(null)} - onSave={handleSaveEdit} - titular={titularAEditar} - /> setConfirmState({ open: false, onConfirm: null })} @@ -217,6 +208,12 @@ const Dashboard = () => { title="Confirmar Eliminación" message="¿Estás seguro de que quieres eliminar este titular? Esta acción no se puede deshacer." /> + setTitularAEditar(null)} + onSave={(id, texto, viñeta) => handleSaveEdit(id, { texto, viñeta: viñeta || null })} + titular={titularAEditar} + /> ); }; diff --git a/frontend/src/components/TablaTitulares.tsx b/frontend/src/components/TablaTitulares.tsx index f903278..42e84fb 100644 --- a/frontend/src/components/TablaTitulares.tsx +++ b/frontend/src/components/TablaTitulares.tsx @@ -1,8 +1,7 @@ // frontend/src/components/TablaTitulares.tsx -import { - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography, Link -} from '@mui/material'; +import { useState } from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography, Link, TextField } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import DragHandleIcon from '@mui/icons-material/DragHandle'; 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 { CSS } from '@dnd-kit/utilities'; import type { Titular } from '../types'; +import type { ActualizarTitularPayload } from '../services/apiService'; interface SortableRowProps { titular: Titular; onDelete: (id: number) => void; + onSave: (id: number, payload: ActualizarTitularPayload) => 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 [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = useState(titular.texto); - const style = { - transform: CSS.Transform.toString(transform), - transition, + const style = { 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" => { @@ -30,7 +37,7 @@ const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => { if (tipo === 'Manual') return 'info'; return 'success'; }; - + const formatFuente = (fuente: string | null) => { if (!fuente) return 'N/A'; try { @@ -46,7 +53,22 @@ const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => { - {titular.texto} + setIsEditing(true)}> + {isEditing ? ( + setEditText(e.target.value)} + onBlur={handleSave} + onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }} + onClick={(e) => e.stopPropagation()} + /> + ) : ( + {titular.texto} + )} + @@ -76,9 +98,10 @@ interface TablaTitularesProps { onReorder: (titulares: Titular[]) => void; onDelete: (id: number) => 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 handleDragEnd = (event: DragEndEvent) => { @@ -115,7 +138,7 @@ const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit }: TablaTitular {titulares.map((titular) => ( - + ))}