Files
MotoresArgentinosV2/Frontend/src/pages/MisAvisosPage.tsx
dmolinari 042cd8c6f1 Feat: Selector de Contacto Independiente y Formato de Precio
- Se divide la selección de medios de contacto entre los datos del usuario, permitiendo mostras el tipo de contacto que prefiera.
- Cuando el precio es igual a 0, se muestra la palabra "Consultar" en lugar de $0 o ARS 0.
2026-02-19 19:47:13 -03:00

802 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { formatCurrency, getImageUrl, parseUTCDate } from "../utils/app.utils";
import { AD_STATUSES, STATUS_CONFIG } from "../constants/adStatuses";
import ConfirmationModal from "../components/ConfirmationModal";
type TabType = "avisos" | "favoritos" | "mensajes";
export default function MisAvisosPage() {
const [activeTab, setActiveTab] = useState<TabType>("avisos");
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);
const [modalConfig, setModalConfig] = useState<{
isOpen: boolean;
title: string;
message: string;
adId: number | null;
newStatus: number | null;
isDanger: boolean;
}>({
isOpen: false,
title: "",
message: "",
adId: null,
newStatus: null,
isDanger: false,
});
// 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") {
alert("¡Pago confirmado! El aviso pasará a moderación.");
cargarAvisos(user!.id);
} else if (res.status === "rejected") {
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);
}
// 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?";
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?";
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.";
}
// 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?';
}
// 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?";
}
// 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.";
}
setModalConfig({
isOpen: true,
title,
message,
adId,
newStatus,
isDanger,
});
};
// 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");
}
};
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);
if (relatedMsg) {
const otherId =
relatedMsg.senderID === user.id
? relatedMsg.receiverID
: relatedMsg.senderID;
// Identificar mensajes no leídos para este chat
const unreadMessages = mensajes.filter(
(m) => m.adID === adId && !m.isRead && m.receiverID === user.id,
);
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,
),
);
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(),
);
// 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>
<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"
>
Identificarse
</Link>
</div>
</div>
);
}
const totalVisitas = avisos.reduce(
(acc, curr) => acc + (curr.viewsCounter || 0),
0,
);
const avisosActivos = avisos.filter((a) => a.statusId === 4).length;
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>
<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>
</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) => (
<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"}`}
>
{tab === "avisos"
? "📦 Mis Avisos"
: tab === "favoritos"
? "⭐ Favoritos"
: "💬 Mensajes"}
</button>
))}
</div>
</header>
<div className="animate-fade-in space-y-8 md:space-y-12">
{activeTab === "avisos" && (
<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="👁️"
/>
<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" && (
<div className="space-y-6">
{avisos.filter((a) => a.statusId !== 9).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 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>
</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>
</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">
{formatCurrency(av.price, av.currency)}
</span>
</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>
)}
</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>
</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>
</div>
)}
{/* 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>
)}
{/* 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-[10px] 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>
</div>
)}
{/* BOTONES COMUNES (Siempre visibles) */}
<div className="grid grid-cols-1 gap-2 mt-1">
<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"
>
<span>👁 Ver Detalle</span>
</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>
</div>
</div>
);
})
)}
</div>
)}
{activeTab === "favoritos" && (
<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>
</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"
>
<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>
</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>
</div>
</div>
))
)}
</div>
)}
{activeTab === "mensajes" && (
<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>
</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}`;
return (
<div
key={item.msg.adID}
onClick={() =>
openChatForAd(item.msg.adID, tituloAviso)
}
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>
<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>
</div>
<p className="text-sm text-gray-400 line-clamp-1">
{item.msg.senderID === user.id ? "Tú: " : ""}
{item.msg.messageText}
</p>
</div>
{item.unread && (
<div className="w-3 h-3 bg-red-500 rounded-full shadow-lg shadow-red-500/50"></div>
)}
</div>
);
})
)}
</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"
}
/>
</div>
);
}
// DROPDOWN DE ESTADO
function StatusDropdown({
currentStatus,
onChange,
}: {
currentStatus: number;
onChange: (val: number) => void;
}) {
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: "❓",
};
const ALLOWED_STATUSES = [
AD_STATUSES.ACTIVE,
AD_STATUSES.PAUSED,
AD_STATUSES.RESERVED,
AD_STATUSES.SOLD,
AD_STATUSES.DELETED,
];
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
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>
</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"}`}
>
<span className="text-sm">{config.icon}</span>
{config.label}
</button>
);
})}
</div>
)}
</div>
);
}
function MetricCard({
label,
value,
icon,
}: {
label: string;
value: any;
icon: string;
}) {
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>
<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>
</div>
</div>
);
}