@@ -9,7 +9,7 @@ import AdDetailsModal from '../components/AdDetailsModal';
import { Link } from 'react-router-dom' ;
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() {
const [ activeTab , setActiveTab ] = useState < TabType > ( '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' } ` }
>
< 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' : '🗑️ Papeler a'}
< / span >
< span className = { ` transition-transform duration-300 ${ isMobileMenuOpen ? 'rotate-180 text-blue-400' : 'text-gray-500' } ` } > ▼ < / span >
< / button >
{ 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" >
{ ( [ 'stats' , 'ads' , 'moderation' , 'transactions' , 'users' , 'audit' ] as TabType [ ] ) . map ( tab = > (
{ ( [ 'stats' , 'ads' , 'moderation' , 'transactions' , 'users' , 'audit' , 'trash' ] as TabType [ ] ) . map ( tab = > (
< button
key = { 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' } ` }
>
{ 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' : '🗑️ Papeler a'}
< / button >
) ) }
< / div >
@@ -244,13 +251,13 @@ export default function AdminPage() {
{ /* Menú tradicional para Escritorio */ }
< 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
key = { 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' } ` }
>
{ 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' : '🗑️ Papeler a'}
< / button >
) ) }
< / div >
@@ -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"
>
< option value = "" className = "bg-gray-900" > Todos los Estad os< / option >
{ Object . entries ( STATUS_CONFIG ) . map ( ( [ id , config ] ) = > (
< option key = { id } value = { id } className = "bg-gray-900" > { config . label } < / option >
) ) }
< option value = "" className = "bg-gray-900" > Activos y Otr os< / option >
{ 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 >
) ) }
< / select >
< / div >
< / div >
@@ -370,7 +379,7 @@ export default function AdminPage() {
< tbody className = "divide-y divide-white/5" >
{ data . ads . map ( ( ad : any , index : number ) = > {
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" >
< 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 = "" / >
@@ -436,7 +445,7 @@ export default function AdminPage() {
< div className = "md:hidden space-y-4" >
{ data . ads . map ( ( ad : any , index : number ) = > {
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" >
< 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" >
@@ -526,6 +535,152 @@ export default function AdminPage() {
< / 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 */ }
{ activeTab === 'moderation' && Array . isArray ( data ) && (
< div className = "space-y-6" >