diff --git a/app/ClientLayout.js b/app/ClientLayout.js index 0707050..96da8eb 100644 --- a/app/ClientLayout.js +++ b/app/ClientLayout.js @@ -36,6 +36,8 @@ import { Clock, Users, DollarSign, + Star, + FileText, } from "lucide-react"; import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; @@ -366,6 +368,48 @@ export default function ClientLayout({ children }) { + setShowUserMenu(false)} + > + +
+

عقاراتي المحجوزة

+

+ عرض وتقييم العقارات المحجوزة +

+
+ + + setShowUserMenu(false)} + > + +
+

تقييماتي

+

+ التقييمات التي تلقيتها +

+
+ + + setShowUserMenu(false)} + > + +
+

الإعدادات

+

+ إعدادات الحساب والأمان +

+
+ + {isOwner && ( <>
@@ -439,6 +483,20 @@ export default function ClientLayout({ children }) {

+ + setShowUserMenu(false)} + > + +
+

دفتر الحسابات

+

+ سجل المعاملات المالية +

+
+ )} @@ -535,6 +593,48 @@ export default function ClientLayout({ children }) {

+ + setShowUserMenu(false)} + > + +
+

عقاراتي المحجوزة

+

+ عرض وتقييم العقارات المحجوزة +

+
+ + + setShowUserMenu(false)} + > + +
+

تقييماتي

+

+ التقييمات التي تلقيتها +

+
+ + + setShowUserMenu(false)} + > + +
+

البلاغات

+

+ تقديم ومتابعة البلاغات +

+
+ )} diff --git a/app/account-verification/page.js b/app/account-verification/page.js new file mode 100644 index 0000000..52b1674 --- /dev/null +++ b/app/account-verification/page.js @@ -0,0 +1,350 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { + Mail, + Phone, + Shield, + ChevronLeft, + Loader2, + CheckCircle, + XCircle, + Send, + Key +} from 'lucide-react'; +import toast, { Toaster } from 'react-hot-toast'; +import AuthService from '../services/AuthService'; +import { sendEmailOTP, sendPhoneOTP, verifyEmail, verifyPhone } from '../utils/api'; + +export default function AccountVerificationPage() { + const router = useRouter(); + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const [emailCode, setEmailCode] = useState(''); + const [phoneCode, setPhoneCode] = useState(''); + const [sendingEmailOTP, setSendingEmailOTP] = useState(false); + const [sendingPhoneOTP, setSendingPhoneOTP] = useState(false); + const [verifyingEmail, setVerifyingEmail] = useState(false); + const [verifyingPhone, setVerifyingPhone] = useState(false); + const [emailVerified, setEmailVerified] = useState(false); + const [phoneVerified, setPhoneVerified] = useState(false); + const [showEmailInput, setShowEmailInput] = useState(false); + const [showPhoneInput, setShowPhoneInput] = useState(false); + + useEffect(() => { + const authUser = AuthService.getUser(); + if (authUser) { + setUser(authUser); + setIsLoading(false); + } else { + router.push('/login'); + } + }, [router]); + + const handleSendEmailOTP = async () => { + setSendingEmailOTP(true); + try { + await sendEmailOTP(); + toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني'); + setShowEmailInput(true); + } catch (err) { + toast.error(err.message || 'فشل إرسال رمز التحقق'); + } finally { + setSendingEmailOTP(false); + } + }; + + const handleVerifyEmail = async () => { + if (!emailCode.trim()) { + toast.error('الرجاء إدخال رمز التحقق'); + return; + } + setVerifyingEmail(true); + try { + await verifyEmail(emailCode.trim()); + setEmailVerified(true); + setShowEmailInput(false); + toast.success('تم التحقق من البريد الإلكتروني بنجاح'); + } catch (err) { + toast.error(err.message || 'رمز التحقق غير صحيح'); + } finally { + setVerifyingEmail(false); + } + }; + + const handleSendPhoneOTP = async () => { + setSendingPhoneOTP(true); + try { + await sendPhoneOTP(); + toast.success('تم إرسال رمز التحقق إلى هاتفك'); + setShowPhoneInput(true); + } catch (err) { + toast.error(err.message || 'فشل إرسال رمز التحقق'); + } finally { + setSendingPhoneOTP(false); + } + }; + + const handleVerifyPhone = async () => { + if (!phoneCode.trim()) { + toast.error('الرجاء إدخال رمز التحقق'); + return; + } + setVerifyingPhone(true); + try { + await verifyPhone(phoneCode.trim()); + setPhoneVerified(true); + setShowPhoneInput(false); + toast.success('تم التحقق من رقم الهاتف بنجاح'); + } catch (err) { + toast.error(err.message || 'رمز التحقق غير صحيح'); + } finally { + setVerifyingPhone(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1 } + } + }; + + const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 } + }; + + return ( +
+ + +
+ + + + +

التحقق من الحساب

+
+ + + +
+ +

البريد الإلكتروني

+ {emailVerified && ( + + + موثق + + )} +
+
+
+
+

{user?.email || 'بريد إلكتروني غير مسجل'}

+

+ {emailVerified + ? 'بريدك الإلكتروني موثق ويمكنك استخدامه لتسجيل الدخول' + : 'يرجى توثيق بريدك الإلكتروني لحماية حسابك'} +

+
+ {!emailVerified && !showEmailInput && ( + + )} +
+ + {showEmailInput && ( + +

أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى بريدك الإلكتروني

+
+ setEmailCode(e.target.value)} + placeholder="رمز التحقق" + maxLength={6} + className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest" + /> + +
+ +
+ )} + + {emailVerified && ( +
+ + تم توثيق البريد الإلكتروني بنجاح +
+ )} +
+
+ + +
+ +

رقم الهاتف

+ {phoneVerified && ( + + + موثق + + )} +
+
+
+
+

{user?.phone || 'رقم هاتف غير مسجل'}

+

+ {phoneVerified + ? 'رقم هاتفك موثق ويمكنك استخدامه لتسجيل الدخول' + : 'يرجى توثيق رقم هاتفك لحماية حسابك'} +

+
+ {!phoneVerified && !showPhoneInput && ( + + )} +
+ + {showPhoneInput && ( + +

أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى هاتفك

+
+ setPhoneCode(e.target.value)} + placeholder="رمز التحقق" + maxLength={6} + className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none text-center text-lg tracking-widest" + /> + +
+ +
+ )} + + {phoneVerified && ( +
+ + تم توثيق رقم الهاتف بنجاح +
+ )} +
+
+ + +
+ +

لماذا يجب توثيق حسابك؟

+
+
    +
  • + + حماية حسابك من الوصول غير المصرح به +
  • +
  • + + استعادة كلمة المرور بسهولة في حال فقدانها +
  • +
  • + + زيادة الثقة مع مالكي العقارات والمستأجرين +
  • +
+
+
+
+
+ ); +} diff --git a/app/auth/choose-role/page.js b/app/auth/choose-role/page.js index b27b832..b926d4a 100644 --- a/app/auth/choose-role/page.js +++ b/app/auth/choose-role/page.js @@ -2,7 +2,7 @@ import { motion } from 'framer-motion'; import Link from 'next/link'; -import { Home, Building, ArrowLeft } from 'lucide-react'; +import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react'; export default function ChooseRolePage() { const containerVariants = { @@ -96,7 +96,7 @@ export default function ChooseRolePage() {

-
+
+ + + +
+ + +
+ + + + +

وكيل عقاري

+

+ تدير عقارات للغير؟ انضم كوسيط عقاري واحصل على فرص مميزة +

+ +
    +
  • + + إدارة عقارات العملاء +
  • +
  • + + عمولات وأرباح مضمونة +
  • +
  • + + لوحة تحكم متكاملة +
  • +
+ + + إنشاء حساب وكيل + +
+
+ +
+ {cfg.label} + + ); +} + +function ReservationCard({ reservation: r, onRate }) { + const isCompleted = STATUS_MAP[r.status] === 'completed'; + const imgSrc = r.propertyImage || r._prop?.images?.[0] || (r.propertyInfo?.images?.[0]); + const imageUrl = imgSrc ? `${API_BASE}${imgSrc}` : null; + const address = r.propertyAddress || r._prop?.address || ''; + const beds = r._prop?.numberOfBedRooms ?? r.numberOfBedRooms ?? 0; + const baths = r._prop?.numberOfBathRooms ?? r.numberOfBathRooms ?? 0; + + return ( + +
+
+
+ + عقار #{r.propertyId || r.id} +
+ +
+ + {imageUrl ? ( +
+ +
+ ) : ( +
+ +
+ )} + + {address && ( +
+ + {address} +
+ )} + + {(beds > 0 || baths > 0) && ( +
+ {beds > 0 && {beds} غرف} + {baths > 0 && {baths} حمامات} +
+ )} + +
+
+ +
من
+
{new Date(r.startDate).toLocaleDateString('ar')}
+
+
+ +
إلى
+
{new Date(r.endDate).toLocaleDateString('ar')}
+
+
+ +
+
{r.totalPrice?.toLocaleString() ?? '—'}
+
السعر الإجمالي
+
+ +
+ + رقم الحجز: #{r.id} +
+ + {isCompleted && ( + + )} +
+
+ ); +} + +export default function BookedPropertiesPage() { + const [reservations, setReservations] = useState([]); + const [loading, setLoading] = useState(true); + const [ratingReservation, setRatingReservation] = useState(null); + const [expandedId, setExpandedId] = useState(null); + + useEffect(() => { + if (!AuthService.getToken()) { + toast.error('يرجى تسجيل الدخول أولاً'); + setLoading(false); + return; + } + loadReservations(); + }, []); + + const loadReservations = useCallback(async () => { + try { + const data = await getUserReservations(); + setReservations(Array.isArray(data) ? data : []); + } catch (err) { + console.error(err); + toast.error('فشل تحميل الحجوزات'); + setReservations([]); + } finally { + setLoading(false); + } + }, []); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ +
+
+

العقارات المحجوزة

+

لديك {reservations.length} حجز

+
+ {reservations.length > 0 && ( + + )} +
+
+ + {reservations.length === 0 ? ( + + +

لا توجد حجوزات

+

لم تقم بحجز أي عقار حتى الآن

+
+ ) : ( +
+ {reservations.map((r, i) => ( + setRatingReservation(r)} /> + ))} +
+ )} + + {reservations.filter(r => STATUS_MAP[r.status] === 'completed').length > 0 && ( + + +
+

تقييم العقارات

+

يمكنك تقييم العقارات المنتهية حجزها لمساعدة المستأجرين الآخرين

+
+
+ )} +
+ + + {ratingReservation && ( + setRatingReservation(null)}> + e.stopPropagation()}> +
+ + { setRatingReservation(null); toast.success('تم إرسال التقييم بنجاح!'); }} + onCancel={() => setRatingReservation(null)} + /> +
+
+
+ )} +
+
+ ); +} diff --git a/app/change-password/page.js b/app/change-password/page.js new file mode 100644 index 0000000..1bfeeca --- /dev/null +++ b/app/change-password/page.js @@ -0,0 +1,207 @@ +'use client'; + +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { useRouter } from 'next/navigation'; +import toast, { Toaster } from 'react-hot-toast'; +import { Lock, Eye, EyeOff, ArrowLeft, Shield } from 'lucide-react'; +import { changePassword } from '../utils/api'; +import AuthService from '../services/AuthService'; + +export default function ChangePasswordPage() { + const router = useRouter(); + const [form, setForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' }); + const [show, setShow] = useState({ old: false, new: false, confirm: false }); + const [isLoading, setIsLoading] = useState(false); + + const handleChange = (field) => (e) => setForm({ ...form, [field]: e.target.value }); + + const toggleShow = (field) => setShow({ ...show, [field]: !show[field] }); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!AuthService.isAuthenticated()) { + toast.error('يرجى تسجيل الدخول أولاً'); + router.push('/login'); + return; + } + + if (form.newPassword !== form.confirmPassword) { + toast.error('كلمة المرور الجديدة وتأكيدها غير متطابقتين'); + return; + } + + if (form.newPassword.length < 6) { + toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل'); + return; + } + + setIsLoading(true); + try { + await changePassword(form.oldPassword, form.newPassword); + toast.success('تم تغيير كلمة المرور بنجاح'); + setTimeout(() => router.push('/profile'), 1200); + } catch (err) { + toast.error(err?.message || 'فشل تغيير كلمة المرور'); + } finally { + setIsLoading(false); + } + }; + + const inputClass = "w-full pr-12 pl-4 py-3 bg-gray-50 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 transition-all"; + + return ( +
+ + + + + + + +
+
+ + + + + تغيير كلمة المرور + + + أدخل كلمة المرور الحالية والجديدة + +
+ +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+ + + {isLoading ? ( +
+
+ جاري الحفظ... +
+ ) : ( + 'تغيير كلمة المرور' + )} + + +
+ +
+ ); +} diff --git a/app/components/FloatingSidebar.js b/app/components/FloatingSidebar.js index dcd938b..ebfd503 100644 --- a/app/components/FloatingSidebar.js +++ b/app/components/FloatingSidebar.js @@ -3,7 +3,7 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import Link from 'next/link'; -import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react'; +import { Heart, Bell, CreditCard, Shield, UserPlus, Settings, CalendarDays, Star, FileText } from 'lucide-react'; import { useFavorites } from '@/app/contexts/FavoritesContext'; import { useNotifications } from '@/app/contexts/NotificationsContext'; @@ -182,6 +182,74 @@ export default function FloatingSidebar({ isRTL, isAdmin }) { {renderTooltip('payments', 'المدفوعات')}
+ showTooltip('booked')} + onMouseLeave={hideTooltip} + > + + + + {renderTooltip('booked', 'حجوزاتي')} + + showTooltip('myRates')} + onMouseLeave={hideTooltip} + > + + + + {renderTooltip('myRates', 'تقييماتي')} + + showTooltip('reports')} + onMouseLeave={hideTooltip} + > + + + + {renderTooltip('reports', 'البلاغات')} + + showTooltip('settings')} + onMouseLeave={hideTooltip} + > + + + + {renderTooltip('settings', 'الإعدادات')} + )}
diff --git a/app/components/NotificationHandler.js b/app/components/NotificationHandler.js index 820c25f..44a200b 100644 --- a/app/components/NotificationHandler.js +++ b/app/components/NotificationHandler.js @@ -4,6 +4,7 @@ import { useEffect, useState, useRef } from "react"; import { initializeApp, getApps } from "firebase/app"; import { getMessaging, getToken, onMessage } from "firebase/messaging"; import AuthService from "../services/AuthService"; +import { setFCMToken } from "../utils/api"; const firebaseConfig = { apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY", @@ -71,21 +72,7 @@ export default function NotificationHandler() { }); if (fcmToken) { - console.log("[FCM] Token:", fcmToken.substring(0, 20) + "..."); - - const authToken = AuthService.getToken(); - if (authToken) { - const apiBase = "https://45.93.137.91.nip.io/api"; - await fetch(`${apiBase}/User/SetFCMToken`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify({ token: fcmToken, deviceType: 2 }), - }); - console.log("[FCM] Token sent to backend"); - } + await setFCMToken(fcmToken, 2); } onMessage(messaging, (payload) => { diff --git a/app/components/ratings/CustomerRatingForm.js b/app/components/ratings/CustomerRatingForm.js new file mode 100644 index 0000000..c1483ff --- /dev/null +++ b/app/components/ratings/CustomerRatingForm.js @@ -0,0 +1,92 @@ +'use client'; + +import { useState } from 'react'; +import { X, Loader2 } from 'lucide-react'; +import toast from 'react-hot-toast'; +import StarRating from './StarRating'; +import { addCustomerRating } from '../../utils/ratings'; + +const RatingField = ({ label, value, onChange }) => ( +
+ + + {value === 0 &&

مطلوب

} +
+); + +export default function CustomerRatingForm({ reservationId, onSuccess, onCancel }) { + const [furnitureIntegrityRating, setFurnitureIntegrityRating] = useState(0); + const [termsComplianceRating, setTermsComplianceRating] = useState(0); + const [renterBehaviorRating, setRenterBehaviorRating] = useState(0); + const [comment, setComment] = useState(''); + const [loading, setLoading] = useState(false); + + const validate = () => { + if (furnitureIntegrityRating === 0) return 'الحفاظ على الأثاث'; + if (termsComplianceRating === 0) return 'الالتزام بالشروط'; + if (renterBehaviorRating === 0) return 'سلوك المستأجر'; + return null; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const missing = validate(); + if (missing) { + toast.error(`يرجى تقييم: ${missing}`); + return; + } + + setLoading(true); + try { + await addCustomerRating({ + reservationId, + furnitureIntegrityRating, + termsComplianceRating, + renterBehaviorRating, + comment: comment.trim() || null, + }); + toast.success('تم إرسال تقييم المستأجر بنجاح!'); + onSuccess?.(); + } catch (err) { + toast.error('حدث خطأ، حاول مرة أخرى'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

تقييم المستأجر

+ {onCancel && ( + + )} +
+
+ + + +
+ +