Fase 2 Completa: Implementada UI con formulario de configuración, modal de adición y funcionalidad CRUD completa en la tabla de titulares.

This commit is contained in:
2025-10-28 11:54:36 -03:00
parent 8e783b73d5
commit 7eee798c99
6 changed files with 257 additions and 87 deletions

View File

@@ -1,19 +1,22 @@
// frontend/src/components/TablaTitulares.tsx
import { useEffect, useState } from 'react';
import {
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import DragHandleIcon from '@mui/icons-material/DragHandle'; // Importar el ícono
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { Titular } from '../types';
import * as api from '../services/apiService';
// Componente para una fila de tabla "arrastrable"
const SortableRow = ({ titular }: { titular: Titular }) => {
// La prop `onDelete` se añade para comunicar el evento al componente padre
interface SortableRowProps {
titular: Titular;
onDelete: (id: number) => void;
}
const SortableRow = ({ titular, onDelete }: SortableRowProps) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id });
const style = {
@@ -28,15 +31,19 @@ const SortableRow = ({ titular }: { titular: Titular }) => {
};
return (
<TableRow ref={setNodeRef} style={style} {...attributes} {...listeners}>
<TableCell>...</TableCell> {/* Handle para arrastrar */}
<TableRow ref={setNodeRef} style={style} {...attributes} >
{/* El handle de arrastre ahora es un ícono */}
<TableCell sx={{ cursor: 'grab' }} {...listeners}>
<DragHandleIcon />
</TableCell>
<TableCell>{titular.texto}</TableCell>
<TableCell>
<Chip label={titular.tipo || 'Scraped'} color={getChipColor(titular.tipo)} size="small" />
</TableCell>
<TableCell>{titular.fuente}</TableCell>
<TableCell>
<IconButton size="small" onClick={() => console.log('Eliminar:', titular.id)}>
{/* Usamos un stopPropagation para que el clic no active el arrastre */}
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(titular.id); }}>
<DeleteIcon />
</IconButton>
</TableCell>
@@ -44,75 +51,58 @@ const SortableRow = ({ titular }: { titular: Titular }) => {
);
};
interface TablaTitularesProps {
titulares: Titular[];
onReorder: (titulares: Titular[]) => void;
onDelete: (id: number) => void;
}
// Componente principal de la tabla
const TablaTitulares = () => {
const [titulares, setTitulares] = useState<Titular[]>([]);
const TablaTitulares = ({ titulares, onReorder, onDelete }: TablaTitularesProps) => {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })); // Evita activar el drag con un simple clic
// Sensores para dnd-kit: reaccionar a clics de puntero
const sensors = useSensors(useSensor(PointerSensor));
const cargarTitulares = async () => {
try {
const data = await api.obtenerTitulares();
setTitulares(data);
} catch (error) {
console.error("Error al cargar titulares:", error);
}
};
useEffect(() => {
cargarTitulares();
}, []);
const handleDragEnd = (event: any) => {
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over.id) {
setTitulares((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
const newArray = arrayMove(items, oldIndex, newIndex);
// Creamos el payload para la API
const payload = newArray.map((item, index) => ({
id: item.id,
nuevoOrden: index
}));
// Llamada a la API en segundo plano
api.actualizarOrdenTitulares(payload).catch(err => {
console.error("Error al reordenar:", err);
// Opcional: revertir el estado si la API falla
});
return newArray;
});
if (over && active.id !== over.id) {
const oldIndex = titulares.findIndex((item) => item.id === active.id);
const newIndex = titulares.findIndex((item) => item.id === over.id);
const newArray = arrayMove(titulares, oldIndex, newIndex);
onReorder(newArray); // Pasamos el nuevo array al padre para que gestione el estado y la llamada a la API
}
};
if (titulares.length === 0) {
return (
<Paper elevation={3} sx={{ padding: 3, textAlign: 'center' }}>
<Typography>No hay titulares para mostrar.</Typography>
</Paper>
);
}
return (
<TableContainer component={Paper}>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={titulares.map(t => t.id)} strategy={verticalListSortingStrategy}>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ width: 50 }}></TableCell> {/* Celda para el drag handle */}
<TableCell>Texto del Titular</TableCell>
<TableCell>Tipo</TableCell>
<TableCell>Fuente</TableCell>
<TableCell>Acciones</TableCell>
</TableRow>
</TableHead>
<TableBody>
{titulares.map((titular) => (
<SortableRow key={titular.id} titular={titular} />
))}
</TableBody>
</Table>
</SortableContext>
</DndContext>
</TableContainer>
<Paper elevation={3}>
<TableContainer>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={titulares.map(t => t.id)} strategy={verticalListSortingStrategy}>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ width: 50 }}></TableCell>
<TableCell>Texto del Titular</TableCell>
<TableCell>Tipo</TableCell>
<TableCell>Fuente</TableCell>
<TableCell>Acciones</TableCell>
</TableRow>
</TableHead>
<TableBody>
{titulares.map((titular) => (
<SortableRow key={titular.id} titular={titular} onDelete={onDelete} />
))}
</TableBody>
</Table>
</SortableContext>
</DndContext>
</TableContainer>
</Paper>
);
};