'use client'; import { useState, useEffect, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import toast, { Toaster } from 'react-hot-toast'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { MapPin, Bed, Bath, Square, DollarSign, Heart, Share2, Phone, Mail, MessageCircle, Calendar, Shield, Star, ChevronLeft, ChevronRight, Check, X, Wifi, Car, Wind, Camera, Home, Building2, Users, Clock, FileText, LogIn, Loader2, ArrowLeft, ImageIcon, ChevronDown, Layers, Sofa, DoorOpen, School, Hospital, Store, TreePine, Building, GraduationCap, ExternalLink, Smile, Ban, Wine, Dog, CassetteTape, Info } from 'lucide-react'; import { getRentProperty, getSaleProperty, getSalePropertyById, bookReservation, getAvailableDateRanges, getOwnerContactInformation, getMyRentListings, getMySaleListings } from '../../utils/api'; import AuthService from '../../services/AuthService'; import { useFavorites } from '@/app/contexts/FavoritesContext'; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums'; import PropertyRatingList from '@/app/components/ratings/PropertyRatingList'; import PropertyRatingForm from '@/app/components/ratings/PropertyRatingForm'; import StarRating from '@/app/components/ratings/StarRating'; import { getPropertyAverageRating } from '../../utils/ratings'; import dynamic from 'next/dynamic'; import 'leaflet/dist/leaflet.css'; const MapContainer = dynamic(() => import('react-leaflet').then(m => m.MapContainer), { ssr: false }); const TileLayer = dynamic(() => import('react-leaflet').then(m => m.TileLayer), { ssr: false }); const Marker = dynamic(() => import('react-leaflet').then(m => m.Marker), { ssr: false }); const Popup = dynamic(() => import('react-leaflet').then(m => m.Popup), { ssr: false }); function formatCurrency(amount) { if (!amount || isNaN(amount)) return '0'; return Number(amount).toLocaleString('ar-SA'); } function buildImageUrl(img) { if (!img) return ''; const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : ''; if (img.startsWith('http')) return img; return `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`; } const serviceLabels = { Electricity: 'كهرباء', Internet: 'إنترنت', Heating: 'تدفئة', Water: 'ماء', Pool: 'مسبح', PrivateGarden: 'حديقة خاصة', Parking: 'موقف سيارات', Security247: 'حراسة 24 س', CentralHeating: 'تدفئة مركزية', CentralAirConditioning: 'تكييف مركزي', EquippedKitchen: 'مطبخ مجهز', MaidsRoom: 'غرفة خادمة', Elevator: 'مصعد', Gym: 'نادي رياضي', Sauna: 'ساونا', Jacuzzi: 'جاكوزي', Balcony: 'بلكونة', Rooftop: 'سطح', Furnished: 'مفروش', AirConditioning: 'تكييف', SatelliteTV: 'تلفاز', Fireplace: 'مدفأة', StudyRoom: 'غرفة دراسة', Storage: 'مستودع', Laundry: 'غرفة غسيل', SmartHome: 'منزل ذكي', }; const termLabels = { NoSmoking: 'ممنوع التدخين', NoAnimals: 'ممنوع الحيوانات الأليفة', NoParties: 'ممنوع الحفلات', NoAlcohol: 'ممنوع الكحول', SuitableForChildren: 'مناسب للأطفال', SuitableForFamilies: 'مناسب للعائلات', SuitableForStudents: 'مناسب للطلاب', SuitableForElderly: 'مناسب لكبار السن', OnlyFemales: 'إناث فقط', OnlyMales: 'ذكور فقط', }; const proximityLabels = { School: 'مدرسة', Hospital: 'مستشفى', Restaurant: 'مطعم', University: 'جامعة', Park: 'حديقة', Mall: 'مركز تسوق', Supermarket: 'سوبر ماركت', Pharmacy: 'صيدلية', Mosque: 'مسجد', Bank: 'بنك', Airport: 'مطار', BusStation: 'موقف باص', }; function mapApiDetail(item) { if (!item) return null; const info = item.propertyInformation || {}; const details = typeof info.detailsJSON === 'object' && info.detailsJSON ? info.detailsJSON : (() => { try { return JSON.parse(info.detailsJSON || '{}'); } catch { return {}; } })(); const isRent = item.dailyRent != null || item.monthlyRent != null; const dailyPrice = item.dailyRent || 0; const monthlyPrice = item.monthlyRent || 0; const salePrice = item.price || 0; const price = isRent ? (monthlyPrice || dailyPrice) : salePrice; const priceUnit = isRent ? (monthlyPrice ? 'monthly' : 'daily') : 'sale'; const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment'; const typeLabel = { apartment: 'شقة', villa: 'فيلا', house: 'بيت', sweet: 'سويت', studio: 'استوديو', office: 'مكتب', shop: 'متجر', warehouse: 'مستودع', farms: 'مزرعة', room: 'غرفة ضمن شقة' }[propType] || 'عقار'; const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available'; const statusLabel = { available: 'متاح', booked: 'محجوز', notAvailable: 'غير متاح', maintenance: 'صيانة' }[status] || 'متاح'; const rawImages = Array.isArray(info.images) ? info.images : []; const photosFallback = Array.isArray(details.photos) ? details.photos : []; const allRaw = rawImages.length > 0 ? rawImages : photosFallback; const images = allRaw.length > 0 ? allRaw.map(buildImageUrl) : []; // Normalize services: Flutter sends list of strings or object with boolean values const rawServices = details.services || {}; const serviceList = Array.isArray(rawServices) ? rawServices : Object.keys(rawServices).filter(k => rawServices[k]); const serviceDetails = details.serviceDetails || {}; const services = {}; serviceList.forEach(s => { services[s] = serviceDetails[s] || true; }); const rawTerms = details.terms || {}; const terms = Array.isArray(rawTerms) ? rawTerms.reduce((acc, t) => ({ ...acc, [t]: true }), {}) : rawTerms; // Try multiple key aliases like Flutter does const floor = details.floorNumber ?? details.floor ?? 0; const salons = details.numberOfSalons ?? details.salonsCount ?? details.salons ?? 0; const balconies = details.numberOfBalconies ?? details.balconiesCount ?? details.balconies ?? 0; const rawProximity = details.nearbyDistances || details.proximity || {}; const proximity = {}; Object.entries(rawProximity).forEach(([k, v]) => { if (!v) return; const normalizedKey = k.charAt(0).toUpperCase() + k.slice(1); proximity[normalizedKey] = v; }); const roomDetails = details.room || details.roomDetails || {}; const displayType = details.displayType || (isRent ? (monthlyPrice && dailyPrice ? 'Both' : monthlyPrice ? 'Monthly' : 'Daily') : 'Sale'); const propertyCondition = details.propertyCondition || ''; const furnished = propertyCondition.toLowerCase().includes('furniture') ? propertyCondition.toLowerCase() === 'withfurniture' : !!item.isFurnished; return { id: item.id, propertyInformationId: info.id, title: details.description || info.address || `عقار #${item.id}`, description: details.description || info.description || '', type: propType, typeLabel, price, priceUnit, priceDisplay: { daily: dailyPrice, monthly: monthlyPrice, sale: salePrice }, isRent, displayType, propertyCondition, furnished, location: { city: extractCity(info.address) || 'دمشق', address: info.address || '', lat: parseFloat(info.cordsX) || 0, lng: parseFloat(info.cordsY) || 0, }, bedrooms: info.numberOfBedRooms || 0, bathrooms: info.numberOfBathRooms || 0, area: info.space || 0, floor, salons, balconies, images, status, statusLabel, services, terms, proximity, roomDetails, details, bookedCount: details.bookedCount || 0, deposit: item.deposit || 0, currencyId: item.currencyId, isSmokeAllow: item.isSmokeAllow, isVisitorAllow: item.isVisitorAllow, specializedFor: item.specializedFor, ownerId: info.ownerId ?? info.userId ?? item.ownerId ?? item.userId ?? null, _raw: item, }; } export default function PropertyDetailsPage() { const params = useParams(); const router = useRouter(); const { isFavorite, addFavorite, removeFavorite } = useFavorites(); const [property, setProperty] = useState(null); const [loading, setLoading] = useState(true); const [currentImage, setCurrentImage] = useState(0); const [showLoginDialog, setShowLoginDialog] = useState(false); const [bookingDates, setBookingDates] = useState({ start: '', end: '' }); const [bookingLoading, setBookingLoading] = useState(false); const [bookingError, setBookingError] = useState(null); const [bookingSuccess, setBookingSuccess] = useState(false); const [availableRanges, setAvailableRanges] = useState([]); const [bookingStep, setBookingStep] = useState('entry'); const [selectedStart, setSelectedStart] = useState(null); const [selectedEnd, setSelectedEnd] = useState(null); const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth()); const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear()); const [pricingMode, setPricingMode] = useState('daily'); const [isOwnProperty, setIsOwnProperty] = useState(false); const [favLoading, setFavLoading] = useState(false); const [avgRating, setAvgRating] = useState(null); const [showRatingForm, setShowRatingForm] = useState(false); const [showContact, setShowContact] = useState(false); const [contactInfo, setContactInfo] = useState(null); const [ownerData, setOwnerData] = useState(null); useEffect(() => { const id = params.id; if (!id) return; setLoading(true); async function fetchProperty() { try { let data = null; try { data = await getRentProperty(id); } catch {} if (!data) { try { data = await getSalePropertyById(id) || await getSaleProperty(id); } catch {} } if (!data) { try { data = await getSaleProperty(id); } catch {} } if (data) { const mapped = mapApiDetail(data); setProperty(mapped); if (mapped) fetchAvgRating(mapped.id); if (mapped && mapped.isRent) { try { const propInfoId = mapped._raw?.propertyInformationId || mapped.id; const ranges = await getAvailableDateRanges(propInfoId); if (ranges && Array.isArray(ranges)) { setAvailableRanges(ranges); } } catch (e) { console.warn('Failed to fetch date ranges', e); } } // Check if current user owns this property via their own listings if (AuthService.isAuthenticated() && AuthService.isOwner()) { try { const [myRent, mySale] = await Promise.allSettled([ getMyRentListings(), getMySaleListings(), ]); const myPropIds = new Set(); const collectIds = (result) => { if (result.status !== 'fulfilled' || !result.value) return; const list = Array.isArray(result.value) ? result.value : [result.value]; list.filter(Boolean).forEach(p => { const info = p.propertyInformation || {}; if (info.id) myPropIds.add(Number(info.id)); if (p.id) myPropIds.add(Number(p.id)); }); }; collectIds(myRent); collectIds(mySale); const propInfoId = mapped._raw?.propertyInformation?.id; if (myPropIds.has(Number(mapped.id)) || (propInfoId && myPropIds.has(Number(propInfoId)))) { setIsOwnProperty(true); } } catch (e) { console.warn('[OwnerCheck] failed:', e); } } } } catch (err) { console.error('[PropertyDetail] Failed:', err); } finally { setLoading(false); } } fetchProperty(); }, [params.id]); const fetchAvgRating = async (propId) => { try { const avg = await getPropertyAverageRating(propId); setAvgRating(avg); } catch {} }; const fetchContactInfo = async () => { if (!property) return; try { const info = await getOwnerContactInformation(property._raw?.propertyInformationId || property.id); setContactInfo(info); setShowContact(true); } catch (err) { toast.error('فشل تحميل معلومات الاتصال'); } }; const handleFavorite = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; } if (!property) return; setFavLoading(true); try { if (isFavorite(property.id)) { await removeFavorite(property.id); } else { await addFavorite(property.id); } } catch (err) { toast.error('حدث خطأ'); } finally { setFavLoading(false); } }; const handleBookNow = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; } if (!bookingDates.start || !bookingDates.end) { setBookingError('يرجى تحديد تاريخ البداية والنهاية'); return; } setBookingLoading(true); setBookingError(null); try { await bookReservation(property._raw?.propertyInformationId || property.id, bookingDates.start, bookingDates.end); setBookingSuccess(true); toast.success('تم إرسال طلب الحجز بنجاح'); } catch (err) { setBookingError(err.message || 'فشل الحجز'); } finally { setBookingLoading(false); } }; const MONTHS_AR = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر']; const DAYS_AR = ['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س']; const availableDatesSet = useMemo(() => { const dates = new Set(); if (!Array.isArray(availableRanges)) return dates; availableRanges.forEach(r => { const start = new Date(r.startDate || r.start); const end = new Date(r.endDate || r.end); for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { dates.add(d.toISOString().split('T')[0]); } }); return dates; }, [availableRanges]); const isDateAvailable = (dateStr) => availableDatesSet.has(dateStr); const isPastDate = (dateStr) => { const today = new Date(); today.setHours(0, 0, 0, 0); return new Date(dateStr) < today; }; const handleDayClick = (dateStr) => { if (bookingStep === 'entry') { setSelectedStart(dateStr); setSelectedEnd(null); setBookingStep('exit'); } else { if (new Date(dateStr) <= new Date(selectedStart)) { setSelectedStart(dateStr); setSelectedEnd(null); setBookingStep('exit'); } else { setSelectedEnd(dateStr); setBookingStep('entry'); } } }; const handleBookingConfirm = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); return; } if (!selectedStart || !selectedEnd) { setBookingError('يرجى تحديد تاريخ البداية والنهاية'); return; } setBookingLoading(true); setBookingError(null); try { const propInfoId = property._raw?.propertyInformationId || property.id; const startDate = new Date(selectedStart + 'T00:00:00.000').toISOString(); const endDate = new Date(selectedEnd + 'T00:00:00.000').toISOString(); await bookReservation(propInfoId, startDate, endDate); setBookingSuccess(true); toast.success('تم إرسال طلب الحجز بنجاح'); } catch (err) { setBookingError(err.message || 'فشل الحجز'); } finally { setBookingLoading(false); } }; const navigateMonth = (delta) => { let month = calendarMonth + delta; let year = calendarYear; if (month < 0) { month = 11; year--; } if (month > 11) { month = 0; year++; } setCalendarMonth(month); setCalendarYear(year); }; const handleRatingSuccess = () => { setShowRatingForm(false); if (property) fetchAvgRating(property.id); }; if (loading) { return (
جاري تحميل العقار...
لم يتم العثور على العقار المطلوب
لا توجد صور
{property.description}
شارك تجربتك مع المستأجرين الآخرين
لا يمكنك حجز عقارك الخاص
سيتم مراجعة طلبك من قبل المالك
للحجز أو إضافة المفضلة، سجل دخولك.
تسجيل الدخول إنشاء حساب