Mejora 2: Implementada edición en línea para el texto de los titulares.
This commit is contained in:
@@ -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}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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" => {
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user