Feat: Papelera de Avisos
- Se añade la sección de Papelera de Avisos para los avisos eliminados que serán removidos de los registros a los 60 días del cambio de estado. Es esta sección se permite restaurar un aviso eliminado al estado "Borrador".
This commit is contained in:
@@ -43,6 +43,12 @@ public class AdminController : ControllerBase
|
|||||||
.AsNoTracking() // Optimización de lectura
|
.AsNoTracking() // Optimización de lectura
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Por defecto, ocultar eliminados a menos que se pida explícitamente
|
||||||
|
if (statusId != (int)AdStatusEnum.Deleted)
|
||||||
|
{
|
||||||
|
query = query.Where(a => a.StatusID != (int)AdStatusEnum.Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
// Filtro por Texto (Marca, Modelo, Email Usuario, Nombre Usuario)
|
// Filtro por Texto (Marca, Modelo, Email Usuario, Nombre Usuario)
|
||||||
if (!string.IsNullOrEmpty(q))
|
if (!string.IsNullOrEmpty(q))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public class AdsV2Controller : ControllerBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
query = query.Where(a => a.UserID == userId.Value);
|
query = query.Where(a => a.UserID == userId.Value && a.StatusID != (int)AdStatusEnum.Deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- LÓGICA DE BÚSQUEDA POR PALABRAS ---
|
// --- LÓGICA DE BÚSQUEDA POR PALABRAS ---
|
||||||
@@ -763,6 +763,7 @@ public class AdsV2Controller : ControllerBase
|
|||||||
var ads = await _context.Favorites
|
var ads = await _context.Favorites
|
||||||
.Where(f => f.UserID == userId)
|
.Where(f => f.UserID == userId)
|
||||||
.Join(_context.Ads, f => f.AdID, a => a.AdID, (f, a) => a)
|
.Join(_context.Ads, f => f.AdID, a => a.AdID, (f, a) => a)
|
||||||
|
.Where(a => a.StatusID != (int)AdStatusEnum.Deleted)
|
||||||
.Include(a => a.Photos)
|
.Include(a => a.Photos)
|
||||||
.Select(a => new
|
.Select(a => new
|
||||||
{
|
{
|
||||||
@@ -824,6 +825,11 @@ public class AdsV2Controller : ControllerBase
|
|||||||
int oldStatus = ad.StatusID;
|
int oldStatus = ad.StatusID;
|
||||||
ad.StatusID = newStatus;
|
ad.StatusID = newStatus;
|
||||||
|
|
||||||
|
if (newStatus == (int)AdStatusEnum.Deleted)
|
||||||
|
{
|
||||||
|
ad.DeletedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
// 📝 AUDITORÍA
|
// 📝 AUDITORÍA
|
||||||
var statusBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? "";
|
var statusBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? "";
|
||||||
_context.AuditLogs.Add(new AuditLog
|
_context.AuditLogs.Add(new AuditLog
|
||||||
|
|||||||
@@ -74,8 +74,12 @@ public class ChatController : ControllerBase
|
|||||||
public async Task<IActionResult> GetInbox(int userId)
|
public async Task<IActionResult> GetInbox(int userId)
|
||||||
{
|
{
|
||||||
// Obtener todas las conversaciones donde el usuario es remitente o destinatario
|
// Obtener todas las conversaciones donde el usuario es remitente o destinatario
|
||||||
|
// Pero filtramos los que pertenecen a avisos eliminados (StatusID != 9)
|
||||||
var messages = await _context.ChatMessages
|
var messages = await _context.ChatMessages
|
||||||
.Where(m => m.SenderID == userId || m.ReceiverID == userId)
|
.Where(m => m.SenderID == userId || m.ReceiverID == userId)
|
||||||
|
.Join(_context.Ads, m => m.AdID, a => a.AdID, (m, a) => new { m, a })
|
||||||
|
.Where(x => x.a.StatusID != (int)AdStatusEnum.Deleted)
|
||||||
|
.Select(x => x.m)
|
||||||
.OrderByDescending(m => m.SentAt)
|
.OrderByDescending(m => m.SentAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -119,7 +123,8 @@ public class ChatController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var count = await _context.ChatMessages
|
var count = await _context.ChatMessages
|
||||||
.CountAsync(m => m.ReceiverID == userId && !m.IsRead);
|
.Join(_context.Ads, m => m.AdID, a => a.AdID, (m, a) => new { m, a })
|
||||||
|
.CountAsync(x => x.m.ReceiverID == userId && !x.m.IsRead && x.a.StatusID != (int)AdStatusEnum.Deleted);
|
||||||
|
|
||||||
return Ok(new { count });
|
return Ok(new { count });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const STATUS_CONFIG: Record<number, { label: string; color: string; bg: s
|
|||||||
icon: '📝'
|
icon: '📝'
|
||||||
},
|
},
|
||||||
[AD_STATUSES.DELETED]: {
|
[AD_STATUSES.DELETED]: {
|
||||||
label: 'Eliminar',
|
label: 'Eliminado',
|
||||||
color: 'text-white',
|
color: 'text-white',
|
||||||
bg: 'bg-red-700/90',
|
bg: 'bg-red-700/90',
|
||||||
border: 'border-red-500/50',
|
border: 'border-red-500/50',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import AdDetailsModal from '../components/AdDetailsModal';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ConfirmationModal from '../components/ConfirmationModal';
|
import ConfirmationModal from '../components/ConfirmationModal';
|
||||||
|
|
||||||
type TabType = 'stats' | 'ads' | 'moderation' | 'transactions' | 'users' | 'audit';
|
type TabType = 'stats' | 'ads' | 'moderation' | 'transactions' | 'users' | 'audit' | 'trash';
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('stats');
|
const [activeTab, setActiveTab] = useState<TabType>('stats');
|
||||||
@@ -45,8 +45,8 @@ export default function AdminPage() {
|
|||||||
let isDanger = false;
|
let isDanger = false;
|
||||||
|
|
||||||
if (newStatus === AD_STATUSES.DELETED) {
|
if (newStatus === AD_STATUSES.DELETED) {
|
||||||
title = "¿Eliminar Aviso?";
|
title = "¿Mover a la Papelera?";
|
||||||
message = "Esta acción eliminará el aviso permanentemente. No se puede deshacer.\n\n¿Estás seguro de continuar?";
|
message = "El aviso se ocultará de los listados y se moverá a la Papelera. Se mantendrá allí por 60 días antes de su eliminación definitiva.\n\n¿Estás seguro de continuar?";
|
||||||
isDanger = true;
|
isDanger = true;
|
||||||
} else if (newStatus === AD_STATUSES.PAUSED) {
|
} else if (newStatus === AD_STATUSES.PAUSED) {
|
||||||
title = "Pausar Publicación";
|
title = "Pausar Publicación";
|
||||||
@@ -179,6 +179,13 @@ export default function AdminPage() {
|
|||||||
page: adsFilters.page
|
page: adsFilters.page
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'trash':
|
||||||
|
res = await AdminService.getAllAds({
|
||||||
|
q: adsFilters.q,
|
||||||
|
statusId: 9, // Forzamos 9 para papelera
|
||||||
|
page: adsFilters.page
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
setData(res);
|
setData(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -222,20 +229,20 @@ export default function AdminPage() {
|
|||||||
className={`w-full flex items-center justify-between bg-white/5 p-4 rounded-2xl border backdrop-blur-xl text-white font-black uppercase tracking-widest text-xs transition-all ${isMobileMenuOpen ? 'border-blue-500 ring-2 ring-blue-500/20' : 'border-white/10'}`}
|
className={`w-full flex items-center justify-between bg-white/5 p-4 rounded-2xl border backdrop-blur-xl text-white font-black uppercase tracking-widest text-xs transition-all ${isMobileMenuOpen ? 'border-blue-500 ring-2 ring-blue-500/20' : 'border-white/10'}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{activeTab === 'stats' ? '📊 Resumen' : activeTab === 'ads' ? '📦 Avisos' : activeTab === 'moderation' ? '🛡️ Moderación' : activeTab === 'transactions' ? '💰 Pagos' : activeTab === 'users' ? '👥 Usuarios' : '📋 Auditoría'}
|
{activeTab === 'stats' ? '📊 Resumen' : activeTab === 'ads' ? '📦 Avisos' : activeTab === 'moderation' ? '🛡️ Moderación' : activeTab === 'transactions' ? '💰 Pagos' : activeTab === 'users' ? '👥 Usuarios' : activeTab === 'audit' ? '📋 Auditoría' : '🗑️ Papelera'}
|
||||||
</span>
|
</span>
|
||||||
<span className={`transition-transform duration-300 ${isMobileMenuOpen ? 'rotate-180 text-blue-400' : 'text-gray-500'}`}>▼</span>
|
<span className={`transition-transform duration-300 ${isMobileMenuOpen ? 'rotate-180 text-blue-400' : 'text-gray-500'}`}>▼</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div className="absolute top-full left-0 right-0 mt-2 bg-[#12141a] border border-white/10 rounded-2xl overflow-hidden z-[100] shadow-2xl animate-scale-up">
|
<div className="absolute top-full left-0 right-0 mt-2 bg-[#12141a] border border-white/10 rounded-2xl overflow-hidden z-[100] shadow-2xl animate-scale-up">
|
||||||
{(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit'] as TabType[]).map(tab => (
|
{(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit', 'trash'] as TabType[]).map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => handleTabChange(tab)}
|
onClick={() => handleTabChange(tab)}
|
||||||
className={`w-full text-left px-6 py-4 text-[10px] font-black uppercase tracking-widest transition-all border-b border-white/5 last:border-0 ${activeTab === tab ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-white/5'}`}
|
className={`w-full text-left px-6 py-4 text-[10px] font-black uppercase tracking-widest transition-all border-b border-white/5 last:border-0 ${activeTab === tab ? 'bg-blue-600 text-white' : 'text-gray-400 hover:bg-white/5'}`}
|
||||||
>
|
>
|
||||||
{tab === 'stats' ? '📊 Resumen' : tab === 'ads' ? '📦 Avisos' : tab === 'moderation' ? '🛡️ Moderación' : tab === 'transactions' ? '💰 Pagos' : tab === 'users' ? '👥 Usuarios' : '📋 Auditoría'}
|
{tab === 'stats' ? '📊 Resumen' : tab === 'ads' ? '📦 Avisos' : tab === 'moderation' ? '🛡️ Moderación' : tab === 'transactions' ? '💰 Pagos' : tab === 'users' ? '👥 Usuarios' : tab === 'audit' ? '📋 Auditoría' : '🗑️ Papelera'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -244,13 +251,13 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
{/* Menú tradicional para Escritorio */}
|
{/* Menú tradicional para Escritorio */}
|
||||||
<div className="hidden md:flex bg-white/5 p-1.5 rounded-2xl border border-white/5 backdrop-blur-xl">
|
<div className="hidden md:flex bg-white/5 p-1.5 rounded-2xl border border-white/5 backdrop-blur-xl">
|
||||||
{(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit'] as TabType[]).map(tab => (
|
{(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit', 'trash'] as TabType[]).map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => handleTabChange(tab)}
|
onClick={() => handleTabChange(tab)}
|
||||||
className={`px-5 md:px-6 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === tab ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/20' : 'text-gray-500 hover:text-white'}`}
|
className={`px-5 md:px-6 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === tab ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/20' : 'text-gray-500 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
{tab === 'stats' ? '📊 Resumen' : tab === 'ads' ? '📦 Avisos' : tab === 'moderation' ? '🛡️ Moderación' : tab === 'transactions' ? '💰 Pagos' : tab === 'users' ? '👥 Usuarios' : '📋 Auditoría'}
|
{tab === 'stats' ? '📊 Resumen' : tab === 'ads' ? '📦 Avisos' : tab === 'moderation' ? '🛡️ Moderación' : tab === 'transactions' ? '💰 Pagos' : tab === 'users' ? '👥 Usuarios' : tab === 'audit' ? '📋 Auditoría' : '🗑️ Papelera'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -341,8 +348,10 @@ export default function AdminPage() {
|
|||||||
onChange={e => setAdsFilters({ ...adsFilters, statusId: e.target.value })}
|
onChange={e => setAdsFilters({ ...adsFilters, statusId: e.target.value })}
|
||||||
className="w-full h-full bg-white/5 border border-white/10 rounded-xl md:rounded-2xl px-4 py-3 md:py-0 text-sm text-white outline-none focus:border-blue-500 appearance-none cursor-pointer"
|
className="w-full h-full bg-white/5 border border-white/10 rounded-xl md:rounded-2xl px-4 py-3 md:py-0 text-sm text-white outline-none focus:border-blue-500 appearance-none cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="" className="bg-gray-900">Todos los Estados</option>
|
<option value="" className="bg-gray-900">Activos y Otros</option>
|
||||||
{Object.entries(STATUS_CONFIG).map(([id, config]) => (
|
{Object.entries(STATUS_CONFIG)
|
||||||
|
.filter(([id]) => id !== "9") // Excluimos eliminados de la lista de Avisos
|
||||||
|
.map(([id, config]) => (
|
||||||
<option key={id} value={id} className="bg-gray-900">{config.label}</option>
|
<option key={id} value={id} className="bg-gray-900">{config.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -370,7 +379,7 @@ export default function AdminPage() {
|
|||||||
<tbody className="divide-y divide-white/5">
|
<tbody className="divide-y divide-white/5">
|
||||||
{data.ads.map((ad: any, index: number) => {
|
{data.ads.map((ad: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
<tr key={ad.adID} className="hover:bg-white/5 transition-colors relative" style={{ zIndex: 50 - index }}>
|
<tr key={ad.adID} className={`hover:bg-white/5 transition-colors relative ${ad.statusID === 9 ? 'opacity-60 bg-red-900/5' : ''}`} style={{ zIndex: 50 - index }}>
|
||||||
<td className="px-8 py-5">
|
<td className="px-8 py-5">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<img src={getImageUrl(ad.thumbnail)} className="w-20 h-14 object-cover rounded-xl border border-white/10" alt="" />
|
<img src={getImageUrl(ad.thumbnail)} className="w-20 h-14 object-cover rounded-xl border border-white/10" alt="" />
|
||||||
@@ -436,7 +445,7 @@ export default function AdminPage() {
|
|||||||
<div className="md:hidden space-y-4">
|
<div className="md:hidden space-y-4">
|
||||||
{data.ads.map((ad: any, index: number) => {
|
{data.ads.map((ad: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
<div key={ad.adID} className="glass p-5 rounded-3xl border border-white/5 space-y-4 shadow-xl relative" style={{ zIndex: 50 - index }}>
|
<div key={ad.adID} className={`glass p-5 rounded-3xl border space-y-4 shadow-xl relative transition-all ${ad.statusID === 9 ? 'border-red-500/30 bg-red-900/10 opacity-70' : 'border-white/5'}`} style={{ zIndex: 50 - index }}>
|
||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
<img src={getImageUrl(ad.thumbnail)} className="w-24 h-16 object-cover rounded-xl border border-white/10" alt="" />
|
<img src={getImageUrl(ad.thumbnail)} className="w-24 h-16 object-cover rounded-xl border border-white/10" alt="" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -526,6 +535,152 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* === VISTA PAPELERA === */}
|
||||||
|
{activeTab === 'trash' && data.ads && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 items-center justify-between bg-white/5 p-6 rounded-[2rem] border border-red-500/10 backdrop-blur-xl">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 w-full">
|
||||||
|
<div className="relative flex-1 group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar en papelera..."
|
||||||
|
value={adsFilters.q}
|
||||||
|
onChange={e => setAdsFilters({ ...adsFilters, q: e.target.value })}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && loadData()}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-2xl px-12 py-4 text-sm text-white outline-none focus:border-red-500 transition-all focus:bg-white/10"
|
||||||
|
/>
|
||||||
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">🔍</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setAdsFilters({ ...adsFilters, page: 1 }); loadData(); }}
|
||||||
|
className="w-full md:w-auto bg-red-600 hover:bg-red-500 text-white px-8 py-4 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all shadow-lg active:scale-95"
|
||||||
|
>
|
||||||
|
Filtrar Papelera
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden md:block glass rounded-[2.5rem] overflow-hidden border border-red-500/10">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead className="bg-red-900/10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-8 py-5 text-xs font-black uppercase tracking-widest text-red-500">Aviso Eliminado</th>
|
||||||
|
<th className="px-8 py-5 text-xs font-black uppercase tracking-widest text-gray-500">Eliminado el</th>
|
||||||
|
<th className="px-8 py-5 text-xs font-black uppercase tracking-widest text-red-400">Borrado Definitivo</th>
|
||||||
|
<th className="px-8 py-5 text-xs font-black uppercase tracking-widest text-gray-500 text-right">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/5">
|
||||||
|
{data.ads.map((ad: any) => {
|
||||||
|
const deleteDate = ad.deletedAt ? parseUTCDate(ad.deletedAt) : null;
|
||||||
|
const hardDeleteDate = deleteDate ? new Date(deleteDate.getTime() + (60 * 24 * 60 * 60 * 1000)) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={ad.adID} className="hover:bg-red-900/5 transition-colors">
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img src={getImageUrl(ad.thumbnail)} className="w-16 h-12 object-cover rounded-lg border border-white/10 grayscale opacity-50" alt="" />
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-black text-gray-300 uppercase block">{ad.brandName} {ad.versionName}</span>
|
||||||
|
<span className="text-[10px] text-gray-600 font-bold">ID: #{ad.adID} • {ad.userName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<span className="text-sm text-gray-400 font-medium">
|
||||||
|
{deleteDate ? deleteDate.toLocaleDateString() : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm text-red-400 font-black">
|
||||||
|
{hardDeleteDate ? hardDeleteDate.toLocaleDateString() : 'N/A'}
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] text-red-400/50 uppercase font-bold italic">Auto-limpieza programada</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => initiateStatusChange(ad.adID, AD_STATUSES.DRAFT)}
|
||||||
|
className="bg-white/5 hover:bg-amber-600/20 text-amber-400 px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest border border-white/10 hover:border-amber-500/30 transition-all"
|
||||||
|
>
|
||||||
|
Restaurar como Borrador
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Móvil Papelera */}
|
||||||
|
<div className="md:hidden space-y-4">
|
||||||
|
{data.ads.map((ad: any) => {
|
||||||
|
const deleteDate = ad.deletedAt ? parseUTCDate(ad.deletedAt) : null;
|
||||||
|
const hardDeleteDate = deleteDate ? new Date(deleteDate.getTime() + (60 * 24 * 60 * 60 * 1000)) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={ad.adID} className="glass p-5 rounded-3xl border border-red-500/10 space-y-4 opacity-80">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<img src={getImageUrl(ad.thumbnail)} className="w-20 h-14 object-cover rounded-xl grayscale opacity-50" alt="" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-black text-white uppercase">{ad.brandName} {ad.versionName}</h4>
|
||||||
|
<p className="text-[10px] text-gray-500 font-bold uppercase tracking-widest">ID: #{ad.adID}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 py-3 border-y border-white/5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Eliminado el</span>
|
||||||
|
<span className="text-xs text-white">{deleteDate ? deleteDate.toLocaleDateString() : 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-[8px] font-black text-red-400 uppercase tracking-widest">Borrado definitivo</span>
|
||||||
|
<span className="text-xs text-red-400 font-bold">{hardDeleteDate ? hardDeleteDate.toLocaleDateString() : 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => initiateStatusChange(ad.adID, AD_STATUSES.DRAFT)}
|
||||||
|
className="w-full bg-amber-600/10 text-amber-400 py-3 rounded-xl border border-amber-500/20 text-[10px] font-black uppercase tracking-widest"
|
||||||
|
>
|
||||||
|
Restaurar como Borrador
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.ads.length === 0 && (
|
||||||
|
<div className="p-20 text-center glass rounded-[2.5rem] border-dashed border-2 border-white/5">
|
||||||
|
<p className="text-gray-500 font-bold uppercase tracking-widest">La papelera está vacía.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Paginación Papelera */}
|
||||||
|
{data.total > data.pageSize && (
|
||||||
|
<div className="flex justify-center gap-4 mt-8">
|
||||||
|
<button
|
||||||
|
disabled={adsFilters.page === 1}
|
||||||
|
onClick={() => { const p = adsFilters.page - 1; setAdsFilters({ ...adsFilters, page: p }); loadData(); }}
|
||||||
|
className="p-4 rounded-xl bg-white/5 border border-white/10 text-white disabled:opacity-20 transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
⬅️
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center px-6 rounded-xl bg-white/5 border border-white/10 text-[10px] font-black uppercase tracking-widest text-gray-400">
|
||||||
|
{data.page} / {Math.ceil(data.total / data.pageSize)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
disabled={adsFilters.page >= Math.ceil(data.total / data.pageSize)}
|
||||||
|
onClick={() => { const p = adsFilters.page + 1; setAdsFilters({ ...adsFilters, page: p }); loadData(); }}
|
||||||
|
className="p-4 rounded-xl bg-white/5 border border-white/10 text-white disabled:opacity-20 transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
➡️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* VISTA MODERACIÓN */}
|
{/* VISTA MODERACIÓN */}
|
||||||
{activeTab === 'moderation' && Array.isArray(data) && (
|
{activeTab === 'moderation' && Array.isArray(data) && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user