174 lines
7.4 KiB
TypeScript
174 lines
7.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import api from '../../services/api';
|
|
import ListingDetailModal from '../../components/Listings/ListingDetailModal';
|
|
import { listingService } from '../../services/listingService';
|
|
import { Search, ExternalLink, Calendar, Tag, User as UserIcon } from 'lucide-react';
|
|
|
|
export default function ListingExplorer() {
|
|
const [listings, setListings] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [query, setQuery] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("");
|
|
const [selectedDetail, setSelectedDetail] = useState<any>(null);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
const handleOpenDetail = async (id: number) => {
|
|
const detail = await listingService.getById(id);
|
|
setSelectedDetail(detail);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const loadListings = async () => {
|
|
setLoading(true);
|
|
try {
|
|
// Usamos el endpoint de búsqueda que ya tiene el backend
|
|
const res = await api.get('/listings', {
|
|
params: { q: query }
|
|
});
|
|
|
|
let data = res.data;
|
|
if (statusFilter) {
|
|
data = data.filter((l: any) => l.status === statusFilter);
|
|
}
|
|
|
|
setListings(data);
|
|
} catch (error) {
|
|
console.error("Error cargando avisos:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadListings();
|
|
}, [statusFilter]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center text-gray-800">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">Explorador de Avisos</h2>
|
|
<p className="text-sm text-gray-500">Gestión y búsqueda histórica de publicaciones</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* BARRA DE HERRAMIENTAS */}
|
|
<div className="bg-white p-4 rounded-xl border shadow-sm flex flex-col md:flex-row gap-4">
|
|
<div className="flex-1 relative">
|
|
<Search className="absolute left-3 top-3 text-gray-400" size={18} />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar por título, contenido o ID..."
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 transition-all"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && loadListings()}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={statusFilter}
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
|
className="border border-gray-200 rounded-lg px-4 py-2 text-sm font-medium text-gray-600 outline-none"
|
|
>
|
|
<option value="">Todos los estados</option>
|
|
<option value="Published">Publicados</option>
|
|
<option value="Pending">Pendientes</option>
|
|
<option value="Rejected">Rechazados</option>
|
|
<option value="Draft">Borradores</option>
|
|
</select>
|
|
|
|
<button
|
|
onClick={loadListings}
|
|
className="bg-gray-900 text-white px-6 py-2 rounded-lg font-bold hover:bg-gray-800 transition shadow-md"
|
|
>
|
|
BUSCAR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* RESULTADOS */}
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead className="bg-gray-50 text-gray-400 text-xs uppercase font-bold tracking-wider">
|
|
<tr>
|
|
<th className="p-4 border-b">ID</th>
|
|
<th className="p-4 border-b">Aviso / Título</th>
|
|
<th className="p-4 border-b">Ingreso (Aviso)</th>
|
|
<th className="p-4 border-b">Valor Producto</th>
|
|
<th className="p-4 border-b">Creado</th>
|
|
<th className="p-4 border-b">Vía</th>
|
|
<th className="p-4 border-b text-center">Estado</th>
|
|
<th className="p-4 border-b text-right">Detalles</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100 text-sm">
|
|
{loading ? (
|
|
<tr><td colSpan={7} className="p-10 text-center text-gray-400">Buscando en la base de datos...</td></tr>
|
|
) : listings.length === 0 ? (
|
|
<tr><td colSpan={7} className="p-10 text-center text-gray-400 italic">No se encontraron avisos con esos criterios.</td></tr>
|
|
) : (
|
|
listings.map((l) => (
|
|
<tr key={l.id} className="hover:bg-gray-50 transition-colors group">
|
|
<td className="p-4 font-mono text-gray-400">#{l.id}</td>
|
|
<td className="p-4">
|
|
<div className="font-bold text-gray-800 group-hover:text-blue-600 transition-colors">{l.title}</div>
|
|
<div className="text-xs text-gray-400 flex items-center gap-1 mt-0.5">
|
|
<Tag size={12} /> {l.categoryName || 'Sin Rubro'}
|
|
</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<div className="font-bold text-green-700">
|
|
${l.adFee?.toLocaleString() ?? "0"}
|
|
</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<div className="text-gray-500 text-xs">${l.price?.toLocaleString()}</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<div className="text-gray-600 flex items-center gap-1.5">
|
|
<Calendar size={14} className="text-gray-300" />
|
|
{new Date(l.createdAt).toLocaleDateString()}
|
|
</div>
|
|
</td>
|
|
<td className="p-4">
|
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-black uppercase ${l.userId === null ? 'bg-blue-50 text-blue-600' : 'bg-green-50 text-green-600'
|
|
}`}>
|
|
<UserIcon size={10} />
|
|
{l.userId === null ? 'Web' : 'Mostrador'}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 text-center">
|
|
<span className={`px-2 py-1 rounded-full text-[10px] font-bold uppercase border ${l.status === 'Published' ? 'bg-green-50 text-green-700 border-green-200' :
|
|
l.status === 'Pending' ? 'bg-yellow-50 text-yellow-700 border-yellow-200' :
|
|
'bg-gray-50 text-gray-600 border-gray-200'
|
|
}`}>
|
|
{l.status}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 text-right">
|
|
<button
|
|
onClick={() => handleOpenDetail(l.id)}
|
|
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all"
|
|
title="Ver Detalles"
|
|
>
|
|
<ExternalLink size={18} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<ListingDetailModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
detail={selectedDetail}
|
|
/>
|
|
</div>
|
|
);
|
|
} |