diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs b/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs index 7aa3ba2..e4f2ed5 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs @@ -43,6 +43,12 @@ public class AdminController : ControllerBase .AsNoTracking() // Optimización de lectura .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) if (!string.IsNullOrEmpty(q)) { diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs index 6c7439e..e98f936 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs @@ -119,7 +119,7 @@ public class AdsV2Controller : ControllerBase } 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 --- @@ -763,6 +763,7 @@ public class AdsV2Controller : ControllerBase var ads = await _context.Favorites .Where(f => f.UserID == userId) .Join(_context.Ads, f => f.AdID, a => a.AdID, (f, a) => a) + .Where(a => a.StatusID != (int)AdStatusEnum.Deleted) .Include(a => a.Photos) .Select(a => new { @@ -824,6 +825,11 @@ public class AdsV2Controller : ControllerBase int oldStatus = ad.StatusID; ad.StatusID = newStatus; + if (newStatus == (int)AdStatusEnum.Deleted) + { + ad.DeletedAt = DateTime.UtcNow; + } + // 📝 AUDITORÍA var statusBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? ""; _context.AuditLogs.Add(new AuditLog diff --git a/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs b/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs index f0100de..98d926d 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs @@ -74,8 +74,12 @@ public class ChatController : ControllerBase public async Task GetInbox(int userId) { // 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 .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) .ToListAsync(); @@ -119,7 +123,8 @@ public class ChatController : ControllerBase } 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 }); } diff --git a/Frontend/src/constants/adStatuses.ts b/Frontend/src/constants/adStatuses.ts index 20de748..8653c12 100644 --- a/Frontend/src/constants/adStatuses.ts +++ b/Frontend/src/constants/adStatuses.ts @@ -63,7 +63,7 @@ export const STATUS_CONFIG: Record('stats'); @@ -45,8 +45,8 @@ export default function AdminPage() { let isDanger = false; if (newStatus === AD_STATUSES.DELETED) { - title = "¿Eliminar Aviso?"; - message = "Esta acción eliminará el aviso permanentemente. No se puede deshacer.\n\n¿Estás seguro de continuar?"; + title = "¿Mover a la Papelera?"; + 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; } else if (newStatus === AD_STATUSES.PAUSED) { title = "Pausar Publicación"; @@ -179,6 +179,13 @@ export default function AdminPage() { page: adsFilters.page }); break; + case 'trash': + res = await AdminService.getAllAds({ + q: adsFilters.q, + statusId: 9, // Forzamos 9 para papelera + page: adsFilters.page + }); + break; } setData(res); } 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'}`} > - {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'} {isMobileMenuOpen && (
- {(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit'] as TabType[]).map(tab => ( + {(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit', 'trash'] as TabType[]).map(tab => ( ))}
@@ -244,13 +251,13 @@ export default function AdminPage() { {/* Menú tradicional para Escritorio */}
- {(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit'] as TabType[]).map(tab => ( + {(['stats', 'ads', 'moderation', 'transactions', 'users', 'audit', 'trash'] as TabType[]).map(tab => ( ))}
@@ -341,10 +348,12 @@ export default function AdminPage() { 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" > - - {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]) => ( + + ))} @@ -370,7 +379,7 @@ export default function AdminPage() { {data.ads.map((ad: any, index: number) => { return ( - +
@@ -436,7 +445,7 @@ export default function AdminPage() {
{data.ads.map((ad: any, index: number) => { return ( -
+
@@ -526,6 +535,152 @@ export default function AdminPage() {
)} + {/* === VISTA PAPELERA === */} + {activeTab === 'trash' && data.ads && ( +
+
+
+
+ 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" + /> + 🔍 +
+
+ +
+ +
+ + + + + + + + + + + {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 ( + + + + + + + ); + })} + +
Aviso EliminadoEliminado elBorrado DefinitivoAcciones
+
+ +
+ {ad.brandName} {ad.versionName} + ID: #{ad.adID} • {ad.userName} +
+
+
+ + {deleteDate ? deleteDate.toLocaleDateString() : 'N/A'} + + +
+ + {hardDeleteDate ? hardDeleteDate.toLocaleDateString() : 'N/A'} + + Auto-limpieza programada +
+
+ +
+
+ + {/* Móvil Papelera */} +
+ {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 ( +
+
+ +
+

{ad.brandName} {ad.versionName}

+

ID: #{ad.adID}

+
+
+
+
+ Eliminado el + {deleteDate ? deleteDate.toLocaleDateString() : 'N/A'} +
+
+ Borrado definitivo + {hardDeleteDate ? hardDeleteDate.toLocaleDateString() : 'N/A'} +
+
+ +
+ ); + })} +
+ + {data.ads.length === 0 && ( +
+

La papelera está vacía.

+
+ )} + + {/* Paginación Papelera */} + {data.total > data.pageSize && ( +
+ +
+ {data.page} / {Math.ceil(data.total / data.pageSize)} +
+ +
+ )} +
+ )} + {/* VISTA MODERACIÓN */} {activeTab === 'moderation' && Array.isArray(data) && (