2025-10-28 11:45:51 -03:00
|
|
|
// frontend/src/components/TablaTitulares.tsx
|
|
|
|
|
|
2025-10-29 12:26:38 -03:00
|
|
|
import { useState } from 'react';
|
2025-10-29 13:18:39 -03:00
|
|
|
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, IconButton, Typography, Link, TextField, Tooltip } from '@mui/material'; // <-- Añadir Tooltip
|
2025-10-28 11:45:51 -03:00
|
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
2025-10-29 11:36:20 -03:00
|
|
|
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
|
|
|
|
import EditIcon from '@mui/icons-material/Edit';
|
2025-10-28 11:54:36 -03:00
|
|
|
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
|
2025-10-28 11:45:51 -03:00
|
|
|
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
|
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
|
|
|
import type { Titular } from '../types';
|
2025-10-29 12:26:38 -03:00
|
|
|
import type { ActualizarTitularPayload } from '../services/apiService';
|
2025-10-28 11:45:51 -03:00
|
|
|
|
2025-10-28 11:54:36 -03:00
|
|
|
interface SortableRowProps {
|
|
|
|
|
titular: Titular;
|
|
|
|
|
onDelete: (id: number) => void;
|
2025-10-29 12:26:38 -03:00
|
|
|
onSave: (id: number, payload: ActualizarTitularPayload) => void;
|
2025-10-28 14:12:05 -03:00
|
|
|
onEdit: (titular: Titular) => void;
|
2025-10-28 11:54:36 -03:00
|
|
|
}
|
|
|
|
|
|
2025-10-29 12:26:38 -03:00
|
|
|
const SortableRow = ({ titular, onDelete, onSave, onEdit }: SortableRowProps) => {
|
2025-10-28 11:45:51 -03:00
|
|
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id });
|
2025-10-29 12:26:38 -03:00
|
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
|
const [editText, setEditText] = useState(titular.texto);
|
2025-10-28 11:45:51 -03:00
|
|
|
|
2025-10-29 12:26:38 -03:00
|
|
|
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);
|
2025-10-28 11:45:51 -03:00
|
|
|
};
|
|
|
|
|
|
2025-10-29 11:36:20 -03:00
|
|
|
const getChipColor = (tipo: Titular['tipo']): "success" | "warning" | "info" => {
|
2025-10-28 11:45:51 -03:00
|
|
|
if (tipo === 'Edited') return 'warning';
|
|
|
|
|
if (tipo === 'Manual') return 'info';
|
|
|
|
|
return 'success';
|
|
|
|
|
};
|
2025-10-29 12:26:38 -03:00
|
|
|
|
2025-10-29 11:36:20 -03:00
|
|
|
const formatFuente = (fuente: string | null) => {
|
|
|
|
|
if (!fuente) return 'N/A';
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(fuente);
|
|
|
|
|
return url.hostname.replace('www.', '');
|
|
|
|
|
} catch {
|
|
|
|
|
return fuente;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:51 -03:00
|
|
|
|
|
|
|
|
return (
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableRow
|
|
|
|
|
ref={setNodeRef}
|
|
|
|
|
style={style}
|
|
|
|
|
{...attributes}
|
|
|
|
|
sx={{
|
|
|
|
|
'&:hover': {
|
|
|
|
|
backgroundColor: 'action.hover'
|
|
|
|
|
},
|
|
|
|
|
// Oculta el borde de la última fila
|
|
|
|
|
'&:last-child td, &:last-child th': { border: 0 },
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<TableCell sx={{ padding: '8px 16px', cursor: 'grab', verticalAlign: 'middle' }} {...listeners}>
|
|
|
|
|
<Tooltip title="Arrastrar para reordenar" placement="top">
|
|
|
|
|
<DragHandleIcon sx={{ color: 'text.secondary' }} />
|
|
|
|
|
</Tooltip>
|
2025-10-28 11:54:36 -03:00
|
|
|
</TableCell>
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableCell sx={{ padding: '8px 16px', verticalAlign: 'middle' }} onClick={() => setIsEditing(true)}>
|
2025-10-29 12:26:38 -03:00
|
|
|
{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>
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableCell sx={{ padding: '8px 16px', verticalAlign: 'middle' }}>
|
2025-10-28 11:45:51 -03:00
|
|
|
<Chip label={titular.tipo || 'Scraped'} color={getChipColor(titular.tipo)} size="small" />
|
|
|
|
|
</TableCell>
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableCell sx={{ padding: '8px 16px', verticalAlign: 'middle' }}>
|
2025-10-29 11:36:20 -03:00
|
|
|
{titular.urlFuente ? (
|
|
|
|
|
<Link href={titular.urlFuente} target="_blank" rel="noopener noreferrer" underline="hover" color="primary.light">
|
|
|
|
|
{formatFuente(titular.fuente)}
|
|
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
formatFuente(titular.fuente)
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableCell sx={{ padding: '8px 16px', verticalAlign: 'middle', textAlign: 'right' }}>
|
|
|
|
|
<Tooltip title="Editar viñeta" placement="top">
|
|
|
|
|
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(titular); }}>
|
|
|
|
|
<EditIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip title="Eliminar titular" placement="top">
|
|
|
|
|
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(titular.id); }} sx={{ color: '#ef4444' }}>
|
|
|
|
|
<DeleteIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
2025-10-28 11:45:51 -03:00
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-28 11:54:36 -03:00
|
|
|
interface TablaTitularesProps {
|
|
|
|
|
titulares: Titular[];
|
|
|
|
|
onReorder: (titulares: Titular[]) => void;
|
|
|
|
|
onDelete: (id: number) => void;
|
2025-10-28 14:12:05 -03:00
|
|
|
onEdit: (titular: Titular) => void;
|
2025-10-29 12:26:38 -03:00
|
|
|
onSave: (id: number, payload: ActualizarTitularPayload) => void;
|
2025-10-28 11:54:36 -03:00
|
|
|
}
|
2025-10-28 11:45:51 -03:00
|
|
|
|
2025-10-29 12:26:38 -03:00
|
|
|
const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit, onSave }: TablaTitularesProps) => {
|
2025-10-29 11:36:20 -03:00
|
|
|
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }));
|
2025-10-28 11:45:51 -03:00
|
|
|
|
2025-10-28 11:54:36 -03:00
|
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
2025-10-28 11:45:51 -03:00
|
|
|
const { active, over } = event;
|
2025-10-28 11:54:36 -03:00
|
|
|
if (over && active.id !== over.id) {
|
|
|
|
|
const oldIndex = titulares.findIndex((item) => item.id === active.id);
|
|
|
|
|
const newIndex = titulares.findIndex((item) => item.id === over.id);
|
2025-10-29 11:36:20 -03:00
|
|
|
onReorder(arrayMove(titulares, oldIndex, newIndex));
|
2025-10-28 11:45:51 -03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-28 11:54:36 -03:00
|
|
|
if (titulares.length === 0) {
|
|
|
|
|
return (
|
2025-10-29 11:36:20 -03:00
|
|
|
<Paper elevation={0} sx={{ p: 3, textAlign: 'center' }}>
|
2025-10-28 11:54:36 -03:00
|
|
|
<Typography>No hay titulares para mostrar.</Typography>
|
|
|
|
|
</Paper>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 11:45:51 -03:00
|
|
|
return (
|
2025-10-29 11:36:20 -03:00
|
|
|
<Paper elevation={0} sx={{ overflow: 'hidden' }}>
|
2025-10-28 11:54:36 -03:00
|
|
|
<TableContainer>
|
|
|
|
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
|
|
|
<SortableContext items={titulares.map(t => t.id)} strategy={verticalListSortingStrategy}>
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHead>
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableRow sx={{ '& .MuiTableCell-root': { padding: '6px 16px', borderBottom: '1px solid rgba(255, 255, 255, 0.12)' } }}>
|
2025-10-29 11:36:20 -03:00
|
|
|
<TableCell sx={{ width: 50 }} />
|
|
|
|
|
<TableCell sx={{ textTransform: 'uppercase', color: 'text.secondary', letterSpacing: '0.05em' }}>Texto del Titular</TableCell>
|
|
|
|
|
<TableCell sx={{ textTransform: 'uppercase', color: 'text.secondary', letterSpacing: '0.05em' }}>Tipo</TableCell>
|
|
|
|
|
<TableCell sx={{ textTransform: 'uppercase', color: 'text.secondary', letterSpacing: '0.05em' }}>Fuente</TableCell>
|
|
|
|
|
<TableCell sx={{ textTransform: 'uppercase', color: 'text.secondary', letterSpacing: '0.05em', textAlign: 'right' }}>Acciones</TableCell>
|
2025-10-28 11:54:36 -03:00
|
|
|
</TableRow>
|
|
|
|
|
</TableHead>
|
2025-10-29 13:18:39 -03:00
|
|
|
<TableBody sx={{ '& .MuiTableRow-root:nth-of-type(odd)': { backgroundColor: 'action.focus' } }}>
|
2025-10-28 11:54:36 -03:00
|
|
|
{titulares.map((titular) => (
|
2025-10-29 12:26:38 -03:00
|
|
|
<SortableRow key={titular.id} titular={titular} onDelete={onDelete} onEdit={onEdit} onSave={onSave} />
|
2025-10-28 11:54:36 -03:00
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</SortableContext>
|
|
|
|
|
</DndContext>
|
|
|
|
|
</TableContainer>
|
|
|
|
|
</Paper>
|
2025-10-28 11:45:51 -03:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default TablaTitulares;
|