diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs index 31b2570..2aafa1f 100644 --- a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs +++ b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs @@ -326,6 +326,9 @@ public class AdsV2Controller : ControllerBase contactPhone = ad.ContactPhone, contactEmail = ad.ContactEmail, displayContactInfo = ad.DisplayContactInfo, + showPhone = ad.ShowPhone, + allowWhatsApp = ad.AllowWhatsApp, + showEmail = ad.ShowEmail, photos = ad.Photos.Select(p => new { p.PhotoID, p.FilePath, p.IsCover, p.SortOrder }), features = ad.Features.Select(f => new { f.FeatureKey, f.FeatureValue }), brand = ad.Brand != null ? new { id = ad.Brand.BrandID, name = ad.Brand.Name } : null, @@ -432,6 +435,9 @@ public class AdsV2Controller : ControllerBase ContactPhone = request.ContactPhone, ContactEmail = request.ContactEmail, DisplayContactInfo = request.DisplayContactInfo, + ShowPhone = request.ShowPhone, + AllowWhatsApp = request.AllowWhatsApp, + ShowEmail = request.ShowEmail, CreatedAt = DateTime.UtcNow }; @@ -655,6 +661,9 @@ public class AdsV2Controller : ControllerBase ad.ContactPhone = updatedAdDto.ContactPhone; ad.ContactEmail = updatedAdDto.ContactEmail; ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo; + ad.ShowPhone = updatedAdDto.ShowPhone; + ad.AllowWhatsApp = updatedAdDto.AllowWhatsApp; + ad.ShowEmail = updatedAdDto.ShowEmail; // Nota: IsFeatured y otros campos sensibles se manejan por separado (pago/admin) // LÓGICA DE ESTADO TRAS RECHAZO diff --git a/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs b/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs index 8603c53..abcff98 100644 --- a/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs +++ b/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs @@ -95,6 +95,15 @@ public class CreateAdRequestDto [JsonPropertyName("displayContactInfo")] public bool DisplayContactInfo { get; set; } = true; + [JsonPropertyName("showPhone")] + public bool ShowPhone { get; set; } = true; + + [JsonPropertyName("allowWhatsApp")] + public bool AllowWhatsApp { get; set; } = true; + + [JsonPropertyName("showEmail")] + public bool ShowEmail { get; set; } = true; + // --- Admin Only --- [JsonPropertyName("targetUserID")] diff --git a/Backend/MotoresArgentinosV2.Core/Entities/AppEntities.cs b/Backend/MotoresArgentinosV2.Core/Entities/AppEntities.cs index 80a897e..f0e9c1e 100644 --- a/Backend/MotoresArgentinosV2.Core/Entities/AppEntities.cs +++ b/Backend/MotoresArgentinosV2.Core/Entities/AppEntities.cs @@ -125,6 +125,11 @@ public class Ad public string? ContactPhone { get; set; } public string? ContactEmail { get; set; } public bool DisplayContactInfo { get; set; } = true; + + public bool ShowPhone { get; set; } = true; + public bool AllowWhatsApp { get; set; } = true; + public bool ShowEmail { get; set; } = true; + public bool IsFeatured { get; set; } public int StatusID { get; set; } diff --git a/Frontend/src/components/FormularioAviso.tsx b/Frontend/src/components/FormularioAviso.tsx index 4f34bf0..eb89d7e 100644 --- a/Frontend/src/components/FormularioAviso.tsx +++ b/Frontend/src/components/FormularioAviso.tsx @@ -1,12 +1,12 @@ -import { useState, useEffect, useRef } from 'react'; -import { AdsV2Service } from '../services/ads.v2.service'; -import { AuthService } from '../services/auth.service'; -import type { DatosAvisoDto } from '../types/aviso.types'; -import SearchableSelect from './SearchableSelect'; -import { AdminService } from '../services/admin.service'; -import CreditCardForm from './CreditCardForm'; -import MercadoPagoLogo from './MercadoPagoLogo'; -import ConfirmationModal from './ConfirmationModal'; +import { useState, useEffect, useRef } from "react"; +import { AdsV2Service } from "../services/ads.v2.service"; +import { AuthService } from "../services/auth.service"; +import type { DatosAvisoDto } from "../types/aviso.types"; +import SearchableSelect from "./SearchableSelect"; +import { AdminService } from "../services/admin.service"; +import CreditCardForm from "./CreditCardForm"; +import MercadoPagoLogo from "./MercadoPagoLogo"; +import ConfirmationModal from "./ConfirmationModal"; import { VEHICLE_TYPES, AUTO_SEGMENTS, @@ -15,10 +15,10 @@ import { MOTO_TRANSMISSIONS, FUEL_TYPES, VEHICLE_CONDITIONS, - STEERING_TYPES -} from '../constants/vehicleOptions'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../context/AuthContext'; + STEERING_TYPES, +} from "../constants/vehicleOptions"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; interface Props { plan: DatosAvisoDto; @@ -29,18 +29,25 @@ interface Props { type PhotoSource = File | { id: number; path: string }; -export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: Props) { +export default function FormularioAviso({ + plan, + onCancel, + onSuccess, + editId, +}: Props) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [photos, setPhotos] = useState([]); const navigate = useNavigate(); // Estados Maestros - const [brands, setBrands] = useState<{ id: number, name: string }[]>([]); + const [brands, setBrands] = useState<{ id: number; name: string }[]>([]); // Estados Autocomplete Modelos - const [modelSearch, setModelSearch] = useState(''); - const [modelSuggestions, setModelSuggestions] = useState<{ id: number, name: string }[]>([]); + const [modelSearch, setModelSearch] = useState(""); + const [modelSuggestions, setModelSuggestions] = useState< + { id: number; name: string }[] + >([]); const [showSuggestions, setShowSuggestions] = useState(false); const modelWrapperRef = useRef(null); @@ -53,54 +60,73 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P const isAdmin = user?.userType === 3; // Buscador de Usuarios para Admin - const [userSearch, setUserSearch] = useState(''); - const [userSuggestions, setUserSuggestions] = useState<{ id: number, name: string, email: string }[]>([]); - const [targetUser, setTargetUser] = useState<{ id: number, name: string, email: string } | null>(null); + const [userSearch, setUserSearch] = useState(""); + const [userSuggestions, setUserSuggestions] = useState< + { id: number; name: string; email: string }[] + >([]); + const [targetUser, setTargetUser] = useState<{ + id: number; + name: string; + email: string; + } | null>(null); const [showUserSuggestions, setShowUserSuggestions] = useState(false); const userWrapperRef = useRef(null); - const [ghostUser, setGhostUser] = useState({ email: '', firstName: '', lastName: '', phone: '' }); + const [ghostUser, setGhostUser] = useState({ + email: "", + firstName: "", + lastName: "", + phone: "", + }); // Estado para el modal de coincidencia de usuario - const [userMatchModal, setUserMatchModal] = useState<{ isOpen: boolean; user: any | null }>({ + const [userMatchModal, setUserMatchModal] = useState<{ + isOpen: boolean; + user: any | null; + }>({ isOpen: false, - user: null + user: null, }); // Estado del vehículo const [vehicleData, setVehicleData] = useState({ - brandId: '', - modelId: '', - modelName: '', + brandId: "", + modelId: "", + modelName: "", year: new Date().getFullYear(), km: 0, price: 0, - currency: 'ARS', - description: '', - fuelType: '', - color: '', - segment: '', - location: '', - condition: '', + currency: "ARS", + description: "", + fuelType: "", + color: "", + segment: "", + location: "", + condition: "", doorCount: undefined as number | undefined, - transmission: '', - steering: '' + transmission: "", + steering: "", }); // Contacto const [contactData, setContactData] = useState({ - phone: (user as any)?.phoneNumber || (user as any)?.phone || '', - email: user?.email || '', - displayInfo: true + phone: (user as any)?.phoneNumber || (user as any)?.phone || "", + email: user?.email || "", + showPhone: true, + allowWhatsApp: true, + showEmail: true, }); // Sincronizar contacto cuando el usuario cargue useEffect(() => { if (user && !editId && !targetUser && !ghostUser.email) { - setContactData(prev => ({ + setContactData((prev) => ({ ...prev, email: user.email || prev.email, - phone: (user as any).phone || (user as any).phoneNumber || prev.phone + phone: (user as any).phone || (user as any).phoneNumber || prev.phone, + showPhone: true, + allowWhatsApp: true, + showEmail: true, })); } }, [user, editId]); @@ -135,12 +161,14 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P const data = await AdminService.searchUsers(userSearch); const users = data.map((u: any) => ({ id: u.userID, - name: `${u.firstName || ''} ${u.lastName || ''} (${u.userName})`.trim(), + name: `${u.firstName || ""} ${u.lastName || ""} (${u.userName})`.trim(), email: u.email, - phone: u.phoneNumber + phone: u.phoneNumber, })); setUserSuggestions(users); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } }, 300); return () => clearTimeout(timer); } else { @@ -158,10 +186,16 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P // 5. CERRAR SUGERENCIAS AL CLICKEAR FUERA useEffect(() => { function handleClickOutside(event: MouseEvent) { - if (modelWrapperRef.current && !modelWrapperRef.current.contains(event.target as Node)) { + if ( + modelWrapperRef.current && + !modelWrapperRef.current.contains(event.target as Node) + ) { setShowSuggestions(false); } - if (userWrapperRef.current && !userWrapperRef.current.contains(event.target as Node)) { + if ( + userWrapperRef.current && + !userWrapperRef.current.contains(event.target as Node) + ) { setShowUserSuggestions(false); } } @@ -173,33 +207,35 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P try { const ad = await AdsV2Service.getById(id); setVehicleData({ - brandId: ad.brandID?.toString() || '', - modelId: ad.modelID?.toString() || '', - modelName: ad.versionName || '', + brandId: ad.brandID?.toString() || "", + modelId: ad.modelID?.toString() || "", + modelName: ad.versionName || "", year: ad.year, km: ad.km, price: ad.price, currency: ad.currency, - description: ad.description || '', - fuelType: (ad.fuelType || ad.FuelType) || '', - color: (ad.color || ad.Color) || '', - segment: (ad.segment || ad.Segment) || '', - location: (ad.location || ad.Location) || '', - condition: (ad.condition || ad.Condition) || '', + description: ad.description || "", + fuelType: ad.fuelType || ad.FuelType || "", + color: ad.color || ad.Color || "", + segment: ad.segment || ad.Segment || "", + location: ad.location || ad.Location || "", + condition: ad.condition || ad.Condition || "", doorCount: ad.doorCount || ad.DoorCount, - transmission: (ad.transmission || ad.Transmission) || '', - steering: (ad.steering || ad.Steering) || '' + transmission: ad.transmission || ad.Transmission || "", + steering: ad.steering || ad.Steering || "", }); - setModelSearch(ad.versionName || ''); + setModelSearch(ad.versionName || ""); setContactData({ - phone: ad.contactPhone || '', - email: ad.contactEmail || '', - displayInfo: ad.displayContactInfo + phone: ad.contactPhone || "", + email: ad.contactEmail || "", + showPhone: ad.showPhone, + allowWhatsApp: ad.allowWhatsApp, + showEmail: ad.showEmail, }); if (ad.photos && ad.photos.length > 0) { const existingPhotos = ad.photos.map((p: any) => ({ id: p.photoID, - path: p.filePath + path: p.filePath, })); setPhotos(existingPhotos); } @@ -211,21 +247,21 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P // --- HANDLERS --- const handleNumberInput = (field: string, value: string) => { - if (value === '') { - setVehicleData(prev => ({ ...prev, [field]: '' })); + if (value === "") { + setVehicleData((prev) => ({ ...prev, [field]: "" })); return; } const num = parseInt(value); - setVehicleData(prev => ({ ...prev, [field]: isNaN(num) ? '' : num })); + setVehicleData((prev) => ({ ...prev, [field]: isNaN(num) ? "" : num })); }; const handleFloatInput = (field: string, value: string) => { - if (value === '') { - setVehicleData(prev => ({ ...prev, [field]: '' })); + if (value === "") { + setVehicleData((prev) => ({ ...prev, [field]: "" })); return; } const num = parseFloat(value); - setVehicleData(prev => ({ ...prev, [field]: isNaN(num) ? '' : num })); + setVehicleData((prev) => ({ ...prev, [field]: isNaN(num) ? "" : num })); }; const handlePhotoChange = (e: React.ChangeEvent) => { @@ -238,14 +274,19 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P } const MAX_SIZE = 3 * 1024 * 1024; - const oversizedFiles = newFiles.filter(file => file.size > MAX_SIZE); + const oversizedFiles = newFiles.filter((file) => file.size > MAX_SIZE); if (oversizedFiles.length > 0) { - alert(`Algunas imágenes superan el límite de 3MB:\n${oversizedFiles.map(f => f.name).join('\n')}`); + alert( + `Algunas imágenes superan el límite de 3MB:\n${oversizedFiles.map((f) => f.name).join("\n")}`, + ); return; } - const invalidTypes = newFiles.filter(file => !['image/jpeg', 'image/png', 'image/webp'].includes(file.type)); + const invalidTypes = newFiles.filter( + (file) => + !["image/jpeg", "image/png", "image/webp"].includes(file.type), + ); if (invalidTypes.length > 0) { alert("Solo se permiten archivos JPG, PNG o WEBP."); return; @@ -261,45 +302,58 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P setShowSuggestions(false); }; - const selectUser = (u: { id: number, name: string, email: string, phone?: string }) => { + const selectUser = (u: { + id: number; + name: string; + email: string; + phone?: string; + }) => { setTargetUser(u); setUserSearch(u.name); setShowUserSuggestions(false); // Actualizamos los datos de contacto con los del usuario seleccionado - setContactData(prev => ({ + setContactData((prev) => ({ ...prev, email: u.email, // Si u.phone viene undefined (porque el backend no lo manda o es null), usamos string vacío - phone: u.phone || '' + phone: u.phone || "", })); }; // Sincronizar datos de usuario fantasma con datos de contacto useEffect(() => { if (!targetUser && (ghostUser.email || ghostUser.phone)) { - setContactData(prev => ({ + setContactData((prev) => ({ ...prev, email: ghostUser.email, - phone: ghostUser.phone + phone: ghostUser.phone, })); } }, [ghostUser, targetUser]); // Detección automática de usuario existente al escribir en "Crear Nuevo" useEffect(() => { - if (!isAdmin || targetUser || !ghostUser.email || ghostUser.email.length < 5) return; + if ( + !isAdmin || + targetUser || + !ghostUser.email || + ghostUser.email.length < 5 + ) + return; const timer = setTimeout(async () => { - if (ghostUser.email.includes('@')) { + if (ghostUser.email.includes("@")) { try { const results = await AdminService.searchUsers(ghostUser.email); - const exactMatch = results.find((u: any) => u.email.toLowerCase() === ghostUser.email.toLowerCase()); + const exactMatch = results.find( + (u: any) => u.email.toLowerCase() === ghostUser.email.toLowerCase(), + ); if (exactMatch) { setUserMatchModal({ isOpen: true, - user: exactMatch + user: exactMatch, }); } } catch (err) { @@ -317,16 +371,16 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P // Mapeamos al formato que espera selectUser const userToSelect = { id: u.userID, - name: `${u.firstName || ''} ${u.lastName || ''} (${u.userName})`.trim(), + name: `${u.firstName || ""} ${u.lastName || ""} (${u.userName})`.trim(), email: u.email, - phone: u.phoneNumber + phone: u.phoneNumber, }; // 1. Asignamos el usuario existente selectUser(userToSelect); // 2. Limpiamos los campos de "Crear Nuevo" - setGhostUser({ email: '', firstName: '', lastName: '', phone: '' }); + setGhostUser({ email: "", firstName: "", lastName: "", phone: "" }); } // Cerramos modal setUserMatchModal({ isOpen: false, user: null }); @@ -334,7 +388,7 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P const handleCancelUserMatch = () => { // Si cancela, limpiamos el email (y resto de campos) para evitar el error de duplicado al intentar guardar - setGhostUser({ email: '', firstName: '', lastName: '', phone: '' }); + setGhostUser({ email: "", firstName: "", lastName: "", phone: "" }); setUserMatchModal({ isOpen: false, user: null }); }; @@ -348,20 +402,39 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P const userSession = AuthService.getCurrentUser(); if (!userSession) throw new Error("Usuario no autenticado"); - if (!vehicleData.brandId || (!vehicleData.modelId && !vehicleData.modelName)) { + if ( + !vehicleData.brandId || + (!vehicleData.modelId && !vehicleData.modelName) + ) { throw new Error("Seleccione una marca y escriba un modelo válido."); } - if (vehicleData.year < 1900 || vehicleData.year > new Date().getFullYear() + 1) { + if ( + vehicleData.year < 1900 || + vehicleData.year > new Date().getFullYear() + 1 + ) { throw new Error("El año ingresado no es válido."); } - if (vehicleData.price <= 0) { - throw new Error("El precio debe ser mayor a 0."); + if (vehicleData.price < 0) { + throw new Error("El precio no puede ser negativo."); } - // Si oculta datos, debe escribir descripción - if (!contactData.displayInfo && vehicleData.description.length < 10) { - throw new Error("Si ocultas tus datos de contacto, debes detallar un medio de comunicación en la descripción."); + if (contactData.showPhone && !contactData.phone) + throw new Error("Para mostrar el teléfono, debes ingresarlo."); + if (contactData.allowWhatsApp && !contactData.phone) + throw new Error("Para habilitar WhatsApp, debes ingresar un teléfono."); + if (contactData.showEmail && !contactData.email) + throw new Error("Para mostrar el email, debes ingresarlo."); + + // Si oculta todo, debe escribir descripción + const isAnyContactVisible = + contactData.showPhone || + contactData.allowWhatsApp || + contactData.showEmail; + if (!isAnyContactVisible && vehicleData.description.length < 10) { + throw new Error( + "Si ocultas todos tus datos de contacto, debes detallar un medio de comunicación en la descripción.", + ); } const finalVersionName = modelSearch; @@ -382,7 +455,10 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P contactPhone: contactData.phone, contactEmail: contactData.email, - displayContactInfo: contactData.displayInfo, + displayContactInfo: isAnyContactVisible, + showPhone: contactData.showPhone, + allowWhatsApp: contactData.allowWhatsApp, + showEmail: contactData.showEmail, fuelType: vehicleData.fuelType, color: vehicleData.color, @@ -394,15 +470,19 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P steering: vehicleData.steering, targetUserID: targetUser?.id, - ghostUserEmail: !targetUser && ghostUser.email ? ghostUser.email : undefined, - ghostFirstName: !targetUser && ghostUser.firstName ? ghostUser.firstName : undefined, - ghostLastName: !targetUser && ghostUser.lastName ? ghostUser.lastName : undefined, - ghostUserPhone: !targetUser && ghostUser.phone ? ghostUser.phone : undefined + ghostUserEmail: + !targetUser && ghostUser.email ? ghostUser.email : undefined, + ghostFirstName: + !targetUser && ghostUser.firstName ? ghostUser.firstName : undefined, + ghostLastName: + !targetUser && ghostUser.lastName ? ghostUser.lastName : undefined, + ghostUserPhone: + !targetUser && ghostUser.phone ? ghostUser.phone : undefined, }; let adId = currentAdId; - const newPhotos = photos.filter(p => p instanceof File) as File[]; + const newPhotos = photos.filter((p) => p instanceof File) as File[]; if (adId) { await AdsV2Service.update(adId, adPayload); @@ -437,8 +517,15 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P // Validación específica para Admin creando usuario fantasma if (isAdmin && !targetUser && !editId) { - if (!ghostUser.email || !ghostUser.firstName || !ghostUser.lastName || !ghostUser.phone) { - setError("Para crear un nuevo cliente, debes completar Nombre, Apellido, Email y Teléfono."); + if ( + !ghostUser.email || + !ghostUser.firstName || + !ghostUser.lastName || + !ghostUser.phone + ) { + setError( + "Para crear un nuevo cliente, debes completar Nombre, Apellido, Email y Teléfono.", + ); setLoading(false); return; } @@ -452,7 +539,6 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P setLoading(false); setShowPaymentForm(true); - } catch (err: any) { console.error("Error al guardar aviso:", err.response?.data || err); const backendError = err.response?.data; @@ -478,12 +564,13 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P const finalPayload = { ...paymentData, adId: currentAdId }; const response = await AdsV2Service.processPayment(finalPayload); - if (response.status === 'approved') { + if (response.status === "approved") { onSuccess(currentAdId!); } else { - navigate(`/pago-confirmado?status=${response.status}&adId=${currentAdId}`); + navigate( + `/pago-confirmado?status=${response.status}&adId=${currentAdId}`, + ); } - } catch (err: any) { console.error("Error completo:", err.response?.data); const detail = err.response?.data?.detail; @@ -491,20 +578,26 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P let userMessage = "El pago no pudo ser procesado. Intenta nuevamente."; const MP_ERRORS: Record = { - 'cc_rejected_bad_filled_card_number': 'Revisá el número de tarjeta.', - 'cc_rejected_bad_filled_date': 'Revisá la fecha de vencimiento.', - 'cc_rejected_bad_filled_other': 'Revisá los datos de la tarjeta.', - 'cc_rejected_bad_filled_security_code': 'Revisá el código de seguridad (CVV).', - 'cc_rejected_blacklist': 'No pudimos procesar tu pago.', - 'cc_rejected_call_for_authorize': 'Debes autorizar el pago llamando a tu banco.', - 'cc_rejected_card_disabled': 'Llamá a tu banco para activar tu tarjeta.', - 'cc_rejected_card_error': 'No pudimos procesar tu pago.', - 'cc_rejected_duplicated_payment': 'Ya hiciste un pago por ese valor. Revisa tus movimientos.', - 'cc_rejected_high_risk': 'Tu pago fue rechazado por motivos de seguridad.', - 'cc_rejected_insufficient_amount': 'Tu tarjeta no tiene fondos suficientes.', - 'cc_rejected_invalid_installments': 'La tarjeta no procesa pagos en la cantidad de cuotas seleccionada.', - 'cc_rejected_max_attempts': 'Llegaste al límite de intentos permitidos.', - 'cc_rejected_other_reason': 'La tarjeta rechazó el pago.' + cc_rejected_bad_filled_card_number: "Revisá el número de tarjeta.", + cc_rejected_bad_filled_date: "Revisá la fecha de vencimiento.", + cc_rejected_bad_filled_other: "Revisá los datos de la tarjeta.", + cc_rejected_bad_filled_security_code: + "Revisá el código de seguridad (CVV).", + cc_rejected_blacklist: "No pudimos procesar tu pago.", + cc_rejected_call_for_authorize: + "Debes autorizar el pago llamando a tu banco.", + cc_rejected_card_disabled: "Llamá a tu banco para activar tu tarjeta.", + cc_rejected_card_error: "No pudimos procesar tu pago.", + cc_rejected_duplicated_payment: + "Ya hiciste un pago por ese valor. Revisa tus movimientos.", + cc_rejected_high_risk: + "Tu pago fue rechazado por motivos de seguridad.", + cc_rejected_insufficient_amount: + "Tu tarjeta no tiene fondos suficientes.", + cc_rejected_invalid_installments: + "La tarjeta no procesa pagos en la cantidad de cuotas seleccionada.", + cc_rejected_max_attempts: "Llegaste al límite de intentos permitidos.", + cc_rejected_other_reason: "La tarjeta rechazó el pago.", }; if (detail && MP_ERRORS[detail]) { @@ -521,9 +614,10 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P }; if (showPaymentForm && currentAdId) { - const rawMonto = plan.importeTotsiniva > 0 - ? plan.importeTotsiniva * 1.105 - : plan.importeSiniva * 1.105; + const rawMonto = + plan.importeTotsiniva > 0 + ? plan.importeTotsiniva * 1.105 + : plan.importeSiniva * 1.105; const monto = Math.round(rawMonto); @@ -531,10 +625,8 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P
{/* HEADER DEL PAGO */}
- {/* Tarjeta contenedora con gradiente sutil hacia abajo */}
- {/* Fondo decorativo "Atmósfera MP" (Brillo azul en la parte superior) */}
@@ -547,11 +639,12 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P
-
- Total a pagar + + Total a pagar + - ${monto.toLocaleString('es-AR')} + ${monto.toLocaleString("es-AR")}
@@ -590,22 +683,31 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P } const buttonText = loading - ? 'Procesando...' - : (isAdmin - ? (editId ? 'Guardar Cambios' : 'Publicar Directamente') - : (editId ? 'Guardar Cambios' : 'Ir a Pagar') - ); + ? "Procesando..." + : isAdmin + ? editId + ? "Guardar Cambios" + : "Publicar Directamente" + : editId + ? "Guardar Cambios" + : "Ir a Pagar"; return ( -
- + {/* --- SECCIÓN ADMIN --- */} {isAdmin && !editId && (
-

Panel Administrativo

+

+ Panel Administrativo +

- + { setUserSearch(e.target.value); setShowUserSuggestions(true); - if (e.target.value === '') setTargetUser(null); + if (e.target.value === "") setTargetUser(null); }} onFocus={() => setShowUserSuggestions(true)} /> {showUserSuggestions && userSuggestions.length > 0 && (
    - {userSuggestions.map(u => ( + {userSuggestions.map((u) => (
  • selectUser(u)} @@ -634,40 +736,64 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P )} {targetUser && (
    - Asignado a: {targetUser.name} (ID: {targetUser.id}) - + + Asignado a: {targetUser.name} (ID:{" "} + {targetUser.id}) + +
    )}
- O crear nuevo + + O crear nuevo +
-
+
setGhostUser({ ...ghostUser, email: e.target.value })} + onChange={(e) => + setGhostUser({ ...ghostUser, email: e.target.value }) + } className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" /> setGhostUser({ ...ghostUser, firstName: e.target.value })} + onChange={(e) => + setGhostUser({ ...ghostUser, firstName: e.target.value }) + } className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" /> setGhostUser({ ...ghostUser, lastName: e.target.value })} + onChange={(e) => + setGhostUser({ ...ghostUser, lastName: e.target.value }) + } className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" /> setGhostUser({ ...ghostUser, phone: e.target.value })} + onChange={(e) => + setGhostUser({ ...ghostUser, phone: e.target.value }) + } className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" />
@@ -679,39 +805,57 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P

- 1 + + 1 + Datos del Vehículo

- + { - setVehicleData({ ...vehicleData, brandId: val, modelId: '', modelName: '' }); - setModelSearch(''); + setVehicleData({ + ...vehicleData, + brandId: val, + modelId: "", + modelName: "", + }); + setModelSearch(""); setModelSuggestions([]); }} placeholder="Buscar Marca..." />
-
- +
+ { + onChange={(e) => { const val = e.target.value; setModelSearch(val); setShowSuggestions(true); - setVehicleData(prev => ({ + setVehicleData((prev) => ({ ...prev, - modelId: '', - modelName: val + modelId: "", + modelName: val, })); }} onFocus={() => setShowSuggestions(true)} @@ -719,7 +863,7 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P /> {showSuggestions && modelSuggestions.length > 0 && (
    - {modelSuggestions.map(m => ( + {modelSuggestions.map((m) => (
  • selectModel(m.id, m.name)} @@ -733,99 +877,244 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P
{/* INPUTS NUMÉRICOS SEGUROS */}
- - handleNumberInput('year', e.target.value)} /> + + handleNumberInput("year", e.target.value)} + />
- - + Kilómetros + + handleNumberInput('km', e.target.value)} /> + value={vehicleData.km ?? ""} + onChange={(e) => handleNumberInput("km", e.target.value)} + />
- +
- handleFloatInput('price', e.target.value)} /> + value={vehicleData.price || ""} + onChange={(e) => handleFloatInput("price", e.target.value)} + />
- - -
-
- - + setVehicleData({ ...vehicleData, fuelType: e.target.value }) + } + > + + {FUEL_TYPES.map((f) => ( + ))}
- - setVehicleData({ ...vehicleData, color: e.target.value })} /> + + +
+
+ + + setVehicleData({ ...vehicleData, color: e.target.value }) + } + />
{plan.idRubro !== VEHICLE_TYPES.MOTOS && (
- - + Puertas + + handleNumberInput('doorCount', e.target.value)} /> + value={vehicleData.doorCount || ""} + onChange={(e) => + handleNumberInput("doorCount", e.target.value) + } + />
)}
- - + setVehicleData({ ...vehicleData, segment: e.target.value }) + } + > + + {(plan.idRubro === VEHICLE_TYPES.MOTOS + ? MOTO_SEGMENTS + : AUTO_SEGMENTS + ).map((s) => ( + ))}
- - setVehicleData({ ...vehicleData, location: e.target.value })} /> + + + setVehicleData({ ...vehicleData, location: e.target.value }) + } + />
- - + setVehicleData({ + ...vehicleData, + condition: e.target.value, + }) + } + > + + {VEHICLE_CONDITIONS.map((c) => ( + + ))}
{plan.idRubro !== VEHICLE_TYPES.MOTOS && (
- - + setVehicleData({ + ...vehicleData, + steering: e.target.value, + }) + } + > + + {STEERING_TYPES.map((s) => ( + + ))}
)} @@ -838,17 +1127,22 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P

- 2 + + 2 + Multimedia y Detalles

- {/* 1. DESCRIPCIÓN (Ancho completo) */}
- - 900 ? 'text-amber-500' : 'text-gray-600'}`}> + + 900 ? "text-amber-500" : "text-gray-600"}`} + > {vehicleData.description.length}/1000
@@ -860,7 +1154,12 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P className="w-full h-35 bg-[#0a0c10]/50 border border-white/10 rounded-2xl p-5 text-sm text-white outline-none focus:border-blue-500/50 focus:bg-[#0a0c10] transition-all resize-none leading-relaxed placeholder:text-gray-700" placeholder="Describe los detalles importantes: • Estado de los neumáticos • Service realizados • Papeles al día..." value={vehicleData.description} - onChange={e => setVehicleData({ ...vehicleData, description: e.target.value })} + onChange={(e) => + setVehicleData({ + ...vehicleData, + description: e.target.value, + }) + } />
@@ -869,23 +1168,36 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P {/* 2. GESTOR DE FOTOS (Debajo) */}
- - + + {photos.length}/5 Máximo
- {photos.length === 0 ? ( // ESTADO VACÍO (Grande) ) : ( // GRILLA CUANDO HAY FOTOS @@ -893,16 +1205,24 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P // En móvil: 2 columnas. En Desktop: 4 columnas.
{photos.map((p, idx) => { - const imageUrl = p instanceof File - ? URL.createObjectURL(p) - : `${import.meta.env.VITE_STATIC_BASE_URL}${(p as { path: string }).path}`; + const imageUrl = + p instanceof File + ? URL.createObjectURL(p) + : `${import.meta.env.VITE_STATIC_BASE_URL}${(p as { path: string }).path}`; // La primera foto es la portada (ocupa 2x2 en la grilla) const isCover = idx === 0; return ( -
- {`foto +
+ {`foto {isCover && (
@@ -915,7 +1235,9 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P
)}
-
{/* COLUMNA DERECHA: RESUMEN DE PAGO + CONTACTO */}
- {/* SECCIÓN 1: RESUMEN Y PAGO */}
-

Resumen de Compra

+

+ Resumen de Compra +

Plan: @@ -956,23 +1286,40 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P
- Total Final + + Total Final + - ${Math.round( - (plan.importeTotsiniva > 0 ? plan.importeTotsiniva : plan.importeSiniva) * 1.105 - ).toLocaleString('es-AR')} + $ + {Math.round( + (plan.importeTotsiniva > 0 + ? plan.importeTotsiniva + : plan.importeSiniva) * 1.105, + ).toLocaleString("es-AR")}
- {error &&
{error}
} + {error && ( +
+ {error} +
+ )}
- -
@@ -980,50 +1327,139 @@ export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: P {/* SECCIÓN 2: DATOS DE CONTACTO */}
-
-

+
+

👤 Datos de Contacto

- -
setContactData({ ...contactData, displayInfo: !contactData.displayInfo })}> - - {contactData.displayInfo ? 'Visible' : 'Oculto'} - -
-
-
-
+

+ Elige qué datos quieres hacer públicos en tu aviso. +

- {!contactData.displayInfo && ( -
- ⚠️ Has ocultado tus datos. Recuerda escribir un teléfono o email en la descripción. -
- )} +
+ {/* Teléfono y WhatsApp */} +
+ + { + const val = e.target.value; + // Si borra el teléfono, desactivamos los checks asociados + setContactData({ + ...contactData, + phone: val, + showPhone: val ? contactData.showPhone : false, + allowWhatsApp: val ? contactData.allowWhatsApp : false, + }); + }} + className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-xs text-white outline-none focus:border-blue-500 mb-3" + placeholder="+54 9 221 ..." + /> -
-
- Email -
+
+ {/* Check Mostrar Teléfono */} + + + {/* Check WhatsApp */} + +
+
+ + {/* Email */} +
+ +
{contactData.email}
-
-
- Teléfono -
- {contactData.phone} -
+ + {/* Check Mostrar Email */} +
- {/* Mensaje condicional para no confundir al admin */} {isAdmin && (targetUser || ghostUser.email) ? "Datos del cliente seleccionado/creado." - : "Para cambiar estos datos, ve a \"Mi Perfil\"."} + : 'Para cambiar tu email principal, ve a "Mi Perfil".'}

-
); -} \ No newline at end of file +} diff --git a/Frontend/src/pages/ExplorarPage.tsx b/Frontend/src/pages/ExplorarPage.tsx index 6683ef1..601bcc1 100644 --- a/Frontend/src/pages/ExplorarPage.tsx +++ b/Frontend/src/pages/ExplorarPage.tsx @@ -1,17 +1,17 @@ -import { useState, useEffect } from 'react'; -import { useSearchParams, Link } from 'react-router-dom'; -import { AdsV2Service, type AdListingDto } from '../services/ads.v2.service'; -import { getImageUrl, formatCurrency } from '../utils/app.utils'; -import SearchableSelect from '../components/SearchableSelect'; -import AdStatusBadge from '../components/AdStatusBadge'; +import { useState, useEffect } from "react"; +import { useSearchParams, Link } from "react-router-dom"; +import { AdsV2Service, type AdListingDto } from "../services/ads.v2.service"; +import { getImageUrl, formatCurrency } from "../utils/app.utils"; +import SearchableSelect from "../components/SearchableSelect"; +import AdStatusBadge from "../components/AdStatusBadge"; import { AUTO_SEGMENTS, MOTO_SEGMENTS, AUTO_TRANSMISSIONS, MOTO_TRANSMISSIONS, FUEL_TYPES, - VEHICLE_CONDITIONS -} from '../constants/vehicleOptions'; + VEHICLE_CONDITIONS, +} from "../constants/vehicleOptions"; export default function ExplorarPage() { const [searchParams, setSearchParams] = useSearchParams(); @@ -19,25 +19,29 @@ export default function ExplorarPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [minPrice, setMinPrice] = useState(searchParams.get('minPrice') || ''); - const [maxPrice, setMaxPrice] = useState(searchParams.get('maxPrice') || ''); - const [currencyFilter, setCurrencyFilter] = useState(searchParams.get('currency') || ''); - const [minYear, setMinYear] = useState(searchParams.get('minYear') || ''); - const [maxYear, setMaxYear] = useState(searchParams.get('maxYear') || ''); - const [brandId, setBrandId] = useState(searchParams.get('brandId') || ''); - const [modelId, setModelId] = useState(searchParams.get('modelId') || ''); - const [fuel, setFuel] = useState(searchParams.get('fuel') || ''); - const [transmission, setTransmission] = useState(searchParams.get('transmission') || ''); + const [minPrice, setMinPrice] = useState(searchParams.get("minPrice") || ""); + const [maxPrice, setMaxPrice] = useState(searchParams.get("maxPrice") || ""); + const [currencyFilter, setCurrencyFilter] = useState( + searchParams.get("currency") || "", + ); + const [minYear, setMinYear] = useState(searchParams.get("minYear") || ""); + const [maxYear, setMaxYear] = useState(searchParams.get("maxYear") || ""); + const [brandId, setBrandId] = useState(searchParams.get("brandId") || ""); + const [modelId, setModelId] = useState(searchParams.get("modelId") || ""); + const [fuel, setFuel] = useState(searchParams.get("fuel") || ""); + const [transmission, setTransmission] = useState( + searchParams.get("transmission") || "", + ); - const [brands, setBrands] = useState<{ id: number, name: string }[]>([]); - const [models, setModels] = useState<{ id: number, name: string }[]>([]); + const [brands, setBrands] = useState<{ id: number; name: string }[]>([]); + const [models, setModels] = useState<{ id: number; name: string }[]>([]); - const q = searchParams.get('q') || ''; - const c = searchParams.get('c') || 'ALL'; + const q = searchParams.get("q") || ""; + const c = searchParams.get("c") || "ALL"; useEffect(() => { - if (c !== 'ALL') { - const typeId = c === 'EAUTOS' ? 1 : 2; + if (c !== "ALL") { + const typeId = c === "EAUTOS" ? 1 : 2; AdsV2Service.getBrands(typeId).then(setBrands); } else { setBrands([]); @@ -61,7 +65,7 @@ export default function ExplorarPage() { try { const data = await AdsV2Service.getAll({ q, - c: c === 'ALL' ? undefined : c, + c: c === "ALL" ? undefined : c, minPrice: minPrice ? Number(minPrice) : undefined, maxPrice: maxPrice ? Number(maxPrice) : undefined, currency: currencyFilter || undefined, @@ -70,7 +74,7 @@ export default function ExplorarPage() { brandId: brandId ? Number(brandId) : undefined, modelId: modelId ? Number(modelId) : undefined, fuel: fuel || undefined, - transmission: transmission || undefined + transmission: transmission || undefined, }); setListings(data); } catch (err) { @@ -86,32 +90,47 @@ export default function ExplorarPage() { const applyFilters = () => { const newParams = new URLSearchParams(searchParams); - if (minPrice) newParams.set('minPrice', minPrice); else newParams.delete('minPrice'); - if (maxPrice) newParams.set('maxPrice', maxPrice); else newParams.delete('maxPrice'); - if (currencyFilter) newParams.set('currency', currencyFilter); else newParams.delete('currency'); - if (minYear) newParams.set('minYear', minYear); else newParams.delete('minYear'); - if (maxYear) newParams.set('maxYear', maxYear); else newParams.delete('maxYear'); - if (brandId) newParams.set('brandId', brandId); else newParams.delete('brandId'); - if (modelId) newParams.set('modelId', modelId); else newParams.delete('modelId'); - if (fuel) newParams.set('fuel', fuel); else newParams.delete('fuel'); - if (transmission) newParams.set('transmission', transmission); else newParams.delete('transmission'); + if (minPrice) newParams.set("minPrice", minPrice); + else newParams.delete("minPrice"); + if (maxPrice) newParams.set("maxPrice", maxPrice); + else newParams.delete("maxPrice"); + if (currencyFilter) newParams.set("currency", currencyFilter); + else newParams.delete("currency"); + if (minYear) newParams.set("minYear", minYear); + else newParams.delete("minYear"); + if (maxYear) newParams.set("maxYear", maxYear); + else newParams.delete("maxYear"); + if (brandId) newParams.set("brandId", brandId); + else newParams.delete("brandId"); + if (modelId) newParams.set("modelId", modelId); + else newParams.delete("modelId"); + if (fuel) newParams.set("fuel", fuel); + else newParams.delete("fuel"); + if (transmission) newParams.set("transmission", transmission); + else newParams.delete("transmission"); setSearchParams(newParams); }; const clearFilters = () => { - setMinPrice(''); setMaxPrice(''); setMinYear(''); setMaxYear(''); - setCurrencyFilter(''); - setBrandId(''); setModelId(''); setFuel(''); setTransmission(''); + setMinPrice(""); + setMaxPrice(""); + setMinYear(""); + setMaxYear(""); + setCurrencyFilter(""); + setBrandId(""); + setModelId(""); + setFuel(""); + setTransmission(""); const newParams = new URLSearchParams(); - if (q) newParams.set('q', q); - if (c !== 'ALL') newParams.set('c', c); + if (q) newParams.set("q", q); + if (c !== "ALL") newParams.set("c", c); setSearchParams(newParams); }; const handleCategoryFilter = (cat: string) => { const newParams = new URLSearchParams(); - if (q) newParams.set('q', q); - if (cat !== 'ALL') newParams.set('c', cat); + if (q) newParams.set("q", q); + if (cat !== "ALL") newParams.set("c", cat); setSearchParams(newParams); }; @@ -119,23 +138,29 @@ export default function ExplorarPage() {
{/* Sidebar Filters - NATURAL FLOW (NO STICKY, NO SCROLL INTERNO) */} -
); -} \ No newline at end of file +} diff --git a/Frontend/src/pages/MisAvisosPage.tsx b/Frontend/src/pages/MisAvisosPage.tsx index 5e351ed..cb1973e 100644 --- a/Frontend/src/pages/MisAvisosPage.tsx +++ b/Frontend/src/pages/MisAvisosPage.tsx @@ -4,7 +4,7 @@ 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 { formatCurrency, getImageUrl, parseUTCDate } from "../utils/app.utils"; import { AD_STATUSES, STATUS_CONFIG } from "../constants/adStatuses"; import ConfirmationModal from "../components/ConfirmationModal"; @@ -374,7 +374,7 @@ export default function MisAvisosPage() { {av.brandName} {av.versionName} - {av.currency} {av.price.toLocaleString()} + {formatCurrency(av.price, av.currency)}
diff --git a/Frontend/src/pages/VehiculoDetailPage.tsx b/Frontend/src/pages/VehiculoDetailPage.tsx index 7d0f94b..ff7810f 100644 --- a/Frontend/src/pages/VehiculoDetailPage.tsx +++ b/Frontend/src/pages/VehiculoDetailPage.tsx @@ -1,12 +1,17 @@ -import { useState, useEffect, useRef } from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { AdsV2Service } from '../services/ads.v2.service'; -import { AuthService } from '../services/auth.service'; -import ChatModal from '../components/ChatModal'; -import { FaWhatsapp, FaMapMarkerAlt, FaInfoCircle, FaShareAlt } from 'react-icons/fa'; -import { AD_STATUSES } from '../constants/adStatuses'; -import AdStatusBadge from '../components/AdStatusBadge'; -import PremiumGallery from '../components/PremiumGallery'; +import { useState, useEffect, useRef } from "react"; +import { useParams, Link } from "react-router-dom"; +import { AdsV2Service } from "../services/ads.v2.service"; +import { AuthService } from "../services/auth.service"; +import ChatModal from "../components/ChatModal"; +import { + FaWhatsapp, + FaMapMarkerAlt, + FaInfoCircle, + FaShareAlt, +} from "react-icons/fa"; +import { AD_STATUSES } from "../constants/adStatuses"; +import AdStatusBadge from "../components/AdStatusBadge"; +import PremiumGallery from "../components/PremiumGallery"; export default function VehiculoDetailPage() { const { id } = useParams(); @@ -43,7 +48,9 @@ export default function VehiculoDetailPage() { }, [id, user?.id]); useEffect(() => { - return () => { viewRegistered.current = false; }; + return () => { + viewRegistered.current = false; + }; }, [id]); const handleFavoriteToggle = async () => { @@ -52,52 +59,102 @@ export default function VehiculoDetailPage() { if (isFavorite) await AdsV2Service.removeFavorite(user.id, Number(id)); else await AdsV2Service.addFavorite(user.id, Number(id)!); setIsFavorite(!isFavorite); - } catch (err) { console.error(err); } + } catch (err) { + console.error(err); + } }; const getWhatsAppLink = (phone: string, title: string) => { - if (!phone) return '#'; - let number = phone.replace(/[^\d]/g, ''); - if (number.startsWith('0')) number = number.substring(1); - if (!number.startsWith('54')) number = `549${number}`; + if (!phone) return "#"; + let number = phone.replace(/[^\d]/g, ""); + if (number.startsWith("0")) number = number.substring(1); + if (!number.startsWith("54")) number = `549${number}`; const message = `Hola, vi tu aviso "${title}" en Motores Argentinos y me interesa.`; return `https://wa.me/${number}?text=${encodeURIComponent(message)}`; }; - const handleShare = (platform: 'wa' | 'fb' | 'copy') => { + const handleShare = (platform: "wa" | "fb" | "copy") => { const url = window.location.href; - const vehicleTitle = `${vehicle.brand?.name || ''} ${vehicle.versionName}`.trim(); + const vehicleTitle = + `${vehicle.brand?.name || ""} ${vehicle.versionName}`.trim(); const text = `Mira este ${vehicleTitle} en Motores Argentinos!`; switch (platform) { - case 'wa': window.open(`https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`, '_blank'); break; - case 'fb': window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, '_blank'); break; - case 'copy': navigator.clipboard.writeText(url); alert('Enlace copiado al portapapeles'); break; + case "wa": + window.open( + `https://wa.me/?text=${encodeURIComponent(text + " " + url)}`, + "_blank", + ); + break; + case "fb": + window.open( + `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, + "_blank", + ); + break; + case "copy": + navigator.clipboard.writeText(url); + alert("Enlace copiado al portapapeles"); + break; } }; - if (loading) return ( -
-
- Cargando... -
- ); + if (loading) + return ( +
+
+ + Cargando... + +
+ ); - if (error || !vehicle) return
{error || "Vehículo no encontrado"}
; + if (error || !vehicle) + return ( +
+ {error || "Vehículo no encontrado"} +
+ ); + + // HELPER: Valida que el dato exista y no sea basura ("0", vacío, etc) + const hasData = (val: string | null | undefined) => { + return val && val.trim().length > 0 && val !== "0"; + }; const isOwnerAdmin = vehicle.ownerUserType === 3; const isAdActive = vehicle.statusID === AD_STATUSES.ACTIVE; - const isContactable = isAdActive && vehicle.displayContactInfo; + + // CALCULAMOS LA DISPONIBILIDAD REAL + // 1. WhatsApp: Debe estar habilitado Y tener un teléfono válido + const canShowWhatsApp = + vehicle.allowWhatsApp && hasData(vehicle.contactPhone); + + // 2. Teléfono: Debe estar habilitado ("Mostrar Número") Y tener un teléfono válido + const canShowPhone = vehicle.showPhone && hasData(vehicle.contactPhone); + + // 3. Email: Debe estar habilitado Y tener un email válido + const canShowEmail = vehicle.showEmail && hasData(vehicle.contactEmail); + + // Es contactable si está Activo Y (Tiene WhatsApp O Tiene Teléfono O Tiene Email) + const isContactable = + isAdActive && (canShowWhatsApp || canShowPhone || canShowEmail); return (
- {/* COLUMNA IZQUIERDA: Galería + Descripción (Desktop) */}
} - featuredBadge={vehicle.isFeatured && ⭐ DESTACADO} - locationBadge={vehicle.location && {vehicle.location}} + featuredBadge={ + vehicle.isFeatured && ( + + ⭐ DESTACADO + + ) + } + locationBadge={ + vehicle.location && ( + + {" "} + {vehicle.location} + + ) + } /> {/* BLOQUE 3: Información General y Técnica (Acomodado debajo de la galería) */} @@ -118,7 +188,10 @@ export default function VehiculoDetailPage() {
📝
-

Descripción del Vendedor

+

+ Descripción del{" "} + Vendedor +

{vehicle.description} @@ -130,18 +203,54 @@ export default function VehiculoDetailPage() {

-

Información Técnica

+

+ Información Técnica +

- - - + + + - - {vehicle.condition && } - {vehicle.doorCount && } - {vehicle.engineSize && } + + {vehicle.condition && ( + + )} + {vehicle.doorCount && ( + + )} + {vehicle.engineSize && ( + + )}
@@ -158,7 +267,9 @@ export default function VehiculoDetailPage() {

- {vehicle.brand?.name} + + {vehicle.brand?.name} + {vehicle.versionName}

@@ -169,57 +280,99 @@ export default function VehiculoDetailPage() {
- Precio + + Precio +
- {vehicle.currency} {vehicle.price?.toLocaleString()} + + {vehicle.price === 0 + ? "CONSULTAR" + : `${vehicle.currency} ${vehicle.price?.toLocaleString()}`} +
{isContactable ? (
- - - Contactar - + {/* BOTÓN WHATSAPP: Usamos la nueva variable canShowWhatsApp */} + {canShowWhatsApp && ( + + + Contactar + + )} - {vehicle.contactPhone && ( + {/* CAJA DE TELÉFONO: Usamos canShowPhone */} + {canShowPhone && (
📞 Llamar: - {vehicle.contactPhone} + + {vehicle.contactPhone} + +
+ )} + + {/* CAJA DE EMAIL: Usamos canShowEmail */} + {canShowEmail && ( +
+ ✉️ + + {vehicle.contactEmail} +
)}
) : (
- {isOwnerAdmin ? 'ℹ️' : - vehicle.statusID === AD_STATUSES.MODERATION_PENDING ? '⏳' : - vehicle.statusID === AD_STATUSES.PAYMENT_PENDING ? '💳' : - vehicle.statusID === AD_STATUSES.SOLD ? '🤝' : '🔒'} + {isOwnerAdmin + ? "ℹ️" + : vehicle.statusID === AD_STATUSES.MODERATION_PENDING + ? "⏳" + : vehicle.statusID === AD_STATUSES.PAYMENT_PENDING + ? "💳" + : vehicle.statusID === AD_STATUSES.SOLD + ? "🤝" + : "🔒"}

- {isOwnerAdmin ? 'Contacto en descripción' : - vehicle.statusID === AD_STATUSES.MODERATION_PENDING ? 'Aviso en Revisión' : - vehicle.statusID === AD_STATUSES.PAYMENT_PENDING ? 'Pago Pendiente' : - vehicle.statusID === AD_STATUSES.SOLD ? 'Vehículo Vendido' : 'No disponible'} + {isOwnerAdmin + ? "Contacto en descripción" + : vehicle.statusID === AD_STATUSES.MODERATION_PENDING + ? "Aviso en Revisión" + : vehicle.statusID === AD_STATUSES.PAYMENT_PENDING + ? "Pago Pendiente" + : vehicle.statusID === AD_STATUSES.SOLD + ? "Vehículo Vendido" + : "No disponible"}

{isOwnerAdmin - ? 'Revisa la descripción para contactar al vendedor.' + ? "Revisa la descripción para contactar al vendedor." : vehicle.statusID === AD_STATUSES.MODERATION_PENDING - ? 'Este aviso está siendo verificado por un moderador. Estará activo pronto.' + ? "Este aviso está siendo verificado por un moderador. Estará activo pronto." : vehicle.statusID === AD_STATUSES.PAYMENT_PENDING - ? 'El pago de este aviso aún no ha sido procesado completamente.' + ? "El pago de este aviso aún no ha sido procesado completamente." : vehicle.statusID === AD_STATUSES.SOLD - ? 'Este vehículo ya ha sido vendido a otro usuario.' - : 'Revisa la descripción para contactar al vendedor.'} + ? "Este vehículo ya ha sido vendido a otro usuario." + : "Revisa la descripción para contactar al vendedor."}

)} -
@@ -240,15 +393,26 @@ export default function VehiculoDetailPage() { ); } -function TechnicalItem({ label, value, icon }: { label: string, value: string, icon: string }) { - if ((value === undefined || value === null || value === '') || value === 'N/A') return null; +function TechnicalItem({ + label, + value, + icon, +}: { + label: string; + value: string; + icon: string; +}) { + if (value === undefined || value === null || value === "" || value === "N/A") + return null; return (
- {label} + + {label} +
{icon} {value}
); -} \ No newline at end of file +} diff --git a/Frontend/src/utils/app.utils.ts b/Frontend/src/utils/app.utils.ts index 2d1f530..38d3fa2 100644 --- a/Frontend/src/utils/app.utils.ts +++ b/Frontend/src/utils/app.utils.ts @@ -31,6 +31,11 @@ export const getImageUrl = (path?: string): string => { * Formatea un número como moneda ARS o USD. */ export const formatCurrency = (amount: number, currency: string = 'ARS') => { + // Lógica para precio 0 + if (amount === 0) { + return 'CONSULTAR'; + } + return new Intl.NumberFormat('es-AR', { style: 'currency', currency: currency,