2026-02-13 15:52:33 -03:00
import { useState , useEffect , useRef } from "react" ;
import { Link } from "react-router-dom" ;
import { AdsV2Service , type AdListingDto } from "../services/ads.v2.service" ;
import { useAuth } from "../context/AuthContext" ;
import { ChatService , type ChatMessage } from "../services/chat.service" ;
import ChatModal from "../components/ChatModal" ;
2026-02-19 19:47:13 -03:00
import { formatCurrency , getImageUrl , parseUTCDate } from "../utils/app.utils" ;
2026-02-13 15:52:33 -03:00
import { AD_STATUSES , STATUS_CONFIG } from "../constants/adStatuses" ;
import ConfirmationModal from "../components/ConfirmationModal" ;
type TabType = "avisos" | "favoritos" | "mensajes" ;
2026-01-29 13:43:44 -03:00
export default function MisAvisosPage() {
2026-02-13 15:52:33 -03:00
const [ activeTab , setActiveTab ] = useState < TabType > ( "avisos" ) ;
2026-01-29 13:43:44 -03:00
const [ avisos , setAvisos ] = useState < AdListingDto [ ] > ( [ ] ) ;
const [ favoritos , setFavoritos ] = useState < AdListingDto [ ] > ( [ ] ) ;
const [ mensajes , setMensajes ] = useState < ChatMessage [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const { user , fetchUnreadCount } = useAuth ( ) ;
2026-02-13 15:52:33 -03:00
const [ selectedChat , setSelectedChat ] = useState < {
adId : number ;
name : string ;
otherUserId : number ;
} | null > ( null ) ;
2026-01-29 13:43:44 -03:00
const [ modalConfig , setModalConfig ] = useState < {
isOpen : boolean ;
title : string ;
message : string ;
adId : number | null ;
newStatus : number | null ;
isDanger : boolean ;
} > ( {
isOpen : false ,
2026-02-13 15:52:33 -03:00
title : "" ,
message : "" ,
2026-01-29 13:43:44 -03:00
adId : null ,
newStatus : null ,
2026-02-13 15:52:33 -03:00
isDanger : false ,
2026-01-29 13:43:44 -03:00
} ) ;
// Función para forzar chequeo manual desde Gestión
const handleVerifyPayment = async ( adId : number ) = > {
try {
const res = await AdsV2Service . checkPaymentStatus ( adId ) ;
2026-02-13 15:52:33 -03:00
if ( res . status === "approved" ) {
2026-01-29 13:43:44 -03:00
alert ( "¡Pago confirmado! El aviso pasará a moderación." ) ;
cargarAvisos ( user ! . id ) ;
2026-02-13 15:52:33 -03:00
} else if ( res . status === "rejected" ) {
2026-01-29 13:43:44 -03:00
alert ( "El pago fue rechazado. Puedes intentar pagar nuevamente." ) ;
cargarAvisos ( user ! . id ) ; // Debería volver a estado Draft/1
} else {
alert ( "El pago sigue pendiente de aprobación por la tarjeta." ) ;
}
} catch ( e ) {
alert ( "Error verificando el pago." ) ;
}
} ;
useEffect ( ( ) = > {
if ( user ) {
cargarMensajes ( user . id ) ;
2026-02-13 15:52:33 -03:00
if ( activeTab === "avisos" ) cargarAvisos ( user . id ) ;
else if ( activeTab === "favoritos" ) cargarFavoritos ( user . id ) ;
2026-01-29 13:43:44 -03:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ user ? . id , activeTab ] ) ;
const initiateStatusChange = ( adId : number , newStatus : number ) = > {
2026-02-13 15:52:33 -03:00
let title = "Cambiar Estado" ;
let message = "¿Estás seguro de realizar esta acción?" ;
2026-01-29 13:43:44 -03:00
let isDanger = false ;
// 1. ELIMINAR
if ( newStatus === AD_STATUSES . DELETED ) {
2026-02-13 15:52:33 -03:00
title = "¿Eliminar Aviso?" ;
message =
"Esta acción eliminará el aviso permanentemente. No se puede deshacer.\n\n¿Estás seguro de continuar?" ;
2026-01-29 13:43:44 -03:00
isDanger = true ;
}
// 2. PAUSAR
else if ( newStatus === AD_STATUSES . PAUSED ) {
2026-02-13 15:52:33 -03:00
title = "Pausar Publicación" ;
message =
"Al pausar el aviso:\n\n• Dejará de ser visible en los listados.\n• Los usuarios NO podrán contactarte.\n\nPodrás reactivarlo cuando quieras, dentro de la vigencia de publicación." ;
2026-01-29 13:43:44 -03:00
}
// 3. VENDIDO
else if ( newStatus === AD_STATUSES . SOLD ) {
2026-02-13 15:52:33 -03:00
title = "¡Felicitaciones!" ;
message =
'Al marcar como VENDIDO:\n\n• Se deshabilitarán nuevas consultas.\n• El aviso mostrará la etiqueta "Vendido" al público.\n\n¿Confirmas que ya vendiste el vehículo?' ;
2026-01-29 13:43:44 -03:00
}
// 4. RESERVADO
else if ( newStatus === AD_STATUSES . RESERVED ) {
2026-02-13 15:52:33 -03:00
title = "Reservar Vehículo" ;
message =
"Al reservar el aviso:\n\n• Se indicará a los interesados que el vehículo está reservado.\n• Se bloquearán nuevos contactos hasta que lo actives o vendas.\n\n¿Deseas continuar?" ;
2026-01-29 13:43:44 -03:00
}
// 5. ACTIVAR (Desde Pausado/Reservado)
else if ( newStatus === AD_STATUSES . ACTIVE ) {
2026-02-13 15:52:33 -03:00
title = "Reactivar Aviso" ;
message =
"El aviso volverá a estar visible para todos y recibirás consultas nuevamente." ;
2026-01-29 13:43:44 -03:00
}
setModalConfig ( {
isOpen : true ,
title ,
message ,
adId ,
newStatus ,
2026-02-13 15:52:33 -03:00
isDanger ,
2026-01-29 13:43:44 -03:00
} ) ;
} ;
// Acción real al confirmar en el modal
const confirmStatusChange = async ( ) = > {
const { adId , newStatus } = modalConfig ;
if ( ! adId || ! newStatus ) return ;
try {
setModalConfig ( { . . . modalConfig , isOpen : false } ) ; // Cerrar modal primero
await AdsV2Service . changeStatus ( adId , newStatus ) ;
if ( user ) cargarAvisos ( user . id ) ;
} catch ( error ) {
2026-02-13 15:52:33 -03:00
alert ( "Error al actualizar estado" ) ;
2026-01-29 13:43:44 -03:00
}
} ;
const cargarAvisos = async ( userId : number ) = > {
setLoading ( true ) ;
try {
const data = await AdsV2Service . getAll ( { userId } ) ;
setAvisos ( data ) ;
} catch ( error ) {
console . error ( error ) ;
} finally {
setLoading ( false ) ;
}
} ;
const cargarFavoritos = async ( userId : number ) = > {
setLoading ( true ) ;
try {
const data = await AdsV2Service . getFavorites ( userId ) ;
setFavoritos ( data ) ;
} catch ( error ) {
console . error ( error ) ;
} finally {
setLoading ( false ) ;
}
} ;
const cargarMensajes = async ( userId : number ) = > {
try {
const data = await ChatService . getInbox ( userId ) ;
setMensajes ( data ) ;
} catch ( error ) {
console . error ( error ) ;
}
} ;
// Marcar como leídos en DB
const openChatForAd = async ( adId : number , adTitle : string ) = > {
if ( ! user ) return ;
2026-02-13 15:52:33 -03:00
const relatedMsg = mensajes . find ( ( m ) = > m . adID === adId ) ;
2026-01-29 13:43:44 -03:00
if ( relatedMsg ) {
2026-02-13 15:52:33 -03:00
const otherId =
relatedMsg . senderID === user . id
? relatedMsg . receiverID
: relatedMsg . senderID ;
2026-01-29 13:43:44 -03:00
// Identificar mensajes no leídos para este chat
2026-02-13 15:52:33 -03:00
const unreadMessages = mensajes . filter (
( m ) = > m . adID === adId && ! m . isRead && m . receiverID === user . id ,
) ;
2026-01-29 13:43:44 -03:00
if ( unreadMessages . length > 0 ) {
// Optimización visual: actualiza la UI localmente de inmediato
2026-02-13 15:52:33 -03:00
setMensajes ( ( prev ) = >
prev . map ( ( m ) = >
unreadMessages . some ( ( um ) = > um . messageID === m . messageID )
? { . . . m , isRead : true }
: m ,
) ,
) ;
2026-01-29 13:43:44 -03:00
try {
// Crea un array de promesas para todas las llamadas a la API
2026-02-13 15:52:33 -03:00
const markAsReadPromises = unreadMessages . map ( ( m ) = >
m . messageID
? ChatService . markAsRead ( m . messageID )
: Promise . resolve ( ) ,
2026-01-29 13:43:44 -03:00
) ;
// Espera a que TODAS las llamadas al backend terminen
await Promise . all ( markAsReadPromises ) ;
// SOLO DESPUÉS de que el backend confirme, actualizamos el contador global
await fetchUnreadCount ( ) ;
} catch ( error ) {
console . error ( "Error al marcar mensajes como leídos:" , error ) ;
// Opcional: podrías revertir el estado local si la API falla
}
}
// Abrir el modal de chat
setSelectedChat ( { adId , name : adTitle , otherUserId : otherId } ) ;
} else {
alert ( "No tienes mensajes activos para este aviso." ) ;
}
} ;
const handleRemoveFavorite = async ( adId : number ) = > {
if ( ! user ) return ;
try {
await AdsV2Service . removeFavorite ( user . id , adId ) ;
cargarFavoritos ( user . id ) ;
} catch ( error ) {
console . error ( error ) ;
}
} ;
const handleCloseChat = ( ) = > {
setSelectedChat ( null ) ;
if ( user ) {
cargarMensajes ( user . id ) ; // Recarga la lista de mensajes por si llegaron nuevos mientras estaba abierto
}
} ;
if ( ! user ) {
return (
< div className = "container mx-auto px-6 py-24 text-center animate-fade-in-up" >
< div className = "glass p-12 rounded-[3rem] max-w-2xl mx-auto border border-white/5 shadow-2xl" >
< span className = "text-7xl mb-8 block" > 🔒 < / span >
2026-02-13 15:52:33 -03:00
< h2 className = "text-5xl font-black mb-4 uppercase tracking-tighter" >
Á rea Privada
< / h2 >
2026-01-29 13:43:44 -03:00
< p className = "text-gray-400 mb-10 text-lg italic" >
Para gestionar tus publicaciones , primero debes iniciar sesión .
< / p >
2026-02-13 15:52:33 -03:00
< Link
to = "/publicar"
className = "bg-blue-600 hover:bg-blue-500 text-white px-12 py-5 rounded-[2rem] font-bold uppercase tracking-widest transition-all inline-block shadow-lg shadow-blue-600/20"
>
2026-01-29 13:43:44 -03:00
Identificarse
< / Link >
< / div >
< / div >
) ;
}
2026-02-13 15:52:33 -03:00
const totalVisitas = avisos . reduce (
( acc , curr ) = > acc + ( curr . viewsCounter || 0 ) ,
0 ,
) ;
const avisosActivos = avisos . filter ( ( a ) = > a . statusId === 4 ) . length ;
2026-01-29 13:43:44 -03:00
return (
< div className = "container mx-auto px-6 py-12 animate-fade-in-up min-h-screen" >
< header className = "flex flex-col md:flex-row justify-between items-start md:items-end mb-16 gap-8" >
< div >
2026-02-13 15:52:33 -03:00
< h2 className = "text-5xl font-black tracking-tighter uppercase mb-4" >
Mis < span className = "text-blue-500" > Avisos < / span >
< / h2 >
2026-01-29 13:43:44 -03:00
< div className = "flex items-center gap-6" >
< div className = "w-14 h-14 bg-gradient-to-tr from-blue-600 to-cyan-400 rounded-2xl flex items-center justify-center text-white text-xl font-black shadow-xl shadow-blue-600/20" >
{ user . username . charAt ( 0 ) . toUpperCase ( ) }
< / div >
< div >
2026-02-13 15:52:33 -03:00
< span className = "text-white text-xl font-black block leading-none" >
{ user . firstName } { user . lastName }
< / span >
< span className = "text-gray-500 text-[10px] uppercase font-black tracking-[0.3em]" >
{ user . email }
< / span >
2026-01-29 13:43:44 -03:00
< / div >
< / div >
< / div >
< div className = "flex w-full md:w-auto bg-white/5 p-1 rounded-xl md:rounded-2xl border border-white/5 backdrop-blur-xl overflow-x-auto no-scrollbar gap-0.5 md:gap-0" >
2026-02-13 15:52:33 -03:00
{ ( [ "avisos" , "favoritos" , "mensajes" ] as TabType [ ] ) . map ( ( tab ) = > (
2026-01-29 13:43:44 -03:00
< button
key = { tab }
onClick = { ( ) = > setActiveTab ( tab ) }
2026-02-13 15:52:33 -03:00
className = { ` flex-1 md:flex-none px-2.5 md:px-8 py-2 md:py-3 rounded-lg md:rounded-xl text-[9px] md:text-[10px] font-black uppercase tracking-widest transition-all whitespace-nowrap ${ activeTab === tab ? "bg-blue-600 text-white shadow-lg shadow-blue-600/20" : "text-gray-500 hover:text-white" } ` }
2026-01-29 13:43:44 -03:00
>
2026-02-13 15:52:33 -03:00
{ tab === "avisos"
? "📦 Mis Avisos"
: tab === "favoritos"
? "⭐ Favoritos"
: "💬 Mensajes" }
2026-01-29 13:43:44 -03:00
< / button >
) ) }
< / div >
< / header >
< div className = "animate-fade-in space-y-8 md:space-y-12" >
2026-02-13 15:52:33 -03:00
{ activeTab === "avisos" && (
2026-01-29 13:43:44 -03:00
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-6 mb-8 md:mb-12" >
2026-02-13 15:52:33 -03:00
< MetricCard
label = "Visualizaciones"
value = { totalVisitas }
icon = "👁️"
/ >
2026-01-29 13:43:44 -03:00
< MetricCard label = "Activos" value = { avisosActivos } icon = "✅" / >
< MetricCard label = "Favoritos" value = { favoritos . length } icon = "⭐" / >
< / div >
) }
{ loading ? (
< div className = "flex justify-center p-24" >
< div className = "animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500" > < / div >
< / div >
) : (
< >
2026-02-13 15:52:33 -03:00
{ activeTab === "avisos" && (
2026-01-29 13:43:44 -03:00
< div className = "space-y-6" >
2026-02-13 15:52:33 -03:00
{ avisos . filter ( ( a ) = > a . statusId !== 9 ) . length === 0 ? (
2026-01-29 13:43:44 -03:00
< div className = "glass p-12 md:p-24 rounded-3xl md:rounded-[3rem] text-center border-dashed border-2 border-white/10" >
< span className = "text-4xl md:text-5xl mb-6 block" > 📂 < / span >
2026-02-13 15:52:33 -03:00
< h3 className = "text-xl md:text-3xl font-bold text-gray-500 uppercase tracking-tighter" >
No tienes avisos
< / h3 >
< Link
to = "/publicar"
className = "mt-8 text-blue-400 font-black uppercase text-xs tracking-widest inline-block border-b border-blue-400 pb-1"
>
Crear mi primer aviso
< / Link >
2026-01-29 13:43:44 -03:00
< / div >
) : (
2026-02-13 15:52:33 -03:00
avisos
. filter ( ( a ) = > a . statusId !== 9 )
. map ( ( av , index ) = > {
const hasMessages = mensajes . some (
( m ) = > m . adID === av . id ,
) ;
const hasUnread = mensajes . some (
( m ) = >
m . adID === av . id &&
! m . isRead &&
m . receiverID === user . id ,
) ;
return (
// 'relative z-index' dinámico
// Esto permite que el dropdown se salga de la tarjeta sin cortarse.
// Usamos un z-index decreciente para que los dropdowns de arriba tapen a las tarjetas de abajo.
< div
key = { av . id }
className = "glass p-6 rounded-[2.5rem] flex flex-col md:flex-row items-center gap-8 border border-white/5 hover:border-blue-500/20 transition-all relative"
style = { { zIndex : 50 - index } }
>
< div className = "w-full md:w-64 h-40 bg-gray-900 rounded-3xl overflow-hidden relative flex-shrink-0 shadow-xl" >
< img
src = { getImageUrl ( av . image ) }
className = "w-full h-full object-cover"
alt = { ` ${ av . brandName } ${ av . versionName } ` }
/ >
< div className = "absolute top-3 left-3 bg-black/60 backdrop-blur-md px-2 py-1 rounded-lg border border-white/10" >
< span className = "text-[9px] font-bold text-white" >
# { av . id }
< / span >
< / div >
2026-01-29 13:43:44 -03:00
< / div >
2026-02-13 15:52:33 -03:00
< div className = "flex-1 w-full text-center md:text-left" >
< div className = "mb-3" >
< h3 className = "text-2xl font-black text-white uppercase tracking-tighter truncate max-w-md" >
{ av . brandName } { av . versionName }
< / h3 >
< span className = "text-blue-400 font-bold text-lg" >
2026-02-19 19:47:13 -03:00
{ formatCurrency ( av . price , av . currency ) }
2026-02-13 15:52:33 -03:00
< / span >
2026-01-29 13:43:44 -03:00
< / div >
2026-02-13 15:52:33 -03:00
< div className = "flex flex-wrap gap-3 justify-center md:justify-start" >
< div className = "bg-white/5 border border-white/5 px-3 py-1.5 rounded-lg flex items-center gap-2" >
< span className = "text-[10px] text-gray-500 font-bold uppercase" >
Año
< / span >
< span className = "text-xs text-white font-bold" >
{ av . year }
< / span >
< / div >
< div className = "bg-white/5 border border-white/5 px-3 py-1.5 rounded-lg flex items-center gap-2" >
< span className = "text-[10px] text-gray-500 font-bold uppercase" >
Visitas
< / span >
< span className = "text-xs text-white font-bold" >
{ av . viewsCounter || 0 }
< / span >
< / div >
{ av . isFeatured && (
< div className = "bg-blue-600/20 border border-blue-500/30 px-3 py-1.5 rounded-lg" >
< span className = "text-[9px] text-blue-300 font-black uppercase tracking-widest" >
⭐ Destacado
< / span >
< / div >
) }
2026-01-29 13:43:44 -03:00
< / div >
2026-02-13 15:52:33 -03:00
< / div >
< div className = "w-full md:w-auto flex flex-col gap-3 min-w-[180px]" >
{ /* CASO 1: BORRADOR (1) -> Botón de Pagar */ }
{ av . statusId === AD_STATUSES . DRAFT && (
< Link
to = { ` /publicar?edit= ${ av . id } ` }
className = "bg-blue-600 hover:bg-blue-500 text-white text-xs font-black uppercase tracking-widest rounded-xl px-4 py-3 text-center shadow-lg shadow-blue-600/20 transition-all"
>
Continuar Pago ➔
< / Link >
) }
{ /* CASO 2: PAGO PENDIENTE (2) -> Botón de Verificar */ }
{ av . statusId === AD_STATUSES . PAYMENT_PENDING && (
< div className = "flex flex-col gap-2" >
< div className = "bg-amber-500/10 border border-amber-500/20 text-amber-400 px-4 py-2 rounded-xl text-center" >
< span className = "block text-[10px] font-black uppercase tracking-widest" >
⏳ Pago Pendiente
< / span >
< / div >
< button
onClick = { ( ) = > handleVerifyPayment ( av . id ) }
className = "bg-white/5 hover:bg-white/10 text-white text-[10px] font-bold uppercase tracking-widest px-4 py-2.5 rounded-xl border border-white/10 transition-all hover:border-white/20 flex items-center justify-center gap-2"
>
🔄 Verificar Ahora
< / button >
2026-01-29 13:43:44 -03:00
< / div >
) }
2026-02-13 15:52:33 -03:00
{ /* CASO 3: EN REVISIÓN (3) -> Cartel informativo */ }
{ av . statusId === AD_STATUSES . MODERATION_PENDING && (
< div className = "bg-blue-500/10 border border-blue-500/20 text-blue-300 px-4 py-3 rounded-xl text-center" >
< span className = "block text-[10px] font-black uppercase tracking-widest" >
⏳ En Revisión
< / span >
< span className = "text-[8px] opacity-70" >
No editable
< / span >
2026-01-29 13:43:44 -03:00
< / div >
2026-02-13 15:52:33 -03:00
) }
2026-01-29 13:43:44 -03:00
2026-02-13 15:52:33 -03:00
{ /* CASO 4: VENCIDO (8) -> Botón de Republicar */ }
{ av . statusId === AD_STATUSES . EXPIRED && (
< div className = "flex flex-col gap-2" >
< div className = "bg-gray-500/10 border border-gray-500/20 text-gray-400 px-4 py-2 rounded-xl text-center" >
< span className = "block text-[10px] font-black uppercase tracking-widest" >
⛔ Finalizado
< / span >
< / div >
< Link
to = { ` /publicar?edit= ${ av . id } ` }
className = "bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-bold uppercase tracking-widest px-4 py-2.5 rounded-xl border border-transparent shadow-lg shadow-blue-600/20 transition-all flex items-center justify-center gap-2"
>
🔄 Republicar
< / Link >
< / div >
) }
2026-01-29 13:43:44 -03:00
2026-02-13 15:52:33 -03:00
{ /* CASO 5: ACTIVOS/PAUSADOS/OTROS (StatusDropdown) */ }
{ av . statusId !== AD_STATUSES . DRAFT &&
av . statusId !== AD_STATUSES . PAYMENT_PENDING &&
av . statusId !== AD_STATUSES . MODERATION_PENDING &&
av . statusId !== AD_STATUSES . EXPIRED &&
av . statusId !== AD_STATUSES . REJECTED && (
< StatusDropdown
currentStatus = {
av . statusId || AD_STATUSES . ACTIVE
}
onChange = { ( newStatus ) = >
initiateStatusChange ( av . id , newStatus )
}
/ >
) }
{ /* --- NUEVO: CASO 6: RECHAZADO --- */ }
{ av . statusId === AD_STATUSES . REJECTED && (
< div className = "flex flex-col gap-2" >
< div className = "bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center" >
< span className = "block text-[10px] font-black uppercase tracking-widest" >
❌ Aviso Rechazado
< / span >
2026-02-16 18:21:10 -03:00
< span className = "text-[10px] opacity-70" >
2026-02-13 15:52:33 -03:00
Revisa los motivos en tu email y edita para
corregir
< / span >
< / div >
< Link
to = { ` /publicar?edit= ${ av . id } ` }
className = "bg-white/10 hover:bg-white/20 text-white text-[10px] font-bold uppercase tracking-widest px-4 py-2.5 rounded-xl border border-white/10 transition-all flex items-center justify-center gap-2"
>
✏ ️ Corregir Aviso
< / Link >
2026-01-29 13:43:44 -03:00
< / div >
2026-02-13 15:52:33 -03:00
) }
{ /* BOTONES COMUNES (Siempre visibles) */ }
< div className = "grid grid-cols-1 gap-2 mt-1" >
2026-01-29 13:43:44 -03:00
< Link
2026-02-13 15:52:33 -03:00
to = { ` /vehiculo/ ${ av . id } ` }
className = "bg-white/5 hover:bg-white/10 text-gray-300 hover:text-white border border-white/5 px-4 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest text-center transition-all flex items-center justify-center gap-2"
2026-01-29 13:43:44 -03:00
>
2026-02-13 15:52:33 -03:00
< span > 👁 ️ Ver Detalle < / span >
2026-01-29 13:43:44 -03:00
< / Link >
2026-02-13 15:52:33 -03:00
{ hasMessages && (
< button
onClick = { ( ) = >
openChatForAd (
av . id ,
` ${ av . brandName } ${ av . versionName } ` ,
)
}
className = "relative bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white border border-white/5 px-4 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center justify-center gap-2 transition-all"
>
💬 Mensajes
{ hasUnread && (
< span className = "absolute top-3 right-3 w-2 h-2 bg-red-500 rounded-full animate-pulse shadow-lg shadow-red-500/50" > < / span >
) }
< / button >
) }
< / div >
2026-01-29 13:43:44 -03:00
< / div >
< / div >
2026-02-13 15:52:33 -03:00
) ;
} )
2026-01-29 13:43:44 -03:00
) }
< / div >
) }
2026-02-13 15:52:33 -03:00
{ activeTab === "favoritos" && (
2026-01-29 13:43:44 -03:00
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" >
{ favoritos . length === 0 ? (
< div className = "col-span-full glass p-12 md:p-24 rounded-3xl md:rounded-[3rem] text-center border-dashed border-2 border-white/10" >
< span className = "text-4xl md:text-5xl mb-6 block" > ⭐ < / span >
2026-02-13 15:52:33 -03:00
< h3 className = "text-xl md:text-3xl font-bold text-gray-500 uppercase tracking-tighter" >
No tienes favoritos
< / h3 >
< Link
to = "/explorar"
className = "mt-8 text-blue-400 font-black uppercase text-xs tracking-widest inline-block border-b border-blue-400 pb-1"
>
Explorar vehículos
< / Link >
2026-01-29 13:43:44 -03:00
< / div >
) : (
favoritos . map ( ( fav ) = > (
2026-02-13 15:52:33 -03:00
< div
key = { fav . id }
className = "glass rounded-[2rem] overflow-hidden border border-white/5 flex flex-col group hover:border-blue-500/30 transition-all"
>
2026-01-29 13:43:44 -03:00
< div className = "relative h-48" >
2026-02-13 15:52:33 -03:00
< img
src = { getImageUrl ( fav . image ) }
className = "w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
alt = { ` ${ fav . brandName } ${ fav . versionName } ` }
/ >
< button
onClick = { ( ) = > handleRemoveFavorite ( fav . id ) }
className = "absolute top-4 right-4 w-10 h-10 bg-black/50 backdrop-blur-md rounded-xl flex items-center justify-center text-red-500 hover:bg-red-500 hover:text-white transition-all shadow-xl"
>
×
< / button >
2026-01-29 13:43:44 -03:00
< / div >
< div className = "p-6" >
2026-02-13 15:52:33 -03:00
< h3 className = "text-lg font-black text-white uppercase tracking-tighter mb-1 truncate" >
{ fav . brandName } { fav . versionName }
< / h3 >
< p className = "text-blue-400 font-extrabold text-xl mb-4" >
{ fav . currency } { fav . price . toLocaleString ( ) }
< / p >
< Link
to = { ` /vehiculo/ ${ fav . id } ` }
className = "block w-full bg-blue-600/10 hover:bg-blue-600 text-blue-400 hover:text-white p-3 rounded-xl text-[10px] font-black uppercase tracking-widest text-center transition-all border border-blue-600/20"
>
Ver Detalle
< / Link >
2026-01-29 13:43:44 -03:00
< / div >
< / div >
) )
) }
< / div >
) }
2026-02-13 15:52:33 -03:00
{ activeTab === "mensajes" && (
2026-01-29 13:43:44 -03:00
< div className = "space-y-4" >
{ mensajes . length === 0 ? (
< div className = "glass p-12 md:p-24 rounded-3xl md:rounded-[3rem] text-center border-dashed border-2 border-white/10" >
< span className = "text-4xl md:text-5xl mb-6 block" > 💬 < / span >
2026-02-13 15:52:33 -03:00
< h3 className = "text-xl md:text-3xl font-bold text-gray-500 uppercase tracking-tighter" >
No tienes mensajes
< / h3 >
< p className = "text-gray-600 mt-2 max-w-sm mx-auto italic text-lg" >
Los moderadores te contactarán por aquí si es necesario .
< / p >
2026-01-29 13:43:44 -03:00
< / div >
) : (
2026-02-13 15:52:33 -03:00
Object . values (
mensajes . reduce ( ( acc : any , curr ) = > {
const key = curr . adID ;
if ( ! acc [ key ] )
acc [ key ] = { msg : curr , count : 0 , unread : false } ;
acc [ key ] . count ++ ;
if ( ! curr . isRead && curr . receiverID === user . id )
acc [ key ] . unread = true ;
if (
new Date ( curr . sentAt ! ) > new Date ( acc [ key ] . msg . sentAt ! )
)
acc [ key ] . msg = curr ;
return acc ;
} , { } ) ,
) . map ( ( item : any ) = > {
const aviso = avisos . find ( ( a ) = > a . id === item . msg . adID ) ;
const tituloAviso = aviso
? ` ${ aviso . brandName } ${ aviso . versionName } `
: ` Aviso # ${ item . msg . adID } ` ;
2026-01-29 13:43:44 -03:00
return (
< div
key = { item . msg . adID }
2026-02-13 15:52:33 -03:00
onClick = { ( ) = >
openChatForAd ( item . msg . adID , tituloAviso )
}
2026-01-29 13:43:44 -03:00
className = "glass p-6 rounded-2xl flex items-center gap-6 border border-white/5 hover:border-blue-500/30 transition-all cursor-pointer group"
>
2026-02-13 15:52:33 -03:00
< div className = "w-16 h-16 bg-blue-600/20 rounded-full flex items-center justify-center text-2xl group-hover:scale-110 transition-transform" >
🛡 ️
< / div >
2026-01-29 13:43:44 -03:00
< div className = "flex-1" >
< div className = "flex justify-between items-center mb-1" >
< h4 className = "font-black uppercase tracking-tighter text-white" >
{ tituloAviso }
< / h4 >
2026-02-13 15:52:33 -03:00
< span className = "text-[10px] text-gray-500 font-bold uppercase" >
{ parseUTCDate (
item . msg . sentAt ! ,
) . toLocaleDateString ( "es-AR" , {
timeZone : "America/Argentina/Buenos_Aires" ,
hour12 : false ,
} ) }
< / span >
2026-01-29 13:43:44 -03:00
< / div >
< p className = "text-sm text-gray-400 line-clamp-1" >
2026-02-13 15:52:33 -03:00
{ item . msg . senderID === user . id ? "Tú: " : "" }
{ item . msg . messageText }
2026-01-29 13:43:44 -03:00
< / p >
< / div >
{ item . unread && (
< div className = "w-3 h-3 bg-red-500 rounded-full shadow-lg shadow-red-500/50" > < / div >
) }
< / div >
2026-02-13 15:52:33 -03:00
) ;
2026-01-29 13:43:44 -03:00
} )
) }
< / div >
) }
< / >
) }
< / div >
{ selectedChat && user && (
< ChatModal
isOpen = { ! ! selectedChat }
onClose = { handleCloseChat }
adId = { selectedChat . adId }
adTitle = { selectedChat . name }
sellerId = { selectedChat . otherUserId }
currentUserId = { user . id }
/ >
) }
{ /* MODAL DE CONFIRMACIÓN */ }
< ConfirmationModal
isOpen = { modalConfig . isOpen }
title = { modalConfig . title }
message = { modalConfig . message }
onConfirm = { confirmStatusChange }
onCancel = { ( ) = > setModalConfig ( { . . . modalConfig , isOpen : false } ) }
isDanger = { modalConfig . isDanger }
2026-02-13 15:52:33 -03:00
confirmText = {
modalConfig . newStatus === AD_STATUSES . SOLD
? "¡Sí, vendido!"
: "Confirmar"
}
2026-01-29 13:43:44 -03:00
/ >
< / div >
) ;
}
// DROPDOWN DE ESTADO
2026-02-13 15:52:33 -03:00
function StatusDropdown ( {
currentStatus ,
onChange ,
} : {
currentStatus : number ;
onChange : ( val : number ) = > void ;
} ) {
2026-01-29 13:43:44 -03:00
const [ isOpen , setIsOpen ] = useState ( false ) ;
const wrapperRef = useRef < HTMLDivElement > ( null ) ;
// Fallback seguro si currentStatus no tiene config
const currentConfig = STATUS_CONFIG [ currentStatus ] || {
2026-02-13 15:52:33 -03:00
label : "Desconocido" ,
color : "text-gray-400" ,
bg : "bg-gray-500/10" ,
border : "border-gray-500/20" ,
icon : "❓" ,
2026-01-29 13:43:44 -03:00
} ;
const ALLOWED_STATUSES = [
AD_STATUSES . ACTIVE ,
AD_STATUSES . PAUSED ,
AD_STATUSES . RESERVED ,
AD_STATUSES . SOLD ,
2026-02-13 15:52:33 -03:00
AD_STATUSES . DELETED ,
2026-01-29 13:43:44 -03:00
] ;
useEffect ( ( ) = > {
function handleClickOutside ( event : MouseEvent ) {
2026-02-13 15:52:33 -03:00
if (
wrapperRef . current &&
! wrapperRef . current . contains ( event . target as Node )
) {
2026-01-29 13:43:44 -03:00
setIsOpen ( false ) ;
}
}
document . addEventListener ( "mousedown" , handleClickOutside ) ;
return ( ) = > document . removeEventListener ( "mousedown" , handleClickOutside ) ;
} , [ ] ) ;
return (
< div className = "relative w-full" ref = { wrapperRef } >
< button
onClick = { ( ) = > setIsOpen ( ! isOpen ) }
className = { ` w-full flex items-center justify-between px-4 py-3 rounded-xl border ${ currentConfig . bg } ${ currentConfig . border } ${ currentConfig . color } transition-all hover:brightness-110 ` }
>
< div className = "flex items-center gap-2" >
< span > { currentConfig . icon } < / span >
2026-02-13 15:52:33 -03:00
< span className = "text-[10px] font-black uppercase tracking-widest" >
{ currentConfig . label }
< / span >
2026-01-29 13:43:44 -03:00
< / div >
< span className = "text-xs" > ▼ < / span >
< / button >
{ isOpen && (
< div className = "absolute z-[100] w-full mt-2 bg-[#1a1d24] border border-white/10 rounded-xl shadow-2xl overflow-hidden animate-fade-in ring-1 ring-white/5" >
{ ALLOWED_STATUSES . map ( ( statusId ) = > {
const config = STATUS_CONFIG [ statusId ] ;
// PROTECCIÓN CONTRA EL ERROR DE "UNDEFINED"
if ( ! config ) return null ;
return (
< button
key = { statusId }
2026-02-13 15:52:33 -03:00
onClick = { ( ) = > {
onChange ( statusId ) ;
setIsOpen ( false ) ;
} }
className = { ` w-full text-left px-4 py-3 text-[10px] font-bold uppercase tracking-widest hover:bg-white/5 transition-colors border-b border-white/5 last:border-0 flex items-center gap-2 ${ statusId === currentStatus ? "text-white bg-white/5" : "text-gray-400" } ` }
2026-01-29 13:43:44 -03:00
>
< span className = "text-sm" > { config . icon } < / span >
{ config . label }
< / button >
) ;
} ) }
< / div >
) }
< / div >
) ;
}
2026-02-13 15:52:33 -03:00
function MetricCard ( {
label ,
value ,
icon ,
} : {
label : string ;
value : any ;
icon : string ;
} ) {
2026-01-29 13:43:44 -03:00
return (
< div className = "glass p-4 md:p-8 rounded-2xl md:rounded-[2rem] border border-white/5 flex flex-row items-center gap-4 md:gap-6 text-left" >
2026-02-13 15:52:33 -03:00
< div className = "w-12 h-12 md:w-16 md:h-16 bg-white/5 rounded-xl md:rounded-2xl flex items-center justify-center text-xl md:text-3xl shadow-inner border border-white/5" >
{ icon }
< / div >
2026-01-29 13:43:44 -03:00
< div >
2026-02-13 15:52:33 -03:00
< span className = "text-2xl md:text-3xl font-black text-white tracking-tighter block leading-none mb-1" >
{ value . toLocaleString ( ) }
< / span >
< span className = "text-[9px] md:text-[10px] font-black uppercase tracking-widest text-gray-500 block leading-tight" >
{ label }
< / span >
2026-01-29 13:43:44 -03:00
< / div >
< / div >
) ;
2026-02-13 15:52:33 -03:00
}