diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs index 3e2cb5c..31b2570 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs @@ -657,6 +657,13 @@ public class AdsV2Controller : ControllerBase ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo; // 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 var adBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? ""; _context.AuditLogs.Add(new AuditLog @@ -770,11 +777,17 @@ public class AdsV2Controller : ControllerBase 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) { 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 diff --git a/Frontend/src/pages/MisAvisosPage.tsx b/Frontend/src/pages/MisAvisosPage.tsx index 69d112b..9b88ec0 100644 --- a/Frontend/src/pages/MisAvisosPage.tsx +++ b/Frontend/src/pages/MisAvisosPage.tsx @@ -1,17 +1,17 @@ -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'; +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'; +type TabType = "avisos" | "favoritos" | "mensajes"; export default function MisAvisosPage() { - const [activeTab, setActiveTab] = useState('avisos'); + const [activeTab, setActiveTab] = useState("avisos"); const [avisos, setAvisos] = useState([]); const [favoritos, setFavoritos] = useState([]); const [mensajes, setMensajes] = useState([]); @@ -19,7 +19,11 @@ export default function MisAvisosPage() { 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<{ isOpen: boolean; @@ -30,21 +34,21 @@ export default function MisAvisosPage() { isDanger: boolean; }>({ isOpen: false, - title: '', - message: '', + title: "", + message: "", adId: null, newStatus: null, - isDanger: false + 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') { + if (res.status === "approved") { alert("¡Pago confirmado! El aviso pasará a moderación."); cargarAvisos(user!.id); - } else if (res.status === 'rejected') { + } 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 { @@ -58,42 +62,47 @@ export default function MisAvisosPage() { useEffect(() => { if (user) { cargarMensajes(user.id); - if (activeTab === 'avisos') cargarAvisos(user.id); - else if (activeTab === 'favoritos') cargarFavoritos(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 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?'; + 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.'; + 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?'; + 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?'; + 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.'; + title = "Reactivar Aviso"; + message = + "El aviso volverá a estar visible para todos y recibirás consultas nuevamente."; } setModalConfig({ @@ -102,7 +111,7 @@ export default function MisAvisosPage() { message, adId, newStatus, - isDanger + isDanger, }); }; @@ -116,7 +125,7 @@ export default function MisAvisosPage() { await AdsV2Service.changeStatus(adId, newStatus); if (user) cargarAvisos(user.id); } 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 const openChatForAd = async (adId: number, adTitle: string) => { if (!user) return; - const relatedMsg = mensajes.find(m => m.adID === adId); + const relatedMsg = mensajes.find((m) => m.adID === adId); 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 - 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) { // 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 - )); + 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() + const markAsReadPromises = unreadMessages.map((m) => + m.messageID + ? ChatService.markAsRead(m.messageID) + : Promise.resolve(), ); // 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 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 @@ -217,11 +236,16 @@ export default function MisAvisosPage() {
🔒 -

Área Privada

+

+ Área Privada +

Para gestionar tus publicaciones, primero debes iniciar sesión.

- + Identificarse
@@ -229,219 +253,332 @@ export default function MisAvisosPage() { ); } - const totalVisitas = avisos.reduce((acc, curr) => acc + (curr.viewsCounter || 0), 0); - const avisosActivos = avisos.filter(a => a.statusId === 4).length; + const totalVisitas = avisos.reduce( + (acc, curr) => acc + (curr.viewsCounter || 0), + 0, + ); + const avisosActivos = avisos.filter((a) => a.statusId === 4).length; return (
-
-

Mis Avisos

+

+ Mis Avisos +

{user.username.charAt(0).toUpperCase()}
- {user.firstName} {user.lastName} - {user.email} + + {user.firstName} {user.lastName} + + + {user.email} +
- {(['avisos', 'favoritos', 'mensajes'] as TabType[]).map(tab => ( + {(["avisos", "favoritos", "mensajes"] as TabType[]).map((tab) => ( ))}
- - {activeTab === 'avisos' && ( + {activeTab === "avisos" && (
- +
)} - {loading ? (
) : ( <> - {activeTab === 'avisos' && ( + {activeTab === "avisos" && (
- {avisos.filter(a => a.statusId !== 9).length === 0 ? ( + {avisos.filter((a) => a.statusId !== 9).length === 0 ? (
📂 -

No tienes avisos

- Crear mi primer aviso +

+ No tienes avisos +

+ + Crear mi primer aviso +
) : ( - 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); + 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. -
- -
- {`${av.brandName} -
- #{av.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. +
+
+ {`${av.brandName} +
+ + #{av.id} + +
-
-
-
-

- {av.brandName} {av.versionName} -

- {av.currency} {av.price.toLocaleString()} +
+
+

+ {av.brandName} {av.versionName} +

+ + {av.currency} {av.price.toLocaleString()} + +
+
+
+ + Año + + + {av.year} + +
+
+ + Visitas + + + {av.viewsCounter || 0} + +
+ {av.isFeatured && ( +
+ + ⭐ Destacado + +
+ )} +
-
-
- Año - {av.year} -
-
- Visitas - {av.viewsCounter || 0} -
- {av.isFeatured && ( -
- ⭐ Destacado -
- )} -
-
-
- - {/* CASO 1: BORRADOR (1) -> Botón de Pagar */} - {av.statusId === AD_STATUSES.DRAFT && ( - - Continuar Pago ➔ - - )} - - {/* CASO 2: PAGO PENDIENTE (2) -> Botón de Verificar */} - {av.statusId === AD_STATUSES.PAYMENT_PENDING && ( -
-
- ⏳ Pago Pendiente -
- -
- )} - - {/* CASO 3: EN REVISIÓN (3) -> Cartel informativo */} - {av.statusId === AD_STATUSES.MODERATION_PENDING && ( -
- ⏳ En Revisión - No editable -
- )} - - {/* CASO 4: VENCIDO (8) -> Botón de Republicar */} - {av.statusId === AD_STATUSES.EXPIRED && ( -
-
- ⛔ Finalizado -
+
+ {/* CASO 1: BORRADOR (1) -> Botón de Pagar */} + {av.statusId === AD_STATUSES.DRAFT && ( - 🔄 Republicar + Continuar Pago ➔ -
- )} - - {/* 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 && ( - initiateStatusChange(av.id, newStatus)} - /> )} - {/* BOTONES COMUNES (Siempre visibles) */} -
- - 👁️ Ver Detalle - + {/* CASO 2: PAGO PENDIENTE (2) -> Botón de Verificar */} + {av.statusId === AD_STATUSES.PAYMENT_PENDING && ( +
+
+ + ⏳ Pago Pendiente + +
+ +
+ )} - {hasMessages && ( - - )} + 👁️ Ver Detalle + + + {hasMessages && ( + + )} +
-
- ); - }) + ); + }) )}
)} - {activeTab === 'favoritos' && ( + {activeTab === "favoritos" && (
{favoritos.length === 0 ? (
-

No tienes favoritos

- Explorar vehículos +

+ No tienes favoritos +

+ + Explorar vehículos +
) : ( favoritos.map((fav) => ( -
+
- {`${fav.brandName} - + {`${fav.brandName} +
-

{fav.brandName} {fav.versionName}

-

{fav.currency} {fav.price.toLocaleString()}

- Ver Detalle +

+ {fav.brandName} {fav.versionName} +

+

+ {fav.currency} {fav.price.toLocaleString()} +

+ + Ver Detalle +
)) @@ -449,49 +586,74 @@ export default function MisAvisosPage() {
)} - {activeTab === 'mensajes' && ( + {activeTab === "mensajes" && (
{mensajes.length === 0 ? (
💬 -

No tienes mensajes

-

Los moderadores te contactarán por aquí si es necesario.

+

+ No tienes mensajes +

+

+ Los moderadores te contactarán por aquí si es necesario. +

) : ( - 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}`; + 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 (
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" > -
🛡️
+
+ 🛡️ +

{tituloAviso}

- {parseUTCDate(item.msg.sentAt!).toLocaleDateString('es-AR', { timeZone: 'America/Argentina/Buenos_Aires', hour12: false })} + + {parseUTCDate( + item.msg.sentAt!, + ).toLocaleDateString("es-AR", { + timeZone: "America/Argentina/Buenos_Aires", + hour12: false, + })} +

- {item.msg.senderID === user.id ? 'Tú: ' : ''}{item.msg.messageText} + {item.msg.senderID === user.id ? "Tú: " : ""} + {item.msg.messageText}

{item.unread && (
)}
- ) + ); }) )}
@@ -518,24 +680,34 @@ export default function MisAvisosPage() { onConfirm={confirmStatusChange} onCancel={() => setModalConfig({ ...modalConfig, isOpen: false })} isDanger={modalConfig.isDanger} - confirmText={modalConfig.newStatus === AD_STATUSES.SOLD ? "¡Sí, vendido!" : "Confirmar"} + confirmText={ + modalConfig.newStatus === AD_STATUSES.SOLD + ? "¡Sí, vendido!" + : "Confirmar" + } />
); } // 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 wrapperRef = useRef(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: '❓' + label: "Desconocido", + color: "text-gray-400", + bg: "bg-gray-500/10", + border: "border-gray-500/20", + icon: "❓", }; const ALLOWED_STATUSES = [ @@ -543,12 +715,15 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on AD_STATUSES.PAUSED, AD_STATUSES.RESERVED, AD_STATUSES.SOLD, - AD_STATUSES.DELETED + AD_STATUSES.DELETED, ]; useEffect(() => { 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); } } @@ -564,7 +739,9 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on >
{currentConfig.icon} - {currentConfig.label} + + {currentConfig.label} +
@@ -580,8 +757,11 @@ function StatusDropdown({ currentStatus, onChange }: { currentStatus: number, on return (