Files
MotoresArgentinosV2/Frontend/src/pages/MisAvisosPage.tsx

802 lines
34 KiB
TypeScript
Raw Normal View History

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";
import { getImageUrl, parseUTCDate } from "../utils/app.utils";
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() {
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();
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,
title: "",
message: "",
2026-01-29 13:43:44 -03:00
adId: null,
newStatus: null,
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);
if (res.status === "approved") {
2026-01-29 13:43:44 -03:00
alert("¡Pago confirmado! El aviso pasará a moderación.");
cargarAvisos(user!.id);
} 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);
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) => {
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) {
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) {
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) {
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) {
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) {
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,
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) {
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;
const relatedMsg = mensajes.find((m) => m.adID === adId);
2026-01-29 13:43:44 -03:00
if (relatedMsg) {
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
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
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
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>
<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>
<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>
);
}
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>
<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>
<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">
{(["avisos", "favoritos", "mensajes"] as TabType[]).map((tab) => (
2026-01-29 13:43:44 -03:00
<button
key={tab}
onClick={() => setActiveTab(tab)}
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
>
{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">
{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">
<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>
) : (
<>
{activeTab === "avisos" && (
2026-01-29 13:43:44 -03:00
<div className="space-y-6">
{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>
<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>
) : (
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>
<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">
{av.currency} {av.price.toLocaleString()}
</span>
2026-01-29 13:43:44 -03:00
</div>
<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>
</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>
)}
{/* 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-01-29 13:43:44 -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
{/* 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>
<span className="text-[8px] opacity-70">
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>
)}
{/* BOTONES COMUNES (Siempre visibles) */}
<div className="grid grid-cols-1 gap-2 mt-1">
2026-01-29 13:43:44 -03:00
<Link
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
>
<span>👁 Ver Detalle</span>
2026-01-29 13:43:44 -03:00
</Link>
{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-01-29 13:43:44 -03:00
)}
</div>
)}
{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>
<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) => (
<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">
<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">
<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>
)}
{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>
<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>
) : (
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}
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"
>
<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>
<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">
{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-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}
confirmText={
modalConfig.newStatus === AD_STATUSES.SOLD
? "¡Sí, vendido!"
: "Confirmar"
}
2026-01-29 13:43:44 -03:00
/>
</div>
);
}
// DROPDOWN DE ESTADO
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] || {
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,
AD_STATUSES.DELETED,
2026-01-29 13:43:44 -03:00
];
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
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>
<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}
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>
);
}
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">
<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>
<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>
);
}