Fix: Estado Rechazado Y Acciones del Lado del Usuario
This commit is contained in:
@@ -657,6 +657,13 @@ public class AdsV2Controller : ControllerBase
|
|||||||
ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo;
|
ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo;
|
||||||
// Nota: IsFeatured y otros campos sensibles se manejan por separado (pago/admin)
|
// Nota: IsFeatured y otros campos sensibles se manejan por separado (pago/admin)
|
||||||
|
|
||||||
|
// LÓGICA DE ESTADO TRAS RECHAZO
|
||||||
|
if (!IsUserAdmin() && ad.StatusID == (int)AdStatusEnum.Rejected)
|
||||||
|
{
|
||||||
|
// Si estaba rechazado y el dueño lo edita, vuelve a revisión.
|
||||||
|
ad.StatusID = (int)AdStatusEnum.ModerationPending;
|
||||||
|
}
|
||||||
|
|
||||||
// 📝 AUDITORÍA
|
// 📝 AUDITORÍA
|
||||||
var adBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? "";
|
var adBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? "";
|
||||||
_context.AuditLogs.Add(new AuditLog
|
_context.AuditLogs.Add(new AuditLog
|
||||||
@@ -770,11 +777,17 @@ public class AdsV2Controller : ControllerBase
|
|||||||
return BadRequest("Debes completar el pago para activar este aviso.");
|
return BadRequest("Debes completar el pago para activar este aviso.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. NUEVO: No tocar si está en moderación
|
// 2. No tocar si está en moderación
|
||||||
if (ad.StatusID == (int)AdStatusEnum.ModerationPending)
|
if (ad.StatusID == (int)AdStatusEnum.ModerationPending)
|
||||||
{
|
{
|
||||||
return BadRequest("El aviso está en revisión. Espera la aprobación del administrador.");
|
return BadRequest("El aviso está en revisión. Espera la aprobación del administrador.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Bloquear si está RECHAZADO
|
||||||
|
if (ad.StatusID == (int)AdStatusEnum.Rejected)
|
||||||
|
{
|
||||||
|
return BadRequest("Este aviso fue rechazado. Debes editarlo y corregirlo para que sea revisado nuevamente.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar estados destino permitidos para el usuario
|
// Validar estados destino permitidos para el usuario
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { AdsV2Service, type AdListingDto } from '../services/ads.v2.service';
|
import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service";
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { ChatService, type ChatMessage } from '../services/chat.service';
|
import { ChatService, type ChatMessage } from "../services/chat.service";
|
||||||
import ChatModal from '../components/ChatModal';
|
import ChatModal from "../components/ChatModal";
|
||||||
import { getImageUrl, parseUTCDate } from '../utils/app.utils';
|
import { getImageUrl, parseUTCDate } from "../utils/app.utils";
|
||||||
import { AD_STATUSES, STATUS_CONFIG } from '../constants/adStatuses';
|
import { AD_STATUSES, STATUS_CONFIG } from "../constants/adStatuses";
|
||||||
import ConfirmationModal from '../components/ConfirmationModal';
|
import ConfirmationModal from "../components/ConfirmationModal";
|
||||||
|
|
||||||
type TabType = 'avisos' | 'favoritos' | 'mensajes';
|
type TabType = "avisos" | "favoritos" | "mensajes";
|
||||||
|
|
||||||
export default function MisAvisosPage() {
|
export default function MisAvisosPage() {
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('avisos');
|
const [activeTab, setActiveTab] = useState<TabType>("avisos");
|
||||||
const [avisos, setAvisos] = useState<AdListingDto[]>([]);
|
const [avisos, setAvisos] = useState<AdListingDto[]>([]);
|
||||||
const [favoritos, setFavoritos] = useState<AdListingDto[]>([]);
|
const [favoritos, setFavoritos] = useState<AdListingDto[]>([]);
|
||||||
const [mensajes, setMensajes] = useState<ChatMessage[]>([]);
|
const [mensajes, setMensajes] = useState<ChatMessage[]>([]);
|
||||||
@@ -19,7 +19,11 @@ export default function MisAvisosPage() {
|
|||||||
|
|
||||||
const { user, fetchUnreadCount } = useAuth();
|
const { user, fetchUnreadCount } = useAuth();
|
||||||
|
|
||||||
const [selectedChat, setSelectedChat] = useState<{ adId: number, name: string, otherUserId: number } | null>(null);
|
const [selectedChat, setSelectedChat] = useState<{
|
||||||
|
adId: number;
|
||||||
|
name: string;
|
||||||
|
otherUserId: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const [modalConfig, setModalConfig] = useState<{
|
const [modalConfig, setModalConfig] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -30,21 +34,21 @@ export default function MisAvisosPage() {
|
|||||||
isDanger: boolean;
|
isDanger: boolean;
|
||||||
}>({
|
}>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
title: '',
|
title: "",
|
||||||
message: '',
|
message: "",
|
||||||
adId: null,
|
adId: null,
|
||||||
newStatus: null,
|
newStatus: null,
|
||||||
isDanger: false
|
isDanger: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Función para forzar chequeo manual desde Gestión
|
// Función para forzar chequeo manual desde Gestión
|
||||||
const handleVerifyPayment = async (adId: number) => {
|
const handleVerifyPayment = async (adId: number) => {
|
||||||
try {
|
try {
|
||||||
const res = await AdsV2Service.checkPaymentStatus(adId);
|
const res = await AdsV2Service.checkPaymentStatus(adId);
|
||||||
if (res.status === 'approved') {
|
if (res.status === "approved") {
|
||||||
alert("¡Pago confirmado! El aviso pasará a moderación.");
|
alert("¡Pago confirmado! El aviso pasará a moderación.");
|
||||||
cargarAvisos(user!.id);
|
cargarAvisos(user!.id);
|
||||||
} else if (res.status === 'rejected') {
|
} else if (res.status === "rejected") {
|
||||||
alert("El pago fue rechazado. Puedes intentar pagar nuevamente.");
|
alert("El pago fue rechazado. Puedes intentar pagar nuevamente.");
|
||||||
cargarAvisos(user!.id); // Debería volver a estado Draft/1
|
cargarAvisos(user!.id); // Debería volver a estado Draft/1
|
||||||
} else {
|
} else {
|
||||||
@@ -58,42 +62,47 @@ export default function MisAvisosPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
cargarMensajes(user.id);
|
cargarMensajes(user.id);
|
||||||
if (activeTab === 'avisos') cargarAvisos(user.id);
|
if (activeTab === "avisos") cargarAvisos(user.id);
|
||||||
else if (activeTab === 'favoritos') cargarFavoritos(user.id);
|
else if (activeTab === "favoritos") cargarFavoritos(user.id);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user?.id, activeTab]);
|
}, [user?.id, activeTab]);
|
||||||
|
|
||||||
const initiateStatusChange = (adId: number, newStatus: number) => {
|
const initiateStatusChange = (adId: number, newStatus: number) => {
|
||||||
let title = 'Cambiar Estado';
|
let title = "Cambiar Estado";
|
||||||
let message = '¿Estás seguro de realizar esta acción?';
|
let message = "¿Estás seguro de realizar esta acción?";
|
||||||
let isDanger = false;
|
let isDanger = false;
|
||||||
|
|
||||||
// 1. ELIMINAR
|
// 1. ELIMINAR
|
||||||
if (newStatus === AD_STATUSES.DELETED) {
|
if (newStatus === AD_STATUSES.DELETED) {
|
||||||
title = '¿Eliminar Aviso?';
|
title = "¿Eliminar Aviso?";
|
||||||
message = 'Esta acción eliminará el aviso permanentemente. No se puede deshacer.\n\n¿Estás seguro de continuar?';
|
message =
|
||||||
|
"Esta acción eliminará el aviso permanentemente. No se puede deshacer.\n\n¿Estás seguro de continuar?";
|
||||||
isDanger = true;
|
isDanger = true;
|
||||||
}
|
}
|
||||||
// 2. PAUSAR
|
// 2. PAUSAR
|
||||||
else if (newStatus === AD_STATUSES.PAUSED) {
|
else if (newStatus === AD_STATUSES.PAUSED) {
|
||||||
title = 'Pausar Publicación';
|
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.';
|
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
|
// 3. VENDIDO
|
||||||
else if (newStatus === AD_STATUSES.SOLD) {
|
else if (newStatus === AD_STATUSES.SOLD) {
|
||||||
title = '¡Felicitaciones!';
|
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?';
|
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
|
// 4. RESERVADO
|
||||||
else if (newStatus === AD_STATUSES.RESERVED) {
|
else if (newStatus === AD_STATUSES.RESERVED) {
|
||||||
title = 'Reservar Vehículo';
|
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?';
|
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)
|
// 5. ACTIVAR (Desde Pausado/Reservado)
|
||||||
else if (newStatus === AD_STATUSES.ACTIVE) {
|
else if (newStatus === AD_STATUSES.ACTIVE) {
|
||||||
title = 'Reactivar Aviso';
|
title = "Reactivar Aviso";
|
||||||
message = 'El aviso volverá a estar visible para todos y recibirás consultas nuevamente.';
|
message =
|
||||||
|
"El aviso volverá a estar visible para todos y recibirás consultas nuevamente.";
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalConfig({
|
setModalConfig({
|
||||||
@@ -102,7 +111,7 @@ export default function MisAvisosPage() {
|
|||||||
message,
|
message,
|
||||||
adId,
|
adId,
|
||||||
newStatus,
|
newStatus,
|
||||||
isDanger
|
isDanger,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,7 +125,7 @@ export default function MisAvisosPage() {
|
|||||||
await AdsV2Service.changeStatus(adId, newStatus);
|
await AdsV2Service.changeStatus(adId, newStatus);
|
||||||
if (user) cargarAvisos(user.id);
|
if (user) cargarAvisos(user.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error al actualizar estado');
|
alert("Error al actualizar estado");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,24 +165,35 @@ export default function MisAvisosPage() {
|
|||||||
// Marcar como leídos en DB
|
// Marcar como leídos en DB
|
||||||
const openChatForAd = async (adId: number, adTitle: string) => {
|
const openChatForAd = async (adId: number, adTitle: string) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
const relatedMsg = mensajes.find(m => m.adID === adId);
|
const relatedMsg = mensajes.find((m) => m.adID === adId);
|
||||||
|
|
||||||
if (relatedMsg) {
|
if (relatedMsg) {
|
||||||
const otherId = relatedMsg.senderID === user.id ? relatedMsg.receiverID : relatedMsg.senderID;
|
const otherId =
|
||||||
|
relatedMsg.senderID === user.id
|
||||||
|
? relatedMsg.receiverID
|
||||||
|
: relatedMsg.senderID;
|
||||||
|
|
||||||
// Identificar mensajes no leídos para este chat
|
// Identificar mensajes no leídos para este chat
|
||||||
const unreadMessages = mensajes.filter(m => m.adID === adId && !m.isRead && m.receiverID === user.id);
|
const unreadMessages = mensajes.filter(
|
||||||
|
(m) => m.adID === adId && !m.isRead && m.receiverID === user.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (unreadMessages.length > 0) {
|
if (unreadMessages.length > 0) {
|
||||||
// Optimización visual: actualiza la UI localmente de inmediato
|
// Optimización visual: actualiza la UI localmente de inmediato
|
||||||
setMensajes(prev => prev.map(m =>
|
setMensajes((prev) =>
|
||||||
unreadMessages.some(um => um.messageID === m.messageID) ? { ...m, isRead: true } : m
|
prev.map((m) =>
|
||||||
));
|
unreadMessages.some((um) => um.messageID === m.messageID)
|
||||||
|
? { ...m, isRead: true }
|
||||||
|
: m,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Crea un array de promesas para todas las llamadas a la API
|
// Crea un array de promesas para todas las llamadas a la API
|
||||||
const markAsReadPromises = unreadMessages.map(m =>
|
const markAsReadPromises = unreadMessages.map((m) =>
|
||||||
m.messageID ? ChatService.markAsRead(m.messageID) : Promise.resolve()
|
m.messageID
|
||||||
|
? ChatService.markAsRead(m.messageID)
|
||||||
|
: Promise.resolve(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Espera a que TODAS las llamadas al backend terminen
|
// Espera a que TODAS las llamadas al backend terminen
|
||||||
@@ -181,7 +201,6 @@ export default function MisAvisosPage() {
|
|||||||
|
|
||||||
// SOLO DESPUÉS de que el backend confirme, actualizamos el contador global
|
// SOLO DESPUÉS de que el backend confirme, actualizamos el contador global
|
||||||
await fetchUnreadCount();
|
await fetchUnreadCount();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al marcar mensajes como leídos:", error);
|
console.error("Error al marcar mensajes como leídos:", error);
|
||||||
// Opcional: podrías revertir el estado local si la API falla
|
// Opcional: podrías revertir el estado local si la API falla
|
||||||
@@ -217,11 +236,16 @@ export default function MisAvisosPage() {
|
|||||||
<div className="container mx-auto px-6 py-24 text-center animate-fade-in-up">
|
<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">
|
<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>
|
<span className="text-7xl mb-8 block">🔒</span>
|
||||||
<h2 className="text-5xl font-black mb-4 uppercase tracking-tighter">Área Privada</h2>
|
<h2 className="text-5xl font-black mb-4 uppercase tracking-tighter">
|
||||||
|
Área Privada
|
||||||
|
</h2>
|
||||||
<p className="text-gray-400 mb-10 text-lg italic">
|
<p className="text-gray-400 mb-10 text-lg italic">
|
||||||
Para gestionar tus publicaciones, primero debes iniciar sesión.
|
Para gestionar tus publicaciones, primero debes iniciar sesión.
|
||||||
</p>
|
</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">
|
<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
|
Identificarse
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,219 +253,332 @@ export default function MisAvisosPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalVisitas = avisos.reduce((acc, curr) => acc + (curr.viewsCounter || 0), 0);
|
const totalVisitas = avisos.reduce(
|
||||||
const avisosActivos = avisos.filter(a => a.statusId === 4).length;
|
(acc, curr) => acc + (curr.viewsCounter || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const avisosActivos = avisos.filter((a) => a.statusId === 4).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-6 py-12 animate-fade-in-up min-h-screen">
|
<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">
|
<header className="flex flex-col md:flex-row justify-between items-start md:items-end mb-16 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-5xl font-black tracking-tighter uppercase mb-4">Mis <span className="text-blue-500">Avisos</span></h2>
|
<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="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">
|
<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()}
|
{user.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-white text-xl font-black block leading-none">{user.firstName} {user.lastName}</span>
|
<span className="text-white text-xl font-black block leading-none">
|
||||||
<span className="text-gray-500 text-[10px] uppercase font-black tracking-[0.3em]">{user.email}</span>
|
{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>
|
||||||
</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">
|
<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 => (
|
{(["avisos", "favoritos", "mensajes"] as TabType[]).map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(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'}`}
|
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'}
|
{tab === "avisos"
|
||||||
|
? "📦 Mis Avisos"
|
||||||
|
: tab === "favoritos"
|
||||||
|
? "⭐ Favoritos"
|
||||||
|
: "💬 Mensajes"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="animate-fade-in space-y-8 md:space-y-12">
|
<div className="animate-fade-in space-y-8 md:space-y-12">
|
||||||
|
{activeTab === "avisos" && (
|
||||||
{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">
|
<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="Visualizaciones"
|
||||||
|
value={totalVisitas}
|
||||||
|
icon="👁️"
|
||||||
|
/>
|
||||||
<MetricCard label="Activos" value={avisosActivos} icon="✅" />
|
<MetricCard label="Activos" value={avisosActivos} icon="✅" />
|
||||||
<MetricCard label="Favoritos" value={favoritos.length} icon="⭐" />
|
<MetricCard label="Favoritos" value={favoritos.length} icon="⭐" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex justify-center p-24">
|
<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 className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{activeTab === 'avisos' && (
|
{activeTab === "avisos" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{avisos.filter(a => a.statusId !== 9).length === 0 ? (
|
{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">
|
<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>
|
<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>
|
<h3 className="text-xl md:text-3xl font-bold text-gray-500 uppercase tracking-tighter">
|
||||||
<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>
|
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
avisos.filter(a => a.statusId !== 9).map((av, index) => {
|
avisos
|
||||||
const hasMessages = mensajes.some(m => m.adID === av.id);
|
.filter((a) => a.statusId !== 9)
|
||||||
const hasUnread = mensajes.some(m => m.adID === av.id && !m.isRead && m.receiverID === user.id);
|
.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 (
|
return (
|
||||||
// 'relative z-index' dinámico
|
// 'relative z-index' dinámico
|
||||||
// Esto permite que el dropdown se salga de la tarjeta sin cortarse.
|
// 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.
|
// Usamos un z-index decreciente para que los dropdowns de arriba tapen a las tarjetas de abajo.
|
||||||
<div
|
<div
|
||||||
key={av.id}
|
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"
|
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 }}
|
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">
|
||||||
<div className="w-full md:w-64 h-40 bg-gray-900 rounded-3xl overflow-hidden relative flex-shrink-0 shadow-xl">
|
<img
|
||||||
<img src={getImageUrl(av.image)} className="w-full h-full object-cover" alt={`${av.brandName} ${av.versionName}`} />
|
src={getImageUrl(av.image)}
|
||||||
<div className="absolute top-3 left-3 bg-black/60 backdrop-blur-md px-2 py-1 rounded-lg border border-white/10">
|
className="w-full h-full object-cover"
|
||||||
<span className="text-[9px] font-bold text-white">#{av.id}</span>
|
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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 w-full text-center md:text-left">
|
<div className="flex-1 w-full text-center md:text-left">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h3 className="text-2xl font-black text-white uppercase tracking-tighter truncate max-w-md">
|
<h3 className="text-2xl font-black text-white uppercase tracking-tighter truncate max-w-md">
|
||||||
{av.brandName} {av.versionName}
|
{av.brandName} {av.versionName}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-blue-400 font-bold text-lg">{av.currency} {av.price.toLocaleString()}</span>
|
<span className="text-blue-400 font-bold text-lg">
|
||||||
|
{av.currency} {av.price.toLocaleString()}
|
||||||
|
</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>
|
||||||
<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]">
|
<div className="w-full md:w-auto flex flex-col gap-3 min-w-[180px]">
|
||||||
|
{/* CASO 1: BORRADOR (1) -> Botón de Pagar */}
|
||||||
{/* CASO 1: BORRADOR (1) -> Botón de Pagar */}
|
{av.statusId === AD_STATUSES.DRAFT && (
|
||||||
{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
|
<Link
|
||||||
to={`/publicar?edit=${av.id}`}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
🔄 Republicar
|
Continuar Pago ➔
|
||||||
</Link>
|
</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 && (
|
|
||||||
<StatusDropdown
|
|
||||||
currentStatus={av.statusId || AD_STATUSES.ACTIVE}
|
|
||||||
onChange={(newStatus) => initiateStatusChange(av.id, newStatus)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* BOTONES COMUNES (Siempre visibles) */}
|
{/* CASO 2: PAGO PENDIENTE (2) -> Botón de Verificar */}
|
||||||
<div className="grid grid-cols-1 gap-2 mt-1">
|
{av.statusId === AD_STATUSES.PAYMENT_PENDING && (
|
||||||
<Link
|
<div className="flex flex-col gap-2">
|
||||||
to={`/vehiculo/${av.id}`}
|
<div className="bg-amber-500/10 border border-amber-500/20 text-amber-400 px-4 py-2 rounded-xl text-center">
|
||||||
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 className="block text-[10px] font-black uppercase tracking-widest">
|
||||||
>
|
⏳ Pago Pendiente
|
||||||
<span>👁️ Ver Detalle</span>
|
</span>
|
||||||
</Link>
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasMessages && (
|
{/* CASO 3: EN REVISIÓN (3) -> Cartel informativo */}
|
||||||
<button
|
{av.statusId === AD_STATUSES.MODERATION_PENDING && (
|
||||||
onClick={() => openChatForAd(av.id, `${av.brandName} ${av.versionName}`)}
|
<div className="bg-blue-500/10 border border-blue-500/20 text-blue-300 px-4 py-3 rounded-xl text-center">
|
||||||
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"
|
<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-[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>
|
||||||
|
</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"
|
||||||
>
|
>
|
||||||
💬 Mensajes
|
<span>👁️ Ver Detalle</span>
|
||||||
{hasUnread && (
|
</Link>
|
||||||
<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>
|
|
||||||
)}
|
{hasMessages && (
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'favoritos' && (
|
{activeTab === "favoritos" && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{favoritos.length === 0 ? (
|
{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">
|
<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>
|
<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>
|
<h3 className="text-xl md:text-3xl font-bold text-gray-500 uppercase tracking-tighter">
|
||||||
<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>
|
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
favoritos.map((fav) => (
|
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
|
||||||
|
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">
|
<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}`} />
|
<img
|
||||||
<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>
|
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>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h3 className="text-lg font-black text-white uppercase tracking-tighter mb-1 truncate">{fav.brandName} {fav.versionName}</h3>
|
<h3 className="text-lg font-black text-white uppercase tracking-tighter mb-1 truncate">
|
||||||
<p className="text-blue-400 font-extrabold text-xl mb-4">{fav.currency} {fav.price.toLocaleString()}</p>
|
{fav.brandName} {fav.versionName}
|
||||||
<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>
|
</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>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -449,49 +586,74 @@ export default function MisAvisosPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'mensajes' && (
|
{activeTab === "mensajes" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{mensajes.length === 0 ? (
|
{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">
|
<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>
|
<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>
|
<h3 className="text-xl md:text-3xl font-bold text-gray-500 uppercase tracking-tighter">
|
||||||
<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>
|
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
Object.values(mensajes.reduce((acc: any, curr) => {
|
Object.values(
|
||||||
const key = curr.adID;
|
mensajes.reduce((acc: any, curr) => {
|
||||||
if (!acc[key]) acc[key] = { msg: curr, count: 0, unread: false };
|
const key = curr.adID;
|
||||||
acc[key].count++;
|
if (!acc[key])
|
||||||
if (!curr.isRead && curr.receiverID === user.id) acc[key].unread = true;
|
acc[key] = { msg: curr, count: 0, unread: false };
|
||||||
if (new Date(curr.sentAt!) > new Date(acc[key].msg.sentAt!)) acc[key].msg = curr;
|
acc[key].count++;
|
||||||
return acc;
|
if (!curr.isRead && curr.receiverID === user.id)
|
||||||
}, {})).map((item: any) => {
|
acc[key].unread = true;
|
||||||
const aviso = avisos.find(a => a.id === item.msg.adID);
|
if (
|
||||||
const tituloAviso = aviso ? `${aviso.brandName} ${aviso.versionName}` : `Aviso #${item.msg.adID}`;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.msg.adID}
|
key={item.msg.adID}
|
||||||
onClick={() => openChatForAd(item.msg.adID, tituloAviso)}
|
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"
|
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="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-1">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<h4 className="font-black uppercase tracking-tighter text-white">
|
<h4 className="font-black uppercase tracking-tighter text-white">
|
||||||
{tituloAviso}
|
{tituloAviso}
|
||||||
</h4>
|
</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>
|
<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>
|
</div>
|
||||||
<p className="text-sm text-gray-400 line-clamp-1">
|
<p className="text-sm text-gray-400 line-clamp-1">
|
||||||
{item.msg.senderID === user.id ? 'Tú: ' : ''}{item.msg.messageText}
|
{item.msg.senderID === user.id ? "Tú: " : ""}
|
||||||
|
{item.msg.messageText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{item.unread && (
|
{item.unread && (
|
||||||
<div className="w-3 h-3 bg-red-500 rounded-full shadow-lg shadow-red-500/50"></div>
|
<div className="w-3 h-3 bg-red-500 rounded-full shadow-lg shadow-red-500/50"></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -518,24 +680,34 @@ export default function MisAvisosPage() {
|
|||||||
onConfirm={confirmStatusChange}
|
onConfirm={confirmStatusChange}
|
||||||
onCancel={() => setModalConfig({ ...modalConfig, isOpen: false })}
|
onCancel={() => setModalConfig({ ...modalConfig, isOpen: false })}
|
||||||
isDanger={modalConfig.isDanger}
|
isDanger={modalConfig.isDanger}
|
||||||
confirmText={modalConfig.newStatus === AD_STATUSES.SOLD ? "¡Sí, vendido!" : "Confirmar"}
|
confirmText={
|
||||||
|
modalConfig.newStatus === AD_STATUSES.SOLD
|
||||||
|
? "¡Sí, vendido!"
|
||||||
|
: "Confirmar"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DROPDOWN DE ESTADO
|
// DROPDOWN DE ESTADO
|
||||||
function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, onChange: (val: number) => void }) {
|
function StatusDropdown({
|
||||||
|
currentStatus,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
currentStatus: number;
|
||||||
|
onChange: (val: number) => void;
|
||||||
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Fallback seguro si currentStatus no tiene config
|
// Fallback seguro si currentStatus no tiene config
|
||||||
const currentConfig = STATUS_CONFIG[currentStatus] || {
|
const currentConfig = STATUS_CONFIG[currentStatus] || {
|
||||||
label: 'Desconocido',
|
label: "Desconocido",
|
||||||
color: 'text-gray-400',
|
color: "text-gray-400",
|
||||||
bg: 'bg-gray-500/10',
|
bg: "bg-gray-500/10",
|
||||||
border: 'border-gray-500/20',
|
border: "border-gray-500/20",
|
||||||
icon: '❓'
|
icon: "❓",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALLOWED_STATUSES = [
|
const ALLOWED_STATUSES = [
|
||||||
@@ -543,12 +715,15 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on
|
|||||||
AD_STATUSES.PAUSED,
|
AD_STATUSES.PAUSED,
|
||||||
AD_STATUSES.RESERVED,
|
AD_STATUSES.RESERVED,
|
||||||
AD_STATUSES.SOLD,
|
AD_STATUSES.SOLD,
|
||||||
AD_STATUSES.DELETED
|
AD_STATUSES.DELETED,
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
if (
|
||||||
|
wrapperRef.current &&
|
||||||
|
!wrapperRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +739,9 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{currentConfig.icon}</span>
|
<span>{currentConfig.icon}</span>
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest">{currentConfig.label}</span>
|
<span className="text-[10px] font-black uppercase tracking-widest">
|
||||||
|
{currentConfig.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs">▼</span>
|
<span className="text-xs">▼</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -580,8 +757,11 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={statusId}
|
key={statusId}
|
||||||
onClick={() => { onChange(statusId); setIsOpen(false); }}
|
onClick={() => {
|
||||||
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'}`}
|
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>
|
<span className="text-sm">{config.icon}</span>
|
||||||
{config.label}
|
{config.label}
|
||||||
@@ -594,14 +774,28 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetricCard({ label, value, icon }: { label: string, value: any, icon: string }) {
|
function MetricCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: any;
|
||||||
|
icon: string;
|
||||||
|
}) {
|
||||||
return (
|
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="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 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>
|
<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-2xl md:text-3xl font-black text-white tracking-tighter block leading-none mb-1">
|
||||||
<span className="text-[9px] md:text-[10px] font-black uppercase tracking-widest text-gray-500 block leading-tight">{label}</span>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user