the best in the west is mouaz
All checks were successful
Build frontend / build (push) Successful in 55s
All checks were successful
Build frontend / build (push) Successful in 55s
This commit is contained in:
@ -36,6 +36,8 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Users,
|
Users,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
|
Star,
|
||||||
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@ -366,6 +368,48 @@ export default function ClientLayout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/booked-properties"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<CalendarDays className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">عقاراتي المحجوزة</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
عرض وتقييم العقارات المحجوزة
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/my-rates"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<Star className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">تقييماتي</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
التقييمات التي تلقيتها
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<Settings className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">الإعدادات</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
إعدادات الحساب والأمان
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-gray-100 my-2"></div>
|
<div className="border-t border-gray-100 my-2"></div>
|
||||||
@ -439,6 +483,20 @@ export default function ClientLayout({ children }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/owner/account-book"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<DollarSign className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">دفتر الحسابات</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
سجل المعاملات المالية
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -535,6 +593,48 @@ export default function ClientLayout({ children }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/booked-properties"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<CalendarDays className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">عقاراتي المحجوزة</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
عرض وتقييم العقارات المحجوزة
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/my-rates"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<Star className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">تقييماتي</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
التقييمات التي تلقيتها
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/reports"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
||||||
|
onClick={() => setShowUserMenu(false)}
|
||||||
|
>
|
||||||
|
<FileText className="w-5 h-5 text-amber-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">البلاغات</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
تقديم ومتابعة البلاغات
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
350
app/account-verification/page.js
Normal file
350
app/account-verification/page.js
Normal file
@ -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 (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 max-w-2xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex items-center gap-3 mb-8"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">التحقق من الحساب</h1>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
||||||
|
<Mail className="w-5 h-5 text-amber-600" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">البريد الإلكتروني</h2>
|
||||||
|
{emailVerified && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
موثق
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">{user?.email || 'بريد إلكتروني غير مسجل'}</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
{emailVerified
|
||||||
|
? 'بريدك الإلكتروني موثق ويمكنك استخدامه لتسجيل الدخول'
|
||||||
|
: 'يرجى توثيق بريدك الإلكتروني لحماية حسابك'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!emailVerified && !showEmailInput && (
|
||||||
|
<button
|
||||||
|
onClick={handleSendEmailOTP}
|
||||||
|
disabled={sendingEmailOTP}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{sendingEmailOTP ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
{sendingEmailOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showEmailInput && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى بريدك الإلكتروني</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={emailCode}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleVerifyEmail}
|
||||||
|
disabled={verifyingEmail}
|
||||||
|
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{verifyingEmail ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
تحقق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSendEmailOTP}
|
||||||
|
disabled={sendingEmailOTP}
|
||||||
|
className="text-xs text-amber-600 hover:text-amber-700"
|
||||||
|
>
|
||||||
|
إعادة إرسال الرمز
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{emailVerified && (
|
||||||
|
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
||||||
|
<CheckCircle className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">تم توثيق البريد الإلكتروني بنجاح</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center gap-3">
|
||||||
|
<Phone className="w-5 h-5 text-amber-600" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">رقم الهاتف</h2>
|
||||||
|
{phoneVerified && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full">
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
موثق
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">{user?.phone || 'رقم هاتف غير مسجل'}</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
{phoneVerified
|
||||||
|
? 'رقم هاتفك موثق ويمكنك استخدامه لتسجيل الدخول'
|
||||||
|
: 'يرجى توثيق رقم هاتفك لحماية حسابك'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!phoneVerified && !showPhoneInput && (
|
||||||
|
<button
|
||||||
|
onClick={handleSendPhoneOTP}
|
||||||
|
disabled={sendingPhoneOTP}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors text-sm disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{sendingPhoneOTP ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
{sendingPhoneOTP ? 'جاري الإرسال...' : 'إرسال رمز التحقق'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showPhoneInput && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-gray-500">أدخل الرمز المكون من 6 أرقام الذي تم إرساله إلى هاتفك</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={phoneCode}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleVerifyPhone}
|
||||||
|
disabled={verifyingPhone}
|
||||||
|
className="px-6 py-3 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{verifyingPhone ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
تحقق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSendPhoneOTP}
|
||||||
|
disabled={sendingPhoneOTP}
|
||||||
|
className="text-xs text-amber-600 hover:text-amber-700"
|
||||||
|
>
|
||||||
|
إعادة إرسال الرمز
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phoneVerified && (
|
||||||
|
<div className="flex items-center gap-2 text-green-600 bg-green-50 px-4 py-3 rounded-xl">
|
||||||
|
<CheckCircle className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">تم توثيق رقم الهاتف بنجاح</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants} className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 text-white">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<Shield className="w-6 h-6" />
|
||||||
|
<h3 className="text-lg font-semibold">لماذا يجب توثيق حسابك؟</h3>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 text-sm text-amber-50">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
|
حماية حسابك من الوصول غير المصرح به
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
|
استعادة كلمة المرور بسهولة في حال فقدانها
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
|
زيادة الثقة مع مالكي العقارات والمستأجرين
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Home, Building, ArrowLeft } from 'lucide-react';
|
import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
export default function ChooseRolePage() {
|
export default function ChooseRolePage() {
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
@ -96,7 +96,7 @@ export default function ChooseRolePage() {
|
|||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
whileHover={{ scale: 1.05, y: -5 }}
|
whileHover={{ scale: 1.05, y: -5 }}
|
||||||
@ -212,6 +212,64 @@ export default function ChooseRolePage() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
whileHover={{ scale: 1.05, y: -5 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="group cursor-pointer"
|
||||||
|
>
|
||||||
|
<Link href="/register/agent">
|
||||||
|
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-3xl p-8 text-white shadow-2xl shadow-purple-500/20 relative overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="absolute -top-20 -right-20 w-40 h-40 bg-white/10 rounded-full"
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
rotate: [0, 90, 0],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 8, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<motion.div
|
||||||
|
className="w-20 h-20 bg-white/20 rounded-2xl flex items-center justify-center mb-6 backdrop-blur-sm"
|
||||||
|
whileHover={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<Briefcase className="w-10 h-10 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h2 className="text-3xl font-bold mb-3">وكيل عقاري</h2>
|
||||||
|
<p className="text-purple-100 mb-6">
|
||||||
|
تدير عقارات للغير؟ انضم كوسيط عقاري واحصل على فرص مميزة
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="space-y-2 text-sm text-purple-100">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
|
إدارة عقارات العملاء
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
|
عمولات وأرباح مضمونة
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-white rounded-full" />
|
||||||
|
لوحة تحكم متكاملة
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="mt-8 w-full bg-white text-purple-600 py-3 rounded-xl font-bold hover:bg-purple-50 transition-colors"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
إنشاء حساب وكيل
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
|
|||||||
222
app/booked-properties/page.js
Normal file
222
app/booked-properties/page.js
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Home, Star, MapPin, Calendar, Clock, Check, X, Loader2,
|
||||||
|
User, MessageCircle, ChevronDown, Image as ImageIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import { getUserReservations } from '../utils/api';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
import PropertyRatingForm from '../components/ratings/PropertyRatingForm';
|
||||||
|
|
||||||
|
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
|
|
||||||
|
const STATUS_CONFIG = {
|
||||||
|
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
|
||||||
|
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300' },
|
||||||
|
depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300' },
|
||||||
|
depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800 border-green-300' },
|
||||||
|
completed: { label: 'منتهي', color: 'bg-teal-100 text-teal-800 border-teal-300' },
|
||||||
|
cancelled: { label: 'ملغي', color: 'bg-red-100 text-red-800 border-red-300' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
||||||
|
|
||||||
|
function StatusBadge({ code }) {
|
||||||
|
const key = STATUS_MAP[code] || 'pending';
|
||||||
|
const cfg = STATUS_CONFIG[key];
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium border ${cfg.color}`}>
|
||||||
|
<Clock className="w-3 h-3" /> {cfg.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden">
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Home className="w-5 h-5 text-amber-500" />
|
||||||
|
<span className="font-semibold text-gray-900">عقار #{r.propertyId || r.id}</span>
|
||||||
|
</div>
|
||||||
|
<StatusBadge code={r.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{imageUrl ? (
|
||||||
|
<div className="w-full h-44 rounded-xl overflow-hidden bg-gray-100">
|
||||||
|
<img src={imageUrl} alt="" className="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-44 rounded-xl bg-gray-100 flex items-center justify-center">
|
||||||
|
<ImageIcon className="w-12 h-12 text-gray-300" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{address && (
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 text-sm">
|
||||||
|
<MapPin className="w-4 h-4 flex-shrink-0" />
|
||||||
|
{address}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(beds > 0 || baths > 0) && (
|
||||||
|
<div className="flex gap-3 text-sm text-gray-600">
|
||||||
|
{beds > 0 && <span>{beds} غرف</span>}
|
||||||
|
{baths > 0 && <span>{baths} حمامات</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
|
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
||||||
|
<div className="text-xs text-gray-500">من</div>
|
||||||
|
<div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 p-3 rounded-xl text-center">
|
||||||
|
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
||||||
|
<div className="text-xs text-gray-500">إلى</div>
|
||||||
|
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 p-3 rounded-xl text-center">
|
||||||
|
<div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
|
||||||
|
<div className="text-xs text-gray-500">السعر الإجمالي</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||||
|
<User className="w-3 h-3" />
|
||||||
|
<span>رقم الحجز: #{r.id}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCompleted && (
|
||||||
|
<button onClick={onRate}
|
||||||
|
className="w-full bg-amber-500 hover:bg-amber-600 text-white py-2.5 rounded-xl text-sm font-medium transition flex items-center justify-center gap-2">
|
||||||
|
<Star className="w-4 h-4" /> تقييم العقار
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
||||||
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">العقارات المحجوزة</h1>
|
||||||
|
<p className="text-gray-600">لديك {reservations.length} حجز</p>
|
||||||
|
</div>
|
||||||
|
{reservations.length > 0 && (
|
||||||
|
<button onClick={() => setExpandedId(expandedId ? null : 'all')}
|
||||||
|
className="flex items-center gap-1 text-sm text-amber-600 hover:text-amber-700 transition">
|
||||||
|
<ChevronDown className={`w-4 h-4 transition-transform ${expandedId ? 'rotate-180' : ''}`} />
|
||||||
|
{expandedId ? 'طي الكل' : 'عرض الكل'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{reservations.length === 0 ? (
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
|
<Home className="w-16 h-16 text-amber-500 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
|
||||||
|
<p className="text-gray-500">لم تقم بحجز أي عقار حتى الآن</p>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{reservations.map((r, i) => (
|
||||||
|
<ReservationCard key={r.id || i} reservation={r} onRate={() => setRatingReservation(r)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reservations.filter(r => STATUS_MAP[r.status] === 'completed').length > 0 && (
|
||||||
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}
|
||||||
|
className="mt-8 bg-amber-50 border border-amber-200 rounded-xl p-4 flex items-start gap-3">
|
||||||
|
<MessageCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-amber-800 font-medium">تقييم العقارات</p>
|
||||||
|
<p className="text-amber-600 text-sm">يمكنك تقييم العقارات المنتهية حجزها لمساعدة المستأجرين الآخرين</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{ratingReservation && (
|
||||||
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
||||||
|
onClick={() => setRatingReservation(null)}>
|
||||||
|
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
|
||||||
|
className="w-full max-w-lg" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="relative">
|
||||||
|
<button onClick={() => setRatingReservation(null)}
|
||||||
|
className="absolute left-2 top-2 z-10 bg-white/80 rounded-full p-1 hover:bg-white transition shadow">
|
||||||
|
<X className="w-5 h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<PropertyRatingForm
|
||||||
|
reservationId={ratingReservation.id}
|
||||||
|
onSuccess={() => { setRatingReservation(null); toast.success('تم إرسال التقييم بنجاح!'); }}
|
||||||
|
onCancel={() => setRatingReservation(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
207
app/change-password/page.js
Normal file
207
app/change-password/page.js
Normal file
@ -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 (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="w-full max-w-md"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/profile')}
|
||||||
|
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span>العودة للملف الشخصي</span>
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||||
|
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200 }}
|
||||||
|
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<Shield className="w-8 h-8 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.h1
|
||||||
|
initial={{ y: 20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
className="text-3xl font-bold text-white mb-2"
|
||||||
|
>
|
||||||
|
تغيير كلمة المرور
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ y: 20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="text-amber-100"
|
||||||
|
>
|
||||||
|
أدخل كلمة المرور الحالية والجديدة
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
كلمة المرور الحالية
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={show.old ? 'text' : 'password'}
|
||||||
|
value={form.oldPassword}
|
||||||
|
onChange={handleChange('oldPassword')}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="أدخل كلمة المرور الحالية"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleShow('old')}
|
||||||
|
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{show.old ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
كلمة المرور الجديدة
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={show.new ? 'text' : 'password'}
|
||||||
|
value={form.newPassword}
|
||||||
|
onChange={handleChange('newPassword')}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="أدخل كلمة المرور الجديدة"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleShow('new')}
|
||||||
|
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{show.new ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
تأكيد كلمة المرور الجديدة
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={show.confirm ? 'text' : 'password'}
|
||||||
|
value={form.confirmPassword}
|
||||||
|
onChange={handleChange('confirmPassword')}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="أعد إدخال كلمة المرور الجديدة"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleShow('confirm')}
|
||||||
|
className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{show.confirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
||||||
|
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
||||||
|
className="w-full bg-gradient-to-l from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold text-lg hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-500/25"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span>جاري الحفظ...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'تغيير كلمة المرور'
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
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 { useFavorites } from '@/app/contexts/FavoritesContext';
|
||||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||||
|
|
||||||
@ -182,6 +182,74 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
|
|||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('payments', 'المدفوعات')}
|
{renderTooltip('payments', 'المدفوعات')}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="relative group"
|
||||||
|
variants={buttonVariants}
|
||||||
|
initial="rest"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onMouseEnter={() => showTooltip('booked')}
|
||||||
|
onMouseLeave={hideTooltip}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/booked-properties"
|
||||||
|
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<CalendarDays className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
|
</Link>
|
||||||
|
{renderTooltip('booked', 'حجوزاتي')}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="relative group"
|
||||||
|
variants={buttonVariants}
|
||||||
|
initial="rest"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onMouseEnter={() => showTooltip('myRates')}
|
||||||
|
onMouseLeave={hideTooltip}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/my-rates"
|
||||||
|
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<Star className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
|
</Link>
|
||||||
|
{renderTooltip('myRates', 'تقييماتي')}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="relative group"
|
||||||
|
variants={buttonVariants}
|
||||||
|
initial="rest"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onMouseEnter={() => showTooltip('reports')}
|
||||||
|
onMouseLeave={hideTooltip}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/reports"
|
||||||
|
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<FileText className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
|
</Link>
|
||||||
|
{renderTooltip('reports', 'البلاغات')}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="relative group"
|
||||||
|
variants={buttonVariants}
|
||||||
|
initial="rest"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onMouseEnter={() => showTooltip('settings')}
|
||||||
|
onMouseLeave={hideTooltip}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||||
|
</Link>
|
||||||
|
{renderTooltip('settings', 'الإعدادات')}
|
||||||
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useEffect, useState, useRef } from "react";
|
|||||||
import { initializeApp, getApps } from "firebase/app";
|
import { initializeApp, getApps } from "firebase/app";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||||
import AuthService from "../services/AuthService";
|
import AuthService from "../services/AuthService";
|
||||||
|
import { setFCMToken } from "../utils/api";
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
|
||||||
@ -71,21 +72,7 @@ export default function NotificationHandler() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (fcmToken) {
|
if (fcmToken) {
|
||||||
console.log("[FCM] Token:", fcmToken.substring(0, 20) + "...");
|
await setFCMToken(fcmToken, 2);
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(messaging, (payload) => {
|
onMessage(messaging, (payload) => {
|
||||||
|
|||||||
92
app/components/ratings/CustomerRatingForm.js
Normal file
92
app/components/ratings/CustomerRatingForm.js
Normal file
@ -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 }) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
|
||||||
|
<StarRating rating={value} onRatingChange={onChange} size={28} />
|
||||||
|
{value === 0 && <p className="text-xs text-red-500">مطلوب</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900">تقييم المستأجر</h3>
|
||||||
|
{onCancel && (
|
||||||
|
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<RatingField label="الحفاظ على الأثاث" value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
|
||||||
|
<RatingField label="الالتزام بالشروط" value={termsComplianceRating} onChange={setTermsComplianceRating} />
|
||||||
|
<RatingField label="سلوك المستأجر" value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">تعليق (اختياري)</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder="شارك تجربتك مع المستأجر..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 rounded-xl transition flex items-center justify-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
|
||||||
|
{loading ? 'جاري الإرسال...' : 'إرسال التقييم'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,33 +1,54 @@
|
|||||||
/**
|
/**
|
||||||
* BuildingType Enum
|
* BuildingType Enum
|
||||||
* Backend values are numeric (0, 1, 2)
|
* Backend values are numeric (0-8)
|
||||||
* Used in: PropertyInformation.buildingType
|
* Used in: PropertyInformation.buildingType
|
||||||
*/
|
*/
|
||||||
const BuildingType = Object.freeze({
|
const BuildingType = Object.freeze({
|
||||||
APARTMENT: 0,
|
APARTMENT: 0,
|
||||||
VILLA: 1,
|
VILLA: 1,
|
||||||
HOUSE: 2,
|
SWEET: 2,
|
||||||
|
ROOM: 3,
|
||||||
|
STUDIO: 4,
|
||||||
|
OFFICE: 5,
|
||||||
|
FARMS: 6,
|
||||||
|
SHOP: 7,
|
||||||
|
WAREHOUSE: 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map numeric value → Arabic label
|
|
||||||
const BuildingTypeLabels = Object.freeze({
|
const BuildingTypeLabels = Object.freeze({
|
||||||
[BuildingType.APARTMENT]: 'شقة',
|
[BuildingType.APARTMENT]: 'شقة',
|
||||||
[BuildingType.VILLA]: 'فيلا',
|
[BuildingType.VILLA]: 'فيلا',
|
||||||
[BuildingType.HOUSE]: 'بيت',
|
[BuildingType.SWEET]: 'سويت',
|
||||||
|
[BuildingType.ROOM]: 'غرفة',
|
||||||
|
[BuildingType.STUDIO]: 'استوديو',
|
||||||
|
[BuildingType.OFFICE]: 'مكتب',
|
||||||
|
[BuildingType.FARMS]: 'مزرعة',
|
||||||
|
[BuildingType.SHOP]: 'متجر',
|
||||||
|
[BuildingType.WAREHOUSE]: 'مستودع',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map numeric value → English key (for UI filters)
|
|
||||||
const BuildingTypeKeys = Object.freeze({
|
const BuildingTypeKeys = Object.freeze({
|
||||||
[BuildingType.APARTMENT]: 'apartment',
|
[BuildingType.APARTMENT]: 'apartment',
|
||||||
[BuildingType.VILLA]: 'villa',
|
[BuildingType.VILLA]: 'villa',
|
||||||
[BuildingType.HOUSE]: 'house',
|
[BuildingType.SWEET]: 'sweet',
|
||||||
|
[BuildingType.ROOM]: 'room',
|
||||||
|
[BuildingType.STUDIO]: 'studio',
|
||||||
|
[BuildingType.OFFICE]: 'office',
|
||||||
|
[BuildingType.FARMS]: 'farms',
|
||||||
|
[BuildingType.SHOP]: 'shop',
|
||||||
|
[BuildingType.WAREHOUSE]: 'warehouse',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reverse map: English key → numeric value
|
|
||||||
const BuildingTypeByKey = Object.freeze({
|
const BuildingTypeByKey = Object.freeze({
|
||||||
apartment: BuildingType.APARTMENT,
|
apartment: BuildingType.APARTMENT,
|
||||||
villa: BuildingType.VILLA,
|
villa: BuildingType.VILLA,
|
||||||
house: BuildingType.HOUSE,
|
sweet: BuildingType.SWEET,
|
||||||
|
room: BuildingType.ROOM,
|
||||||
|
studio: BuildingType.STUDIO,
|
||||||
|
office: BuildingType.OFFICE,
|
||||||
|
farms: BuildingType.FARMS,
|
||||||
|
shop: BuildingType.SHOP,
|
||||||
|
warehouse: BuildingType.WAREHOUSE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey };
|
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey };
|
||||||
|
|||||||
13
app/enums/CancellationReason.js
Normal file
13
app/enums/CancellationReason.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const CancellationReason = Object.freeze({
|
||||||
|
NONE: 0,
|
||||||
|
DEPOSIT_NOT_PAID: 1,
|
||||||
|
OWNER_CANCELED: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const CancellationReasonLabels = Object.freeze({
|
||||||
|
[CancellationReason.NONE]: 'بدون سبب',
|
||||||
|
[CancellationReason.DEPOSIT_NOT_PAID]: 'عدم دفع العربون',
|
||||||
|
[CancellationReason.OWNER_CANCELED]: 'إلغاء من المالك',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { CancellationReason, CancellationReasonLabels };
|
||||||
@ -5,16 +5,22 @@
|
|||||||
const Currency = Object.freeze({
|
const Currency = Object.freeze({
|
||||||
SYP: 1,
|
SYP: 1,
|
||||||
USD: 2,
|
USD: 2,
|
||||||
|
EUR: 3,
|
||||||
|
TRY: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
const CurrencyLabels = Object.freeze({
|
const CurrencyLabels = Object.freeze({
|
||||||
[Currency.SYP]: 'ليرة سورية',
|
[Currency.SYP]: 'ليرة سورية',
|
||||||
[Currency.USD]: 'دولار أمريكي',
|
[Currency.USD]: 'دولار أمريكي',
|
||||||
|
[Currency.EUR]: 'يورو',
|
||||||
|
[Currency.TRY]: 'ليرة تركية',
|
||||||
});
|
});
|
||||||
|
|
||||||
const CurrencySymbols = Object.freeze({
|
const CurrencySymbols = Object.freeze({
|
||||||
[Currency.SYP]: 'SYP',
|
[Currency.SYP]: 'SYP',
|
||||||
[Currency.USD]: 'USD',
|
[Currency.USD]: 'USD',
|
||||||
|
[Currency.EUR]: 'EUR',
|
||||||
|
[Currency.TRY]: 'TRY',
|
||||||
});
|
});
|
||||||
|
|
||||||
export { Currency, CurrencyLabels, CurrencySymbols };
|
export { Currency, CurrencyLabels, CurrencySymbols };
|
||||||
|
|||||||
7
app/enums/DeviceType.js
Normal file
7
app/enums/DeviceType.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const DeviceType = Object.freeze({
|
||||||
|
ANDROID: 'Android',
|
||||||
|
IOS: 'Ios',
|
||||||
|
WEB: 'Web',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { DeviceType };
|
||||||
11
app/enums/Language.js
Normal file
11
app/enums/Language.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const Language = Object.freeze({
|
||||||
|
ARABIC: 'Arabic',
|
||||||
|
ENGLISH: 'English',
|
||||||
|
});
|
||||||
|
|
||||||
|
const LanguageLabels = Object.freeze({
|
||||||
|
[Language.ARABIC]: 'العربية',
|
||||||
|
[Language.ENGLISH]: 'English',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Language, LanguageLabels };
|
||||||
@ -5,29 +5,26 @@
|
|||||||
*/
|
*/
|
||||||
const PropertyStatus = Object.freeze({
|
const PropertyStatus = Object.freeze({
|
||||||
AVAILABLE: 0,
|
AVAILABLE: 0,
|
||||||
BOOKED: 1,
|
NOT_AVAILABLE: 1,
|
||||||
MAINTENANCE: 2,
|
BOOKED: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map numeric value → Arabic label
|
|
||||||
const PropertyStatusLabels = Object.freeze({
|
const PropertyStatusLabels = Object.freeze({
|
||||||
[PropertyStatus.AVAILABLE]: 'متاح',
|
[PropertyStatus.AVAILABLE]: 'متاح',
|
||||||
|
[PropertyStatus.NOT_AVAILABLE]: 'غير متاح',
|
||||||
[PropertyStatus.BOOKED]: 'محجوز',
|
[PropertyStatus.BOOKED]: 'محجوز',
|
||||||
[PropertyStatus.MAINTENANCE]: 'صيانة',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map numeric value → English key (for UI filters)
|
|
||||||
const PropertyStatusKeys = Object.freeze({
|
const PropertyStatusKeys = Object.freeze({
|
||||||
[PropertyStatus.AVAILABLE]: 'available',
|
[PropertyStatus.AVAILABLE]: 'available',
|
||||||
|
[PropertyStatus.NOT_AVAILABLE]: 'notAvailable',
|
||||||
[PropertyStatus.BOOKED]: 'booked',
|
[PropertyStatus.BOOKED]: 'booked',
|
||||||
[PropertyStatus.MAINTENANCE]: 'maintenance',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reverse map: English key → numeric value
|
|
||||||
const PropertyStatusByKey = Object.freeze({
|
const PropertyStatusByKey = Object.freeze({
|
||||||
available: PropertyStatus.AVAILABLE,
|
available: PropertyStatus.AVAILABLE,
|
||||||
|
notAvailable: PropertyStatus.NOT_AVAILABLE,
|
||||||
booked: PropertyStatus.BOOKED,
|
booked: PropertyStatus.BOOKED,
|
||||||
maintenance: PropertyStatus.MAINTENANCE,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey };
|
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey };
|
||||||
|
|||||||
11
app/enums/SalePropertiesStatus.js
Normal file
11
app/enums/SalePropertiesStatus.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const SalePropertiesStatus = Object.freeze({
|
||||||
|
PENDING: 0,
|
||||||
|
CONFIRMED: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SalePropertiesStatusLabels = Object.freeze({
|
||||||
|
[SalePropertiesStatus.PENDING]: 'قيد الانتظار',
|
||||||
|
[SalePropertiesStatus.CONFIRMED]: 'مؤكد',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { SalePropertiesStatus, SalePropertiesStatusLabels };
|
||||||
11
app/enums/TransactionType.js
Normal file
11
app/enums/TransactionType.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const TransactionType = Object.freeze({
|
||||||
|
ONLINE: 0,
|
||||||
|
MANUAL: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const TransactionTypeLabels = Object.freeze({
|
||||||
|
[TransactionType.ONLINE]: 'أونلاين',
|
||||||
|
[TransactionType.MANUAL]: 'يدوي',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { TransactionType, TransactionTypeLabels };
|
||||||
@ -19,3 +19,8 @@ export { RentType, RentTypeLabels } from './RentType';
|
|||||||
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
|
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
|
||||||
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
|
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
|
||||||
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
|
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
|
||||||
|
export { DeviceType } from './DeviceType';
|
||||||
|
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';
|
||||||
|
export { TransactionType, TransactionTypeLabels } from './TransactionType';
|
||||||
|
export { CancellationReason, CancellationReasonLabels } from './CancellationReason';
|
||||||
|
export { Language, LanguageLabels } from './Language';
|
||||||
|
|||||||
143
app/faq/page.js
Normal file
143
app/faq/page.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { HelpCircle, ChevronDown, Search } from 'lucide-react';
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
question: 'كيفية البحث عن عقار؟',
|
||||||
|
answer: 'يمكنك البحث عن عقار من خلال استخدام شريط البحث في الصفحة الرئيسية، حيث يمكنك تحديد المدينة ونوع العقار والسعر. كما يمكنك استخدام الفلاتر المتقدمة لتحديد عدد الغرف والحمامات والمساحة وطرق الدفع. يمكنك أيضاً تصفح العقارات المميزة والتوصيات المقدمة خصيصاً لك.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'كيف أحجز عقاراً؟',
|
||||||
|
answer: 'بعد العثور على العقار المناسب، يمكنك النقر على "حجز" واختيار تواريخ الإقامة. سيطلب منك تسجيل الدخول أو إنشاء حساب إذا لم يكن لديك حساب. بعد ذلك، قم بتأكيد الحجز وسيتم إخطار المالك بطلبك. يمكنك متابعة حالة حجزك من صفحة الحجوزات.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'كيف أضيف عقاراً؟',
|
||||||
|
answer: 'لتسجيل عقارك على المنصة، يجب عليك أولاً إنشاء حساب مالك. بعد تسجيل الدخول، انتقل إلى لوحة التحكم واختر "إضافة عقار". قم بتعبئة جميع المعلومات المطلوبة مثل الموقع، المساحة، عدد الغرف، الصور، السعر، وغيرها من التفاصيل. بعد المراجعة، سيتم نشر عقارك على المنصة.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'كيف أتواصل مع المالك؟',
|
||||||
|
answer: 'بعد تسجيل الدخول، يمكنك التواصل مع المالك من خلال صفحة العقار بالضغط على زر "تواصل مع المالك". ستظهر لك معلومات الاتصال بالمالك بما في ذلك رقم الهاتف والبريد الإلكتروني. يمكنك أيضاً مراسلة المالك مباشرة من خلال نظام المراسلة الداخلي في المنصة.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'ما هي سياسة الإلغاء؟',
|
||||||
|
answer: 'سياسة الإلغاء تختلف حسب كل عقار. بشكل عام، يمكن إلغاء الحجز قبل 48 ساعة من موعد الوصول للحصول على استرداد كامل. للإلغاء خلال أقل من 48 ساعة، قد يتم تطبيق رسوم إلغاء. ننصحك بمراجعة سياسة الإلغاء المدونة في صفحة العقار قبل تأكيد الحجز.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'كيف أقيم العقار؟',
|
||||||
|
answer: 'بعد انتهاء إقامتك، يمكنك تقييم العقار من خلال صفحة الحجوزات. اختر الحجز المناسب ثم اضغط على "تقييم". يمكنك إعطاء تقييم بالنجوم وكتابة تعليق يصف تجربتك. تقييمات المستخدمين تساعد الآخرين في اتخاذ القرار المناسب.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'هل يمكنني تغيير كلمة المرور؟',
|
||||||
|
answer: 'نعم، يمكنك تغيير كلمة المرور من خلال صفحة الملف الشخصي. اختر "تغيير كلمة المرور" وأدخل كلمة المرور الحالية ثم كلمة المرور الجديدة. يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل وتحتوي على حرف كبير وحرف صغير ورقم.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'كيف أحذف حسابي؟',
|
||||||
|
answer: 'لحذف حسابك نهائياً، يرجى التواصل مع فريق الدعم الفني من خلال صفحة الدعم. سنقوم بمعالجة طلبك خلال 24 ساعة. يرجى العلم أنه لا يمكن استعادة الحساب بعد حذفه، وسيتم فقدان جميع البيانات والحجوزات المرتبطة به.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function FAQPage() {
|
||||||
|
const [openIndex, setOpenIndex] = useState(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const filteredFaqs = faqs.filter(
|
||||||
|
(faq) =>
|
||||||
|
faq.question.includes(searchTerm) ||
|
||||||
|
faq.answer.includes(searchTerm)
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleFAQ = (index) => {
|
||||||
|
setOpenIndex(openIndex === index ? null : index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
|
<HelpCircle className="w-10 h-10 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">الأسئلة الشائعة</h1>
|
||||||
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
|
إجابات على أكثر الأسئلة شيوعاً حول استخدام المنصة
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="relative mb-8"
|
||||||
|
>
|
||||||
|
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="ابحث في الأسئلة الشائعة..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pr-12 pl-4 py-4 bg-white border border-gray-200 rounded-2xl shadow-sm focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredFaqs.map((faq, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFAQ(index)}
|
||||||
|
className="w-full flex items-center justify-between p-5 text-right"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-semibold text-gray-900 flex-1">
|
||||||
|
{faq.question}
|
||||||
|
</span>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: openIndex === index ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5 text-gray-500 shrink-0" />
|
||||||
|
</motion.div>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{openIndex === index && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<p className="px-5 pb-5 text-gray-600 leading-relaxed border-t border-gray-100 pt-4">
|
||||||
|
{faq.answer}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredFaqs.length === 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="text-center py-12"
|
||||||
|
>
|
||||||
|
<p className="text-gray-500 text-lg">لا توجد نتائج للبحث</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,30 +1,74 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Mail, ArrowLeft, CheckCircle } from 'lucide-react';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import { Mail, Key, Lock, CheckCircle, ArrowLeft, RefreshCw } from 'lucide-react';
|
||||||
|
import { requestForgetPasswordOtp, verifyForgetPasswordOtp } from '../utils/api';
|
||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleRequestOtp = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
if (!email.trim()) {
|
||||||
|
toast.error('يرجى إدخال البريد الإلكتروني');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await requestForgetPasswordOtp(email);
|
||||||
|
toast.success('تم إرسال رمز التحقق إلى بريدك الإلكتروني');
|
||||||
|
setStep(2);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err?.message || 'فشل إرسال رمز التحقق');
|
||||||
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsSubmitted(true);
|
}
|
||||||
}, 1500);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVerifyOtp = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!code.trim()) {
|
||||||
|
toast.error('يرجى إدخال رمز التحقق');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
toast.error('كلمة المرور الجديدة يجب أن تكون 6 أحرف على الأقل');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await verifyForgetPasswordOtp(email, code, newPassword);
|
||||||
|
toast.success('تم إعادة تعيين كلمة المرور بنجاح');
|
||||||
|
setTimeout(() => router.push('/login'), 1200);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err?.message || 'فشل التحقق من الرمز');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputClass = "w-full pr-12 pl-4 py-3 bg-white/10 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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center p-4 relative overflow-hidden">
|
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50 flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full opacity-20 blur-3xl animate-pulse"></div>
|
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-400 rounded-full opacity-20 blur-3xl animate-pulse"></div>
|
||||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-pink-500 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
|
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -38,14 +82,14 @@ export default function ForgotPasswordPage() {
|
|||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className="absolute -top-16 left-0"
|
className="absolute -top-16 left-0"
|
||||||
>
|
>
|
||||||
<Link href="/login" className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors group">
|
<Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
|
||||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||||
<span>العودة لتسجيل الدخول</span>
|
<span>العودة لتسجيل الدخول</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
|
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/60 overflow-hidden">
|
||||||
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-8 text-center">
|
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ y: 20, opacity: 0 }}
|
initial={{ y: 20, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
@ -57,70 +101,141 @@ export default function ForgotPasswordPage() {
|
|||||||
initial={{ y: 20, opacity: 0 }}
|
initial={{ y: 20, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="text-purple-100"
|
className="text-amber-100"
|
||||||
>
|
>
|
||||||
سنرسل لك رابط لإعادة تعيين كلمة المرور
|
{step === 1 ? 'أدخل بريدك الإلكتروني لاستلام رمز التحقق' : 'أدخل رمز التحقق وكلمة المرور الجديدة'}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
{!isSubmitted ? (
|
<AnimatePresence mode="wait">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
{step === 1 && (
|
||||||
<div>
|
<motion.form
|
||||||
<label className="block text-sm font-medium text-gray-200 mb-2">
|
key="step1"
|
||||||
البريد الإلكتروني
|
initial={{ opacity: 0, x: -30 }}
|
||||||
</label>
|
animate={{ opacity: 1, x: 0 }}
|
||||||
<div className="relative group">
|
exit={{ opacity: 0, x: 30 }}
|
||||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
onSubmit={handleRequestOtp}
|
||||||
<Mail className="w-5 h-5 text-gray-400 group-focus-within:text-purple-500 transition-colors" />
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
البريد الإلكتروني
|
||||||
|
</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="أدخل بريدك الإلكتروني"
|
||||||
|
dir="ltr"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-400 transition-all"
|
|
||||||
placeholder="أدخل بريدك الإلكتروني"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<motion.button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 text-white py-3 rounded-xl font-bold text-lg hover:from-purple-600 hover:to-pink-600 transition-all transform hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-purple-500/25"
|
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
||||||
|
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
||||||
|
className="w-full bg-gradient-to-l from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold text-lg hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-500/25"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span>جاري الإرسال...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'إرسال رمز التحقق'
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<motion.form
|
||||||
|
key="step2"
|
||||||
|
initial={{ opacity: 0, x: 30 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -30 }}
|
||||||
|
onSubmit={handleVerifyOtp}
|
||||||
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
<div>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
رمز التحقق
|
||||||
<span>جاري الإرسال...</span>
|
</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Key className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="أدخل رمز التحقق"
|
||||||
|
dir="ltr"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
'إرسال رابط الاستعادة'
|
|
||||||
)}
|
<div>
|
||||||
</button>
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
</form>
|
كلمة المرور الجديدة
|
||||||
) : (
|
</label>
|
||||||
<motion.div
|
<div className="relative group">
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
<Lock className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
|
||||||
className="text-center py-6"
|
</div>
|
||||||
>
|
<input
|
||||||
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
type="password"
|
||||||
<CheckCircle className="w-10 h-10 text-green-500" />
|
value={newPassword}
|
||||||
</div>
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
<h3 className="text-xl font-bold text-white mb-2">تم الإرسال بنجاح!</h3>
|
className={inputClass}
|
||||||
<p className="text-gray-300 mb-6">
|
placeholder="أدخل كلمة المرور الجديدة"
|
||||||
تم إرسال رابط استعادة كلمة المرور إلى {email}
|
required
|
||||||
</p>
|
minLength={6}
|
||||||
<Link
|
/>
|
||||||
href="/login"
|
</div>
|
||||||
className="inline-block px-6 py-3 bg-white/10 text-white rounded-xl hover:bg-white/20 transition-colors"
|
</div>
|
||||||
>
|
|
||||||
العودة لتسجيل الدخول
|
<motion.button
|
||||||
</Link>
|
type="submit"
|
||||||
</motion.div>
|
disabled={isLoading}
|
||||||
)}
|
whileHover={{ scale: isLoading ? 1 : 1.02 }}
|
||||||
|
whileTap={{ scale: isLoading ? 1 : 0.98 }}
|
||||||
|
className="w-full bg-gradient-to-l from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold text-lg hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-500/25"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span>جاري التحقق...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'إعادة تعيين كلمة المرور'
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
className="text-sm text-amber-600 hover:text-amber-700 transition-colors flex items-center justify-center gap-1 mx-auto"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
<span>تغيير البريد الإلكتروني</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
152
app/my-rates/page.js
Normal file
152
app/my-rates/page.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Star, User, Calendar, MessageSquare, ThumbsUp, Loader2 } from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import { getCustomerRatings } from '../utils/ratings';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
import StarRating from '../components/ratings/StarRating';
|
||||||
|
|
||||||
|
const RATING_FIELDS = [
|
||||||
|
{ key: 'cleanRating', label: 'النظافة' },
|
||||||
|
{ key: 'servicesRating', label: 'الخدمات' },
|
||||||
|
{ key: 'ownerBehaviorRating', label: 'سلوك المالك' },
|
||||||
|
{ key: 'experienceRating', label: 'التجربة العامة' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function calcOverall(ratings) {
|
||||||
|
if (!ratings || ratings.length === 0) return 0;
|
||||||
|
const total = ratings.reduce((sum, r) => {
|
||||||
|
const avg = RATING_FIELDS.reduce((s, f) => s + (Number(r[f.key]) || 0), 0) / RATING_FIELDS.length;
|
||||||
|
return sum + avg;
|
||||||
|
}, 0);
|
||||||
|
return total / ratings.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RatingCard({ rating }) {
|
||||||
|
const dateStr = rating.createdAt
|
||||||
|
? new Date(rating.createdAt).toLocaleDateString('ar')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
||||||
|
<div className="flex items-center gap-3 mb-4 pb-3 border-b border-gray-100">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
|
||||||
|
<User className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">{rating.ownerName || 'مالك العقار'}</p>
|
||||||
|
{dateStr && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-gray-400">
|
||||||
|
<Calendar className="w-3 h-3" /> {dateStr}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 mb-4">
|
||||||
|
{RATING_FIELDS.map(({ key, label }) => (
|
||||||
|
<div key={key} className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-600">{label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StarRating rating={Number(rating[key]) || 0} size={18} readOnly />
|
||||||
|
<span className="text-sm font-medium text-gray-700 w-6 text-left">
|
||||||
|
{Number(rating[key]) || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rating.comment && (
|
||||||
|
<div className="bg-gray-50 rounded-xl p-3 flex items-start gap-2">
|
||||||
|
<MessageSquare className="w-4 h-4 text-gray-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-gray-600">{rating.comment}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MyRatesPage() {
|
||||||
|
const [ratings, setRatings] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = AuthService.getUser();
|
||||||
|
if (!user || !user.id) {
|
||||||
|
toast.error('يرجى تسجيل الدخول أولاً');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadRatings(user.id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function loadRatings(userId) {
|
||||||
|
try {
|
||||||
|
const data = await getCustomerRatings(userId);
|
||||||
|
const list = Array.isArray(data) ? data : [];
|
||||||
|
setRatings(list);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('فشل تحميل التقييمات');
|
||||||
|
setRatings([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
||||||
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overall = calcOverall(ratings);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">تقييماتي</h1>
|
||||||
|
<p className="text-gray-600">التقييمات التي تلقيتها من مالكي العقارات</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{ratings.length > 0 && (
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-6 mb-8 text-white text-center shadow-lg">
|
||||||
|
<ThumbsUp className="w-8 h-8 mx-auto mb-2 opacity-80" />
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-1">
|
||||||
|
<StarRating rating={Math.round(overall)} size={24} readOnly />
|
||||||
|
</div>
|
||||||
|
<div className="text-4xl font-bold">{overall.toFixed(1)}</div>
|
||||||
|
<div className="text-amber-100 text-sm mt-1">المعدل العام</div>
|
||||||
|
<div className="text-amber-100 text-xs mt-2">
|
||||||
|
بناءً على {ratings.length} {ratings.length === 1 ? 'تقييم' : 'تقييمات'}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ratings.length === 0 ? (
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
|
<Star className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد تقييمات</h3>
|
||||||
|
<p className="text-gray-500">لم تقم باستئجار أي عقار بعد لتتلقى تقييمات</p>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{ratings.map((r, i) => (
|
||||||
|
<RatingCard key={r.id || i} rating={r} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle } from 'lucide-react';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle, CheckCheck, Loader2 } from 'lucide-react';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
import { getUserNotifications } from '@/app/utils/api';
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { notifications, unreadCount, isLoading } = useNotifications();
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -16,23 +19,48 @@ export default function NotificationsPage() {
|
|||||||
router.push('/login');
|
router.push('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
fetchNotifications();
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
const fetchNotifications = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await getUserNotifications();
|
||||||
|
const items = Array.isArray(data) ? data : [];
|
||||||
|
setNotifications(items);
|
||||||
|
setUnreadCount(items.length);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching notifications:', err);
|
||||||
|
setError(err.message || 'فشل تحميل الإشعارات');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const markAsRead = (id) => {
|
const markAsRead = (id) => {
|
||||||
// This will be handled by context if needed
|
setNotifications(prev =>
|
||||||
|
prev.map(n => (n.id === id ? { ...n, read: true } : n))
|
||||||
|
);
|
||||||
|
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = () => {
|
||||||
// This will be handled by context if needed
|
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
||||||
|
setUnreadCount(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<motion.div
|
||||||
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<Loader2 className="w-12 h-12 text-amber-500 mx-auto mb-4 animate-spin" />
|
||||||
<p className="text-gray-600">جاري التحميل...</p>
|
<p className="text-gray-600">جاري التحميل...</p>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -40,11 +68,23 @@ export default function NotificationsPage() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
|
||||||
<p className="text-gray-500">{error}</p>
|
<p className="text-gray-500 mb-4">{error}</p>
|
||||||
</div>
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={fetchNotifications}
|
||||||
|
className="px-6 py-2 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors"
|
||||||
|
>
|
||||||
|
إعادة المحاولة
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -52,48 +92,99 @@ export default function NotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<div className="flex justify-between items-center mb-8">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex justify-between items-center mb-8"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإشعارات</h1>
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإشعارات</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{unreadCount > 0 ? `لديك ${unreadCount} إشعار غير مقروء` : 'جميع الإشعارات مقروءة'}
|
{unreadCount > 0 ? `لديك ${unreadCount} إشعار غير مقروء` : 'جميع الإشعارات مقروءة'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{unreadCount > 0 && (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={markAllAsRead}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-xl text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<CheckCheck className="w-4 h-4 text-amber-500" />
|
||||||
|
تحديد الكل كمقروء
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{notifications.length === 0 ? (
|
{notifications.length === 0 ? (
|
||||||
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
|
||||||
|
>
|
||||||
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<Bell className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد إشعارات</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد إشعارات</h3>
|
||||||
<p className="text-gray-500">ستظهر هنا الإشعارات المتعلقة بحجوزاتك ومدفوعاتك</p>
|
<p className="text-gray-500">ستظهر هنا الإشعارات المتعلقة بحجوزاتك ومدفوعاتك</p>
|
||||||
</div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{notifications.map((notification, index) => (
|
<AnimatePresence>
|
||||||
<div
|
{notifications.map((notification, index) => (
|
||||||
key={index}
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md border-gray-200"
|
key={notification.id || index}
|
||||||
>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="p-5 flex gap-4">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="w-12 h-12 bg-blue-50 rounded-full flex items-center justify-center shrink-0">
|
exit={{ opacity: 0, x: 100 }}
|
||||||
<Bell className="w-6 h-6 text-blue-600" />
|
transition={{ delay: index * 0.05, type: 'spring', stiffness: 100 }}
|
||||||
</div>
|
whileHover={{ scale: 1.01 }}
|
||||||
<div className="flex-1">
|
onClick={() => markAsRead(notification.id)}
|
||||||
<div className="flex justify-between items-start">
|
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${
|
||||||
<div>
|
!notification.read ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'
|
||||||
<h3 className="font-bold text-gray-900">{notification.title}</h3>
|
}`}
|
||||||
{notification.message && (
|
>
|
||||||
<p className="text-gray-600 text-sm mt-1">{notification.message}</p>
|
<div className="p-5 flex gap-4">
|
||||||
)}
|
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${
|
||||||
{notification.date && (
|
!notification.read ? 'bg-amber-100' : 'bg-gray-100'
|
||||||
<p className="text-xs text-gray-400 mt-2">{notification.date}</p>
|
}`}>
|
||||||
|
{!notification.read ? (
|
||||||
|
<Bell className="w-6 h-6 text-amber-600" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle className="w-6 h-6 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex justify-between items-start gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className={`text-base ${!notification.read ? 'font-bold text-gray-900' : 'font-medium text-gray-700'}`}>
|
||||||
|
{notification.title}
|
||||||
|
</h3>
|
||||||
|
{notification.message && (
|
||||||
|
<p className="text-gray-500 text-sm mt-1 line-clamp-2">{notification.message}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
{notification.date && (
|
||||||
|
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />
|
||||||
|
{notification.date}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{notification.type && (
|
||||||
|
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||||
|
<MessageCircle className="w-3 h-3" />
|
||||||
|
{notification.type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!notification.read && (
|
||||||
|
<span className="w-2.5 h-2.5 bg-amber-500 rounded-full shrink-0 mt-2" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
256
app/onboarding/page.js
Normal file
256
app/onboarding/page.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Home, Search, Rocket, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'sweethome_onboarding_completed';
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
icon: Home,
|
||||||
|
title: 'مرحباً بك في SweetHome',
|
||||||
|
description: 'نحن هنا لمساعدتك في إيجاد العقار المثالي الذي تحلم به. اكتشف آلاف العقارات المتاحة للإيجار والبيع في جميع أنحاء سوريا.',
|
||||||
|
gradient: 'from-amber-500 to-orange-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Search,
|
||||||
|
title: 'ابحث عن منزل أحلامك',
|
||||||
|
description: 'استخدم محرك البحث الذكي لدينا للعثور على العقار المناسب. تصفح حسب الموقع، السعر، النوع، والمزيد من الفلاتر المتقدمة.',
|
||||||
|
gradient: 'from-blue-500 to-indigo-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Rocket,
|
||||||
|
title: 'انطلق الآن',
|
||||||
|
description: 'انشئ حسابك مجاناً وابدأ رحلة البحث عن منزلك الجديد. يمكنك حفظ العقارات المفضلة والتواصل مع المالكين مباشرة.',
|
||||||
|
gradient: 'from-emerald-500 to-teal-600',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function OnboardingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [direction, setDirection] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const completed = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (completed === 'true') {
|
||||||
|
router.replace('/');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const goToStep = (index) => {
|
||||||
|
setDirection(index > currentStep ? 1 : -1);
|
||||||
|
setCurrentStep(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep < steps.length - 1) {
|
||||||
|
goToStep(currentStep + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
if (currentStep > 0) {
|
||||||
|
goToStep(currentStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
const step = steps[currentStep];
|
||||||
|
const isLastStep = currentStep === steps.length - 1;
|
||||||
|
const isFirstStep = currentStep === 0;
|
||||||
|
const IconComponent = step.icon;
|
||||||
|
|
||||||
|
const slideVariants = {
|
||||||
|
enter: (dir) => ({ x: dir > 0 ? 300 : -300, opacity: 0 }),
|
||||||
|
center: { x: 0, opacity: 1 },
|
||||||
|
exit: (dir) => ({ x: dir > 0 ? -300 : 300, opacity: 0 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
{[...Array(30)].map((_, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute rounded-full bg-white/5"
|
||||||
|
style={{
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
width: Math.random() * 6 + 2,
|
||||||
|
height: Math.random() * 6 + 2,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, -30, 0],
|
||||||
|
opacity: [0.1, 0.4, 0.1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: Math.random() * 8 + 6,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: Math.random() * 4,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className={`absolute top-1/4 -left-20 w-72 h-72 bg-${step.gradient.split(' ')[0].replace('from-', '')}/10 rounded-full blur-3xl`}
|
||||||
|
animate={{ scale: [1, 1.3, 1], x: [0, 40, 0] }}
|
||||||
|
transition={{ duration: 10, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className={`absolute bottom-1/4 -right-20 w-80 h-80 bg-${step.gradient.split(' ')[1]}/10 rounded-full blur-3xl`}
|
||||||
|
animate={{ scale: [1, 1.2, 1], x: [0, -30, 0] }}
|
||||||
|
transition={{ duration: 12, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative w-full max-w-lg z-10">
|
||||||
|
<div className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
|
||||||
|
<AnimatePresence mode="wait" custom={direction}>
|
||||||
|
<motion.div
|
||||||
|
key={currentStep}
|
||||||
|
custom={direction}
|
||||||
|
variants={slideVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||||
|
className="p-10"
|
||||||
|
>
|
||||||
|
<div className={`bg-gradient-to-br ${step.gradient} rounded-2xl p-8 text-center mb-8 relative overflow-hidden`}>
|
||||||
|
<motion.div
|
||||||
|
className="absolute -top-12 -right-12 w-32 h-32 bg-white/10 rounded-full"
|
||||||
|
animate={{ scale: [1, 1.2, 1] }}
|
||||||
|
transition={{ duration: 4, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute -bottom-8 -left-8 w-24 h-24 bg-white/10 rounded-full"
|
||||||
|
animate={{ scale: [1, 1.3, 1] }}
|
||||||
|
transition={{ duration: 5, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0, rotate: -180 }}
|
||||||
|
animate={{ scale: 1, rotate: 0 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200, delay: 0.1 }}
|
||||||
|
className="w-24 h-24 mx-auto bg-white/20 rounded-3xl flex items-center justify-center backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<IconComponent className="w-12 h-12 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.15 }}
|
||||||
|
className="text-3xl font-bold text-white mb-4"
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.25 }}
|
||||||
|
className="text-gray-300 leading-relaxed text-base"
|
||||||
|
>
|
||||||
|
{step.description}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.35 }}
|
||||||
|
className="flex items-center justify-center gap-2 mb-8"
|
||||||
|
>
|
||||||
|
{steps.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => goToStep(index)}
|
||||||
|
className={`rounded-full transition-all duration-300 ${
|
||||||
|
index === currentStep
|
||||||
|
? 'w-8 h-3 bg-gradient-to-r ' + step.gradient
|
||||||
|
: 'w-3 h-3 bg-white/20 hover:bg-white/40'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="flex items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
{!isFirstStep ? (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={prevStep}
|
||||||
|
className="flex items-center gap-2 px-5 py-3 rounded-xl text-gray-300 hover:text-white hover:bg-white/10 transition-all border border-white/10"
|
||||||
|
>
|
||||||
|
<ArrowRight className="w-5 h-5" />
|
||||||
|
<span>السابق</span>
|
||||||
|
</motion.button>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLastStep ? (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={nextStep}
|
||||||
|
className={`flex items-center gap-2 px-6 py-3 rounded-xl text-white font-medium bg-gradient-to-r ${step.gradient} hover:shadow-lg transition-all`}
|
||||||
|
>
|
||||||
|
<span>التالي</span>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
</motion.button>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Link href="/auth/choose-role" onClick={handleFinish}>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="px-6 py-3 rounded-xl bg-white text-gray-900 font-bold hover:bg-gray-100 transition-all"
|
||||||
|
>
|
||||||
|
تسجيل جديد
|
||||||
|
</motion.button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/login" onClick={handleFinish}>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="px-6 py-3 rounded-xl bg-gradient-to-r from-emerald-500 to-teal-600 text-white font-bold hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
تسجيل دخول
|
||||||
|
</motion.button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
className="text-center text-gray-500 text-xs mt-4"
|
||||||
|
>
|
||||||
|
{currentStep + 1} / {steps.length}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
238
app/owner/account-book/page.js
Normal file
238
app/owner/account-book/page.js
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
DollarSign,
|
||||||
|
TrendingUp,
|
||||||
|
Calendar,
|
||||||
|
Users,
|
||||||
|
Building,
|
||||||
|
Download,
|
||||||
|
Loader2,
|
||||||
|
ArrowLeft,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import AuthService from '@/app/services/AuthService';
|
||||||
|
import { getOwnerStatistics } from '@/app/utils/api';
|
||||||
|
|
||||||
|
const StatCard = ({ title, value, icon: Icon, color, subtitle }) => (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className={`w-12 h-12 ${color} rounded-xl flex items-center justify-center`}>
|
||||||
|
<Icon className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm text-gray-500 mb-1">{title}</h3>
|
||||||
|
<div className="text-2xl font-bold text-gray-900">{value}</div>
|
||||||
|
{subtitle && <p className="text-xs text-gray-400 mt-1">{subtitle}</p>}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function OwnerAccountBookPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalRevenue: 0,
|
||||||
|
totalReservations: 0,
|
||||||
|
activeProperties: 0,
|
||||||
|
});
|
||||||
|
const [transactions, setTransactions] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (AuthService.isGuest()) {
|
||||||
|
router.push('/auth/choose-role');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!AuthService.isOwner()) {
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUser = AuthService.getUser();
|
||||||
|
if (authUser) {
|
||||||
|
setUser({
|
||||||
|
name: authUser.name || authUser.email,
|
||||||
|
email: authUser.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const data = await getOwnerStatistics();
|
||||||
|
if (data) {
|
||||||
|
setStats({
|
||||||
|
totalRevenue: data.totalRevenue ?? 0,
|
||||||
|
totalReservations: data.totalReservations ?? 0,
|
||||||
|
activeProperties: data.activeProperties ?? 0,
|
||||||
|
});
|
||||||
|
if (data.transactions) {
|
||||||
|
setTransactions(data.transactions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('تعذر تحميل إحصائيات الحساب');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const formatCurrency = (amount) => {
|
||||||
|
const num = Number(amount) || 0;
|
||||||
|
return num.toLocaleString() + ' ل.س';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
toast.success('جاري تصدير البيانات...');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-gray-600">جاري تحميل بيانات الحساب...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="container mx-auto px-4 max-w-7xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">دفتر الحسابات</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
مرحباً {user?.name}، نظرة عامة على حساباتك
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="px-5 py-2.5 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center gap-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
تصدير
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<StatCard
|
||||||
|
title="إجمالي الإيرادات"
|
||||||
|
value={formatCurrency(stats.totalRevenue)}
|
||||||
|
icon={DollarSign}
|
||||||
|
color="bg-green-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="إجمالي الحجوزات"
|
||||||
|
value={stats.totalReservations}
|
||||||
|
icon={Calendar}
|
||||||
|
color="bg-blue-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="العقارات النشطة"
|
||||||
|
value={stats.activeProperties}
|
||||||
|
icon={Building}
|
||||||
|
color="bg-amber-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">المعاملات المالية</h2>
|
||||||
|
</div>
|
||||||
|
{transactions.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||||
|
التاريخ
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||||
|
البيان
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||||
|
المبلغ
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||||
|
الحالة
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
|
{transactions.map((tx, idx) => (
|
||||||
|
<tr
|
||||||
|
key={tx.id || idx}
|
||||||
|
className={`hover:bg-amber-50/40 transition-colors ${
|
||||||
|
idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-center text-gray-700">
|
||||||
|
{tx.date}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-center text-gray-800 font-medium">
|
||||||
|
{tx.description}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-center font-mono font-semibold text-gray-800">
|
||||||
|
{formatCurrency(tx.amount)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-center">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium ${
|
||||||
|
tx.status === 'completed'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: tx.status === 'pending'
|
||||||
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tx.status === 'completed'
|
||||||
|
? 'مكتمل'
|
||||||
|
: tx.status === 'pending'
|
||||||
|
? 'قيد الانتظار'
|
||||||
|
: 'ملغي'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<DollarSign className="w-12 h-12 text-gray-300 mx-auto mb-4" />
|
||||||
|
<p className="text-gray-500">لا توجد معاملات مالية بعد</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
@ -48,10 +48,11 @@ import {
|
|||||||
Minus,
|
Minus,
|
||||||
Save,
|
Save,
|
||||||
Wind,
|
Wind,
|
||||||
Move
|
Move,
|
||||||
|
Trees
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import { addRentProperty, getCurrencies, uploadPicture } from '../../../utils/api';
|
import { addRentProperty, addSaleProperty, getCurrencies, uploadPicture } from '../../../utils/api';
|
||||||
import {
|
import {
|
||||||
BuildingType,
|
BuildingType,
|
||||||
RentPropertyCondition,
|
RentPropertyCondition,
|
||||||
@ -85,12 +86,14 @@ function MapClickHandler({ onMapClick }) {
|
|||||||
|
|
||||||
export default function AddPropertyPage() {
|
export default function AddPropertyPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const purpose = searchParams.get('purpose') || 'rent';
|
||||||
|
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const totalSteps = 4;
|
const totalSteps = purpose === 'sale' ? 4 : 4;
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
propertyType: 'apartment', // apartment, villa, suite, room
|
propertyType: 'apartment',
|
||||||
|
|
||||||
furnished: false,
|
furnished: false,
|
||||||
|
|
||||||
@ -152,8 +155,13 @@ export default function AddPropertyPage() {
|
|||||||
const propertyTypes = [
|
const propertyTypes = [
|
||||||
{ id: 'apartment', label: 'شقة', icon: Building },
|
{ id: 'apartment', label: 'شقة', icon: Building },
|
||||||
{ id: 'villa', label: 'فيلا', icon: Home },
|
{ id: 'villa', label: 'فيلا', icon: Home },
|
||||||
{ id: 'suite', label: 'سويت', icon: Sofa },
|
{ id: 'sweet', label: 'سويت', icon: Sofa },
|
||||||
{ id: 'room', label: 'غرفة ضمن شقة', icon: DoorOpen }
|
{ id: 'room', label: 'غرفة', icon: DoorOpen },
|
||||||
|
{ id: 'studio', label: 'استوديو', icon: Sofa },
|
||||||
|
{ id: 'office', label: 'مكتب', icon: Building },
|
||||||
|
{ id: 'farms', label: 'مزرعة', icon: Trees },
|
||||||
|
{ id: 'shop', label: 'متجر', icon: Warehouse },
|
||||||
|
{ id: 'warehouse', label: 'مستودع', icon: Warehouse },
|
||||||
];
|
];
|
||||||
|
|
||||||
const serviceList = [
|
const serviceList = [
|
||||||
@ -493,15 +501,19 @@ const handleMapClick = async (coords) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
if (formData.offerType === 'daily' && !formData.dailyPrice) {
|
if (purpose === 'sale') {
|
||||||
newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
if (!formData.salePrice) newErrors.salePrice = 'سعر البيع مطلوب';
|
||||||
}
|
} else {
|
||||||
if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
|
if (formData.offerType === 'daily' && !formData.dailyPrice) {
|
||||||
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
||||||
}
|
}
|
||||||
if (formData.offerType === 'both') {
|
if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
|
||||||
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
||||||
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
}
|
||||||
|
if (formData.offerType === 'both') {
|
||||||
|
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
||||||
|
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -535,25 +547,18 @@ const handleMapClick = async (coords) => {
|
|||||||
if (!validateStep()) return;
|
if (!validateStep()) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
console.log('[AddProperty] Building RentPropertyDto payload...');
|
|
||||||
|
|
||||||
// Map UI property type to API BuildingType enum
|
// Map UI property type to API BuildingType enum
|
||||||
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, suite: BuildingType.APARTMENT, room: BuildingType.APARTMENT };
|
const buildingTypeMap = { apartment: BuildingType.APARTMENT, villa: BuildingType.VILLA, sweet: BuildingType.SWEET, suite: BuildingType.SWEET, room: BuildingType.ROOM, studio: BuildingType.STUDIO, office: BuildingType.OFFICE, farms: BuildingType.FARMS, shop: BuildingType.SHOP, warehouse: BuildingType.WAREHOUSE };
|
||||||
|
|
||||||
// Map offer type to RentType enum: 0=Monthly, 1=Daily
|
|
||||||
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
|
||||||
|
|
||||||
// Services: collect selected service enum names into array
|
|
||||||
const selectedServices = Object.entries(formData.services)
|
const selectedServices = Object.entries(formData.services)
|
||||||
.filter(([, v]) => v)
|
.filter(([, v]) => v)
|
||||||
.map(([k]) => k); // k is already the enum value (e.g. "Electricity")
|
.map(([k]) => k);
|
||||||
|
|
||||||
// Terms: collect selected term enum names into array
|
|
||||||
const selectedTerms = Object.entries(formData.terms)
|
const selectedTerms = Object.entries(formData.terms)
|
||||||
.filter(([, v]) => v)
|
.filter(([, v]) => v)
|
||||||
.map(([k]) => k); // k is already the enum value (e.g. "NoSmoking")
|
.map(([k]) => k);
|
||||||
|
|
||||||
// Build detailsJSON matching Flutter structure
|
|
||||||
const detailsJSON = JSON.stringify({
|
const detailsJSON = JSON.stringify({
|
||||||
services: selectedServices,
|
services: selectedServices,
|
||||||
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: 'in general' }), {}),
|
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: 'in general' }), {}),
|
||||||
@ -578,40 +583,53 @@ const handleMapClick = async (coords) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
const propInfo = {
|
||||||
propertyInformation: {
|
cordsX: formData.lat ? String(formData.lat) : '',
|
||||||
cordsX: formData.lat ? String(formData.lat) : '',
|
cordsY: formData.lng ? String(formData.lng) : '',
|
||||||
cordsY: formData.lng ? String(formData.lng) : '',
|
address: `${formData.city} - ${formData.district} - ${formData.address}`.trim(),
|
||||||
address: `${formData.city} - ${formData.district} - ${formData.address}`.trim(),
|
description: formData.description || '',
|
||||||
description: formData.description || '',
|
numberOfBathRooms: formData.bathrooms || 0,
|
||||||
numberOfBathRooms: formData.bathrooms || 0,
|
numberOfRooms: (formData.bedrooms || 0) + (formData.livingRooms || 0),
|
||||||
numberOfRooms: (formData.bedrooms || 0) + (formData.livingRooms || 0),
|
numberOfBedRooms: formData.bedrooms || 0,
|
||||||
numberOfBedRooms: formData.bedrooms || 0,
|
space: parseFloat(formData.space) || 0,
|
||||||
space: parseFloat(formData.space) || 0,
|
detailsJSON,
|
||||||
detailsJSON,
|
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
|
||||||
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
|
status: 0,
|
||||||
status: 0,
|
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
|
||||||
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
|
images: uploadedImagePaths,
|
||||||
images: uploadedImagePaths,
|
|
||||||
},
|
|
||||||
deposit: parseFloat(formData.deposit) || 0,
|
|
||||||
monthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
|
||||||
dailyRent: parseFloat(formData.dailyPrice) || 0,
|
|
||||||
rating: 0,
|
|
||||||
currencyId: selectedCurrencyId,
|
|
||||||
rentType: rentTypeMap[formData.offerType] ?? RentType.MONTHLY,
|
|
||||||
isSmokeAllow: !formData.terms[PropertyTerm.NO_SMOKING],
|
|
||||||
specializedFor: false,
|
|
||||||
isVisitorAllow: !formData.terms[PropertyTerm.NO_PARTIES],
|
|
||||||
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[AddProperty] Payload:', JSON.stringify(payload, null, 2));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await addRentProperty(payload);
|
if (purpose === 'sale') {
|
||||||
console.log('[AddProperty] API response:', res);
|
const payload = {
|
||||||
toast.success('تم إضافة العقار بنجاح!');
|
propInfo,
|
||||||
|
price: parseFloat(formData.salePrice) || 0,
|
||||||
|
currencyId: selectedCurrencyId,
|
||||||
|
};
|
||||||
|
console.log('[AddProperty] Sale payload:', JSON.stringify(payload, null, 2));
|
||||||
|
const res = await addSaleProperty(payload);
|
||||||
|
console.log('[AddProperty] Sale API response:', res);
|
||||||
|
toast.success('تم إضافة عقار للبيع بنجاح!');
|
||||||
|
} else {
|
||||||
|
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
||||||
|
const payload = {
|
||||||
|
propertyInformation: propInfo,
|
||||||
|
deposit: parseFloat(formData.deposit) || 0,
|
||||||
|
monthlyRent: parseFloat(formData.monthlyPrice) || 0,
|
||||||
|
dailyRent: parseFloat(formData.dailyPrice) || 0,
|
||||||
|
rating: 0,
|
||||||
|
currencyId: selectedCurrencyId,
|
||||||
|
rentType: rentTypeMap[formData.offerType] ?? RentType.MONTHLY,
|
||||||
|
isSmokeAllow: !formData.terms[PropertyTerm.NO_SMOKING],
|
||||||
|
specializedFor: false,
|
||||||
|
isVisitorAllow: !formData.terms[PropertyTerm.NO_PARTIES],
|
||||||
|
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
||||||
|
};
|
||||||
|
console.log('[AddProperty] Rent payload:', JSON.stringify(payload, null, 2));
|
||||||
|
const res = await addRentProperty(payload);
|
||||||
|
console.log('[AddProperty] Rent API response:', res);
|
||||||
|
toast.success('تم إضافة عقار للإيجار بنجاح!');
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push('/owner/properties');
|
router.push('/owner/properties');
|
||||||
}, 1500);
|
}, 1500);
|
||||||
@ -663,7 +681,7 @@ const handleMapClick = async (coords) => {
|
|||||||
<div className="flex justify-between mt-2 text-xs text-gray-500">
|
<div className="flex justify-between mt-2 text-xs text-gray-500">
|
||||||
<span>معلومات العقار</span>
|
<span>معلومات العقار</span>
|
||||||
<span>التفاصيل والخدمات</span>
|
<span>التفاصيل والخدمات</span>
|
||||||
<span>السعر</span>
|
<span>{purpose === 'sale' ? 'سعر البيع' : 'السعر'}</span>
|
||||||
<span>الموقع والصور</span>
|
<span>الموقع والصور</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -927,7 +945,53 @@ const handleMapClick = async (coords) => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 3 && (
|
{step === 3 && purpose === 'sale' && (
|
||||||
|
<motion.div variants={fadeInUp} className="space-y-8">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<DollarSign className="w-10 h-10 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">سعر البيع</h2>
|
||||||
|
<p className="text-gray-600">حدد سعر البيع والعملة</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
سعر البيع (ل.س) <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.salePrice || ''}
|
||||||
|
onChange={(e) => setFormData({...formData, salePrice: e.target.value})}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
||||||
|
errors.salePrice ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
placeholder="مثال: 50000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.salePrice && <p className="text-red-500 text-sm mt-1">{errors.salePrice}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
العملة <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedCurrencyId}
|
||||||
|
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
>
|
||||||
|
{Object.entries(CurrencyLabels).map(([id, label]) => (
|
||||||
|
<option key={id} value={id}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && purpose === 'rent' && (
|
||||||
<motion.div variants={fadeInUp} className="space-y-8">
|
<motion.div variants={fadeInUp} className="space-y-8">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
|||||||
@ -46,7 +46,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '../../services/AuthService';
|
import AuthService from '../../services/AuthService';
|
||||||
import { getMyRentListings } from '../../utils/api';
|
import { getMyRentListings, getMySaleListings, editRentProperty } from '../../utils/api';
|
||||||
|
|
||||||
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
|
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
@ -721,24 +721,31 @@ export default function OwnerPropertiesPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[OwnerProperties] Fetching listings for user:', userId);
|
console.log('[OwnerProperties] Fetching listings for user:', userId);
|
||||||
const data = await getMyRentListings();
|
|
||||||
const list = Array.isArray(data) ? data : (data ? [data] : []);
|
|
||||||
console.log('[OwnerProperties] API returned:', list.length, 'properties');
|
|
||||||
|
|
||||||
const mapped = list.map((item) => {
|
const [rentData, saleData] = await Promise.allSettled([
|
||||||
|
getMyRentListings(),
|
||||||
|
getMySaleListings(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rentList = rentData.status === 'fulfilled' ? (Array.isArray(rentData.value) ? rentData.value : (rentData.value ? [rentData.value] : [])) : [];
|
||||||
|
const saleList = saleData.status === 'fulfilled' ? (Array.isArray(saleData.value) ? saleData.value : (saleData.value ? [saleData.value] : [])) : [];
|
||||||
|
|
||||||
|
console.log('[OwnerProperties] Rent:', rentList.length, 'Sale:', saleList.length);
|
||||||
|
|
||||||
|
const mappedRent = rentList.map((item) => {
|
||||||
const info = item.propertyInformation || {};
|
const info = item.propertyInformation || {};
|
||||||
const details = (() => {
|
const details = (() => { try { return JSON.parse(info.detailsJSON || '{}'); } catch { return {}; } })();
|
||||||
try { return JSON.parse(info.detailsJSON || '{}'); } catch { return {}; }
|
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
|
||||||
})();
|
const raw = Array.isArray(info.images) ? info.images : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: info.address || `عقار #${item.id}`,
|
title: info.address || `عقار #${item.id}`,
|
||||||
propertyType: { 0: 'apartment', 1: 'villa', 2: 'house' }[info.buildingType] || 'apartment',
|
propertyType: { 0: 'apartment', 1: 'villa', 2: 'house' }[info.buildingType] || 'apartment',
|
||||||
purpose: 'rent',
|
purpose: 'rent',
|
||||||
rentType: { 0: 'daily', 1: 'weekly', 2: 'monthly' }[item.rentType] || 'daily',
|
rentType: { 0: 'daily', 1: 'monthly' }[item.rentType] || 'daily',
|
||||||
dailyPrice: item.dailyRent || 0,
|
dailyPrice: item.dailyRent || 0,
|
||||||
monthlyPrice: item.monthlyRent || 0,
|
monthlyPrice: item.monthlyRent || 0,
|
||||||
|
salePrice: item.price || 0,
|
||||||
deposit: item.deposit || 0,
|
deposit: item.deposit || 0,
|
||||||
location: info.address || '',
|
location: info.address || '',
|
||||||
bedrooms: info.numberOfBedRooms || 0,
|
bedrooms: info.numberOfBedRooms || 0,
|
||||||
@ -746,11 +753,7 @@ export default function OwnerPropertiesPage() {
|
|||||||
area: info.space || 0,
|
area: info.space || 0,
|
||||||
livingRooms: details.livingRooms || 0,
|
livingRooms: details.livingRooms || 0,
|
||||||
status: { 0: 'available', 1: 'booked', 2: 'maintenance' }[info.status] || 'available',
|
status: { 0: 'available', 1: 'booked', 2: 'maintenance' }[info.status] || 'available',
|
||||||
images: (() => {
|
images: raw.length > 0 ? raw.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`) : ['/property-placeholder.jpg'],
|
||||||
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
|
|
||||||
const raw = Array.isArray(info.images) ? info.images : [];
|
|
||||||
return raw.length > 0 ? raw.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`) : ['/property-placeholder.jpg'];
|
|
||||||
})(),
|
|
||||||
createdAt: item.createdAt || new Date().toISOString(),
|
createdAt: item.createdAt || new Date().toISOString(),
|
||||||
furnished: details.furnished || false,
|
furnished: details.furnished || false,
|
||||||
description: info.description || '',
|
description: info.description || '',
|
||||||
@ -765,7 +768,42 @@ export default function OwnerPropertiesPage() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setProperties(mapped);
|
const mappedSale = saleList.map((item) => {
|
||||||
|
const info = item.propertyInformation || {};
|
||||||
|
const details = (() => { try { return JSON.parse(info.detailsJSON || '{}'); } catch { return {}; } })();
|
||||||
|
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
|
||||||
|
const raw = Array.isArray(info.images) ? info.images : [];
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: info.address || `عقار للبيع #${item.id}`,
|
||||||
|
propertyType: { 0: 'apartment', 1: 'villa', 2: 'house' }[info.buildingType] || 'apartment',
|
||||||
|
purpose: 'sale',
|
||||||
|
dailyPrice: 0,
|
||||||
|
monthlyPrice: 0,
|
||||||
|
salePrice: item.price || 0,
|
||||||
|
deposit: 0,
|
||||||
|
location: info.address || '',
|
||||||
|
bedrooms: info.numberOfBedRooms || 0,
|
||||||
|
bathrooms: info.numberOfBathRooms || 0,
|
||||||
|
area: info.space || 0,
|
||||||
|
livingRooms: details.livingRooms || 0,
|
||||||
|
status: 'available',
|
||||||
|
images: raw.length > 0 ? raw.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`) : ['/property-placeholder.jpg'],
|
||||||
|
createdAt: item.createdAt || new Date().toISOString(),
|
||||||
|
furnished: details.furnished || false,
|
||||||
|
description: info.description || '',
|
||||||
|
address: info.address || '',
|
||||||
|
city: '',
|
||||||
|
district: '',
|
||||||
|
services: details.services || {},
|
||||||
|
terms: details.terms || {},
|
||||||
|
rating: item.rating || 0,
|
||||||
|
currencyId: item.currencyId,
|
||||||
|
_raw: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setProperties([...mappedRent, ...mappedSale]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OwnerProperties] Failed to load properties:', err);
|
console.error('[OwnerProperties] Failed to load properties:', err);
|
||||||
toast.error('فشل في تحميل العقارات');
|
toast.error('فشل في تحميل العقارات');
|
||||||
@ -788,12 +826,58 @@ export default function OwnerPropertiesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveEdit = (updatedProperty) => {
|
const handleSaveEdit = async (updatedProperty) => {
|
||||||
const newProperties = properties.map(p =>
|
try {
|
||||||
p.id === updatedProperty.id ? updatedProperty : p
|
if (updatedProperty.purpose === 'rent' && updatedProperty._raw) {
|
||||||
);
|
const buildingTypeMap = { apartment: 0, villa: 1, sweet: 2, room: 3, studio: 4, office: 5, farms: 6, shop: 7, warehouse: 8 };
|
||||||
updatePropertiesInStorage(newProperties);
|
const raw = updatedProperty._raw;
|
||||||
setEditModal({ isOpen: false, property: null });
|
const rentTypeMap = { daily: 1, monthly: 0, both: 0 };
|
||||||
|
const detailsJSON = JSON.stringify({
|
||||||
|
services: updatedProperty.services || {},
|
||||||
|
terms: updatedProperty.terms || {},
|
||||||
|
furnished: updatedProperty.furnished,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
propertyInformation: {
|
||||||
|
cordsX: raw.propertyInformation?.cordsX || '',
|
||||||
|
cordsY: raw.propertyInformation?.cordsY || '',
|
||||||
|
address: updatedProperty.address || raw.propertyInformation?.address || '',
|
||||||
|
description: updatedProperty.description || raw.propertyInformation?.description || '',
|
||||||
|
numberOfBathRooms: updatedProperty.bathrooms || raw.propertyInformation?.numberOfBathRooms || 0,
|
||||||
|
numberOfRooms: (updatedProperty.bedrooms || 0) + (updatedProperty.livingRooms || 0),
|
||||||
|
numberOfBedRooms: updatedProperty.bedrooms || raw.propertyInformation?.numberOfBedRooms || 0,
|
||||||
|
space: parseFloat(updatedProperty.area) || raw.propertyInformation?.space || 0,
|
||||||
|
detailsJSON,
|
||||||
|
buildingType: buildingTypeMap[updatedProperty.propertyType] ?? 0,
|
||||||
|
status: updatedProperty.status === 'available' ? 0 : 1,
|
||||||
|
propertyType: updatedProperty.furnished ? 0 : 1,
|
||||||
|
images: raw.propertyInformation?.images || [],
|
||||||
|
},
|
||||||
|
deposit: parseFloat(updatedProperty.deposit) || raw.deposit || 0,
|
||||||
|
monthlyRent: parseFloat(updatedProperty.monthlyPrice) || raw.monthlyRent || 0,
|
||||||
|
dailyRent: parseFloat(updatedProperty.dailyPrice) || raw.dailyRent || 0,
|
||||||
|
rating: updatedProperty.rating || 0,
|
||||||
|
currencyId: updatedProperty.currencyId || raw.currencyId || 1,
|
||||||
|
rentType: rentTypeMap[updatedProperty.rentType] ?? 0,
|
||||||
|
isSmokeAllow: !updatedProperty.terms?.NoSmoking,
|
||||||
|
isVisitorAllow: !updatedProperty.terms?.NoParties,
|
||||||
|
type: updatedProperty.furnished ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
await editRentProperty(updatedProperty.id, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = properties.map(p =>
|
||||||
|
p.id === updatedProperty.id ? updatedProperty : p
|
||||||
|
);
|
||||||
|
updatePropertiesInStorage(newProperties);
|
||||||
|
setEditModal({ isOpen: false, property: null });
|
||||||
|
toast.success('تم تحديث العقار بنجاح');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[OwnerProperties] Edit failed:', err);
|
||||||
|
toast.error('فشل تحديث العقار');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
|
|||||||
@ -1,93 +1,138 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { CreditCard, Download, Eye } from 'lucide-react';
|
import { motion } from 'framer-motion';
|
||||||
|
import { CreditCard, Loader2, Home, Calendar, Check, X, Clock } from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
import Link from 'next/link';
|
import { getUserReservations, payDeposit } from '@/app/utils/api';
|
||||||
|
|
||||||
const mockPayments = [
|
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
{
|
|
||||||
id: 1,
|
const STATUS_CONFIG = {
|
||||||
property: 'فيلا فاخرة في المزة',
|
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300', depositPaid: false },
|
||||||
amount: 2500000,
|
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300', depositPaid: false },
|
||||||
date: '2024-03-10',
|
depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300', depositPaid: true },
|
||||||
status: 'completed',
|
depositConfirmed: { label: 'تم تأكيد الدفع', color: 'bg-green-100 text-green-800 border-green-300', depositPaid: true },
|
||||||
invoiceId: 'INV-001'
|
completed: { label: 'منتهي', color: 'bg-teal-100 text-teal-800 border-teal-300', depositPaid: true },
|
||||||
},
|
cancelled: { label: 'ملغي', color: 'bg-red-100 text-red-800 border-red-300', depositPaid: false },
|
||||||
{
|
};
|
||||||
id: 2,
|
|
||||||
property: 'شقة حديثة في الشهباء',
|
|
||||||
amount: 750000,
|
|
||||||
date: '2024-03-05',
|
|
||||||
status: 'completed',
|
|
||||||
invoiceId: 'INV-002'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PaymentsPage() {
|
export default function PaymentsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [payments, setPayments] = useState([]);
|
const [reservations, setReservations] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [payingId, setPayingId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (AuthService.isAdmin()) {
|
if (AuthService.isAdmin()) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
loadReservations();
|
||||||
setPayments(mockPayments);
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 500);
|
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const formatCurrency = (amount) => amount?.toLocaleString() + ' ل.س';
|
const loadReservations = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const data = await getUserReservations();
|
||||||
|
setReservations(Array.isArray(data) ? data : []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('فشل تحميل المدفوعات');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
const handlePayDeposit = async (reservation) => {
|
||||||
|
setPayingId(reservation.id);
|
||||||
|
try {
|
||||||
|
await payDeposit({ reservationId: reservation.id });
|
||||||
|
toast.success('تم دفع السلفة بنجاح!');
|
||||||
|
loadReservations();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err?.message || 'فشل عملية الدفع');
|
||||||
|
} finally {
|
||||||
|
setPayingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (v) => (v ?? 0).toLocaleString() + ' ل.س';
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
||||||
<div className="text-center">
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
||||||
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">جاري التحميل...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">المدفوعات</h1>
|
|
||||||
<p className="text-gray-600">سجل المعاملات المالية والفواتير</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{payments.length === 0 ? (
|
return (
|
||||||
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">المدفوعات</h1>
|
||||||
|
<p className="text-gray-600">إدارة مدفوعات الحجوزات والدفعات المقدمة</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{reservations.length === 0 ? (
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||||
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
|
||||||
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
||||||
</div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{payments.map((payment) => (
|
{reservations.map((r, i) => {
|
||||||
<div key={payment.id} className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
const statusKey = STATUS_MAP[r.status] || 'pending';
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
const cfg = STATUS_CONFIG[statusKey];
|
||||||
<div>
|
const amount = r.depositAmount || r.totalPrice || 0;
|
||||||
<h3 className="font-bold text-gray-900">{payment.property}</h3>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">رقم الفاتورة: {payment.invoiceId}</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-2">{payment.date}</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-xl font-bold text-amber-600">{formatCurrency(payment.amount)}</div>
|
|
||||||
<span className="inline-block px-2 py-1 bg-green-100 text-green-800 rounded-lg text-xs mt-1">
|
|
||||||
مكتمل
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
return (
|
||||||
))}
|
<motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<Home className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900">
|
||||||
|
{r.propertyAddress || r._prop?.address || `عقار #${r.propertyId || r.id}`}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">حجز #{r.id}</p>
|
||||||
|
<div className="flex items-center gap-3 mt-1 text-xs text-gray-400">
|
||||||
|
<span className="flex items-center gap-1"><Calendar className="w-3 h-3" /> {new Date(r.startDate).toLocaleDateString('ar')}</span>
|
||||||
|
<span className="flex items-center gap-1"><Clock className="w-3 h-3" /> {new Date(r.endDate).toLocaleDateString('ar')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right w-full md:w-auto">
|
||||||
|
<div className="text-xl font-bold text-amber-600">{formatCurrency(amount)}</div>
|
||||||
|
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium border ${cfg.color}`}>
|
||||||
|
{cfg.depositPaid ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
||||||
|
{cfg.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canPay(r.status) && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100 flex justify-end">
|
||||||
|
<button onClick={() => handlePayDeposit(r)} disabled={payingId === r.id}
|
||||||
|
className="flex items-center gap-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white px-6 py-2.5 rounded-xl text-sm font-medium transition">
|
||||||
|
{payingId === r.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
|
||||||
|
{payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
105
app/privacy/page.js
Normal file
105
app/privacy/page.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Shield, Lock, Eye, Database, RefreshCw, Trash2, CheckCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'المقدمة',
|
||||||
|
icon: Shield,
|
||||||
|
content:
|
||||||
|
'نحن في SweetHome نلتزم بحماية خصوصية مستخدمينا. توضح سياسة الخصوصية هذه كيفية جمع واستخدام وحماية المعلومات الشخصية التي تقدمها عند استخدام منصتنا. باستخدامك للمنصة، فإنك توافق على الممارسات الموضحة في هذه السياسة.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'المعلومات التي نجمعها',
|
||||||
|
icon: Database,
|
||||||
|
content:
|
||||||
|
'نجمع المعلومات التي تقدمها مباشرة عند إنشاء حساب، مثل الاسم، البريد الإلكتروني، رقم الهاتف، ومعلومات الدفع. كما نجمع معلومات حول استخدامك للمنصة، مثل العقارات التي تتصفحها، الحجوزات التي تقوم بها، وتقييماتك. قد نجمع أيضاً معلومات تقنية مثل عنوان IP ونوع المتصفح.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'كيف نستخدم معلوماتك',
|
||||||
|
icon: Eye,
|
||||||
|
content:
|
||||||
|
'نستخدم معلوماتك لتقديم وتحسين خدماتنا، ومعالجة الحجوزات والمدفوعات، والتواصل معك بشأن حساباتك وحجوزاتك، وإرسال التحديثات والعروض الترويجية (بموافقتك)، وتحسين تجربة المستخدم وتطوير ميزات جديدة، والامتثال للالتزامات القانونية.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'حماية البيانات وأمانها',
|
||||||
|
icon: Lock,
|
||||||
|
content:
|
||||||
|
'نحن نتخذ إجراءات أمنية مناسبة لحماية بياناتك الشخصية من الوصول غير المصرح به أو التعديل أو الإفصاح أو الإتلاف. تشمل هذه الإجراءات التشفير، وجدران الحماية، وضوابط الوصول الصارمة. ومع ذلك، لا يمكن ضمان أمان مطلق لنقل البيانات عبر الإنترنت.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'مشاركة البيانات مع أطراف ثالثة',
|
||||||
|
icon: RefreshCw,
|
||||||
|
content:
|
||||||
|
'لا نبيع أو نشارك معلوماتك الشخصية مع أطراف ثالثة لأغراض تسويقية دون موافقتك الصريحة. قد نشارك معلوماتك مع مزودي الخدمة الذين يساعدوننا في تشغيل المنصة (مثل معالجة الدفعات)، مع الالتزام باتفاقيات سرية صارمة. قد نكشف عن معلوماتك إذا كان ذلك مطلوباً بموجب القانون.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'حقوقك وخياراتك',
|
||||||
|
icon: Trash2,
|
||||||
|
content:
|
||||||
|
'لديك الحق في الوصول إلى بياناتك الشخصية وتحديثها أو تصحيحها أو حذفها في أي وقت. يمكنك إدارة تفضيلات الاتصال من إعدادات حسابك. يمكنك طلب حذف حسابك وجميع بياناتك المرتبطة به من خلال التواصل مع فريق الدعم. سنستجيب لطلباتك في أقرب وقت ممكن وفقاً للقوانين المعمول بها.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
|
<Shield className="w-10 h-10 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">سياسة الخصوصية</h1>
|
||||||
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
|
نلتزم بحماية خصوصيتك وأمان بياناتك الشخصية
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{sections.map((section, index) => {
|
||||||
|
const Icon = section.icon;
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
|
<Icon className="w-6 h-6 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-3">{section.title}</h2>
|
||||||
|
<p className="text-gray-600 leading-relaxed">{section.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
|
||||||
|
<p className="text-amber-700">
|
||||||
|
تم آخر تحديث لسياسة الخصوصية في 1 مايو 2026. للمزيد من المعلومات أو الاستفسارات، يرجى التواصل مع فريق الدعم.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -43,10 +43,10 @@ function mapApiProperty(item, index) {
|
|||||||
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
|
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
|
||||||
const monthlyPrice = item.monthlyRent ?? 0;
|
const monthlyPrice = item.monthlyRent ?? 0;
|
||||||
|
|
||||||
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' };
|
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'sweet', 3: 'room', 4: 'studio', 5: 'office', 6: 'farms', 7: 'shop', 8: 'warehouse' };
|
||||||
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment';
|
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment';
|
||||||
|
|
||||||
const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' };
|
const statusMap = { 0: 'available', 1: 'notAvailable', 2: 'booked' };
|
||||||
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
|
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
|
||||||
|
|
||||||
const features = [];
|
const features = [];
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
648
app/register/agent/page.js
Normal file
648
app/register/agent/page.js
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
// app/register/agent/page.js
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef, useMemo } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import {
|
||||||
|
User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
|
||||||
|
Camera, X, CheckCircle, XCircle, ArrowLeft, Building,
|
||||||
|
Loader2, Shield, KeyRound, MapPin, FileText, BadgeCheck, Briefcase
|
||||||
|
} from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import { registerRealEstateAgent, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
|
||||||
|
import AuthService from '../../services/AuthService';
|
||||||
|
|
||||||
|
export default function AgentRegisterPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [showOtpModal, setShowOtpModal] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
whatsapp: '',
|
||||||
|
nationalNumber: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
agencyAddress: '',
|
||||||
|
licenseNumber: '',
|
||||||
|
agreeTerms: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [idImages, setIdImages] = useState({ front: null, back: null, license: null });
|
||||||
|
const [idImagePreviews, setIdImagePreviews] = useState({ front: '', back: '', license: '' });
|
||||||
|
const [otpCode, setOtpCode] = useState('');
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
const fileInputFrontRef = useRef(null);
|
||||||
|
const fileInputBackRef = useRef(null);
|
||||||
|
const fileInputLicenseRef = useRef(null);
|
||||||
|
const fileInputLicenseStep3Ref = useRef(null);
|
||||||
|
|
||||||
|
const handleImageUpload = (side, file) => {
|
||||||
|
if (!file) return;
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.error('الرجاء اختيار صورة صالحة');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setIdImagePreviews(prev => ({ ...prev, [side]: reader.result }));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
setIdImages(prev => ({ ...prev, [side]: file }));
|
||||||
|
toast.success('تم رفع الصورة بنجاح', { style: { background: '#dcfce7', color: '#166534' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone);
|
||||||
|
|
||||||
|
const validateStep1 = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
if (!formData.firstName) newErrors.firstName = 'الاسم الأول مطلوب';
|
||||||
|
if (!formData.lastName) newErrors.lastName = 'اسم العائلة مطلوب';
|
||||||
|
if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب';
|
||||||
|
else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح';
|
||||||
|
if (!formData.phone) newErrors.phone = 'رقم الهاتف مطلوب';
|
||||||
|
else if (!validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
|
||||||
|
if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة';
|
||||||
|
else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
|
||||||
|
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateStep2 = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
if (!formData.nationalNumber) newErrors.nationalNumber = 'الرقم الوطني مطلوب';
|
||||||
|
if (!formData.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب';
|
||||||
|
else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
|
||||||
|
if (!idImages.front) newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
|
||||||
|
if (!idImages.back) newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateStep3 = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
if (!formData.agencyAddress) newErrors.agencyAddress = 'عنوان الوكالة مطلوب';
|
||||||
|
if (!formData.licenseNumber) newErrors.licenseNumber = 'رقم الترخيص مطلوب';
|
||||||
|
if (!idImages.license) newErrors.license = 'صورة الترخيص مطلوبة';
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
if (step === 1 && validateStep1()) {
|
||||||
|
setStep(2);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
} else if (step === 2 && validateStep2()) {
|
||||||
|
setStep(3);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
} else if (step === 3 && validateStep3()) {
|
||||||
|
setStep(4);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
toast.error('يرجى تصحيح الأخطاء في النموذج');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!formData.agreeTerms) {
|
||||||
|
toast.error('يجب الموافقة على الشروط والأحكام');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('FirstName', formData.firstName);
|
||||||
|
fd.append('LastName', formData.lastName);
|
||||||
|
fd.append('Email', formData.email);
|
||||||
|
fd.append('PhoneNumber', formData.phone);
|
||||||
|
fd.append('WhatsAppNumber', formData.whatsapp);
|
||||||
|
fd.append('NationalNumber', formData.nationalNumber);
|
||||||
|
fd.append('Password', formData.password);
|
||||||
|
fd.append('AgencyAddress', formData.agencyAddress);
|
||||||
|
fd.append('LicenseNumber', formData.licenseNumber);
|
||||||
|
fd.append('Language', '0');
|
||||||
|
|
||||||
|
if (idImages.front) fd.append('FrontIdCarImagePath', idImages.front);
|
||||||
|
if (idImages.back) fd.append('RearIdCarImagePath', idImages.back);
|
||||||
|
if (idImages.license) fd.append('LicenseImagePath', idImages.license);
|
||||||
|
|
||||||
|
const res = await registerRealEstateAgent(fd);
|
||||||
|
if (res.ok || res.status === 200) {
|
||||||
|
const tempToken = res.data;
|
||||||
|
if (tempToken) AuthService.addToken(tempToken);
|
||||||
|
toast.success(res.message || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
|
||||||
|
|
||||||
|
const loginRes = await loginWithEmail(formData.email, formData.password);
|
||||||
|
if (loginRes.status === 206) {
|
||||||
|
const otpToken = loginRes.data;
|
||||||
|
if (otpToken) AuthService.addToken(otpToken);
|
||||||
|
toast(loginRes.message || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
|
||||||
|
setShowOtpModal(true);
|
||||||
|
} else if (loginRes.status === 200) {
|
||||||
|
const loginToken = loginRes.data;
|
||||||
|
if (loginToken) AuthService.addToken(loginToken);
|
||||||
|
toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!');
|
||||||
|
router.push('/');
|
||||||
|
} else {
|
||||||
|
toast.success('تم إنشاء الحساب بنجاح! يرجى تسجيل الدخول');
|
||||||
|
setTimeout(() => router.push('/login'), 1500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || res.data?.message || 'فشل في إنشاء الحساب');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'حدث خطأ أثناء التسجيل');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOTP = async () => {
|
||||||
|
if (!otpCode || otpCode.length < 4) {
|
||||||
|
toast.error('يرجى إدخال رمز التحقق');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await verifyEmail(otpCode);
|
||||||
|
if (res.status === 200) {
|
||||||
|
AuthService.deleteToken();
|
||||||
|
toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!');
|
||||||
|
setShowOtpModal(false);
|
||||||
|
setTimeout(() => router.push('/login'), 1500);
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || res.data?.message || 'رمز التحقق غير صحيح');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'حدث خطأ أثناء التحقق');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResendOTP = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await sendEmailOTP();
|
||||||
|
toast.success('تم إرسال رمز تحقق جديد');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('فشل في إرسال الرمز');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fadeInUp = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
transition: { duration: 0.5 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const staggerContainer = {
|
||||||
|
animate: { transition: { staggerChildren: 0.1 } }
|
||||||
|
};
|
||||||
|
|
||||||
|
const backgroundElements = useMemo(() => {
|
||||||
|
const circles = [
|
||||||
|
{ style: { top: '20%', right: '20%', width: '256px', height: '256px' }, className: 'bg-purple-500/5' },
|
||||||
|
{ style: { bottom: '20%', left: '20%', width: '320px', height: '320px' }, className: 'bg-amber-500/5' },
|
||||||
|
{ style: { top: '50%', left: '50%', width: '384px', height: '384px', transform: 'translate(-50%, -50%)' }, className: 'bg-purple-500/5' },
|
||||||
|
];
|
||||||
|
const dots = Array.from({ length: 10 }).map((_, i) => ({
|
||||||
|
left: `${5 + i * 10}%`,
|
||||||
|
top: `${10 + (i * 7) % 80}%`,
|
||||||
|
size: `${80 + (i % 5) * 15}px`
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{circles.map((circle, i) => (
|
||||||
|
<div key={`circle-${i}`} className={`absolute rounded-full ${circle.className}`} style={circle.style} />
|
||||||
|
))}
|
||||||
|
{dots.map((dot, i) => (
|
||||||
|
<div key={`dot-${i}`} className="absolute rounded-full bg-purple-500/10" style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stepTitles = ['المعلومات الأساسية', 'معلومات الهوية', 'معلومات الوكالة', 'تأكيد التسجيل'];
|
||||||
|
const stepDescriptions = ['أدخل معلوماتك الأساسية', 'يرجى رفع صور الهوية للتحقق', 'أدخل بيانات الوكالة والترخيص', 'راجع معلوماتك قبل الإرسال'];
|
||||||
|
|
||||||
|
const renderStepFields = () => {
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">الاسم الأول <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<User className={`w-5 h-5 ${errors.firstName ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="text" value={formData.firstName}
|
||||||
|
onChange={(e) => { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="الاسم الأول" />
|
||||||
|
</div>
|
||||||
|
{errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">اسم العائلة <span className="text-red-500">*</span></label>
|
||||||
|
<input type="text" value={formData.lastName}
|
||||||
|
onChange={(e) => { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }}
|
||||||
|
className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="اسم العائلة" />
|
||||||
|
{errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">البريد الإلكتروني <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="email" value={formData.email}
|
||||||
|
onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل بريدك الإلكتروني" />
|
||||||
|
</div>
|
||||||
|
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Phone className={`w-5 h-5 ${errors.phone ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="tel" value={formData.phone}
|
||||||
|
onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل رقم هاتفك" />
|
||||||
|
</div>
|
||||||
|
{errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">كلمة المرور <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type={showPassword ? "text" : "password"} value={formData.password}
|
||||||
|
onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }}
|
||||||
|
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل كلمة المرور" />
|
||||||
|
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||||
|
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">تأكيد كلمة المرور <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword}
|
||||||
|
onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }}
|
||||||
|
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أعد إدخال كلمة المرور" />
|
||||||
|
<button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||||
|
{showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
|
||||||
|
</button>
|
||||||
|
{formData.confirmPassword && (
|
||||||
|
<div className="absolute inset-y-0 left-12 flex items-center">
|
||||||
|
{formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">الرقم الوطني <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<User className={`w-5 h-5 ${errors.nationalNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="text" value={formData.nationalNumber}
|
||||||
|
onChange={(e) => { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل الرقم الوطني" />
|
||||||
|
</div>
|
||||||
|
{errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">رقم الواتساب <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<MessageCircle className={`w-5 h-5 ${errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="tel" value={formData.whatsapp}
|
||||||
|
onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل رقم الواتساب" />
|
||||||
|
</div>
|
||||||
|
{errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">صورة الهوية - الوجه الأمامي <span className="text-red-500">*</span></label>
|
||||||
|
<div onClick={() => fileInputFrontRef.current?.click()}
|
||||||
|
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.front ? 'border-green-500 bg-green-500/10' : errors.front ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-purple-500 hover:bg-white/5'}`}>
|
||||||
|
<input ref={fileInputFrontRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('front', e.target.files?.[0])} className="hidden" />
|
||||||
|
{idImagePreviews.front ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Image src={idImagePreviews.front} alt="Front ID" width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, front: null})); setIdImagePreviews(prev => ({...prev, front: ''})); }}
|
||||||
|
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
|
||||||
|
<X className="w-4 h-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">اضغط لرفع الصورة</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • حتى 5MB</p></>)}
|
||||||
|
</div>
|
||||||
|
{errors.front && <p className="text-red-500 text-sm mt-1">{errors.front}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">صورة الهوية - الوجه الخلفي <span className="text-red-500">*</span></label>
|
||||||
|
<div onClick={() => fileInputBackRef.current?.click()}
|
||||||
|
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.back ? 'border-green-500 bg-green-500/10' : errors.back ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-purple-500 hover:bg-white/5'}`}>
|
||||||
|
<input ref={fileInputBackRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('back', e.target.files?.[0])} className="hidden" />
|
||||||
|
{idImagePreviews.back ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Image src={idImagePreviews.back} alt="Back ID" width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, back: null})); setIdImagePreviews(prev => ({...prev, back: ''})); }}
|
||||||
|
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
|
||||||
|
<X className="w-4 h-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">اضغط لرفع الصورة</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • حتى 5MB</p></>)}
|
||||||
|
</div>
|
||||||
|
{errors.back && <p className="text-red-500 text-sm mt-1">{errors.back}</p>}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">عنوان الوكالة <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<MapPin className={`w-5 h-5 ${errors.agencyAddress ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="text" value={formData.agencyAddress}
|
||||||
|
onChange={(e) => { setFormData({...formData, agencyAddress: e.target.value}); setErrors({...errors, agencyAddress: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.agencyAddress ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل عنوان الوكالة" />
|
||||||
|
</div>
|
||||||
|
{errors.agencyAddress && <p className="text-red-500 text-sm mt-1">{errors.agencyAddress}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">رقم الترخيص <span className="text-red-500">*</span></label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<BadgeCheck className={`w-5 h-5 ${errors.licenseNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-purple-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input type="text" value={formData.licenseNumber}
|
||||||
|
onChange={(e) => { setFormData({...formData, licenseNumber: e.target.value}); setErrors({...errors, licenseNumber: null}); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.licenseNumber ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
placeholder="أدخل رقم الترخيص العقاري" />
|
||||||
|
</div>
|
||||||
|
{errors.licenseNumber && <p className="text-red-500 text-sm mt-1">{errors.licenseNumber}</p>}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp}>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">صورة الترخيص <span className="text-red-500">*</span></label>
|
||||||
|
<div onClick={() => fileInputLicenseStep3Ref.current?.click()}
|
||||||
|
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.license ? 'border-green-500 bg-green-500/10' : errors.license ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-purple-500 hover:bg-white/5'}`}>
|
||||||
|
<input ref={fileInputLicenseStep3Ref} type="file" accept="image/*" onChange={(e) => handleImageUpload('license', e.target.files?.[0])} className="hidden" />
|
||||||
|
{idImagePreviews.license ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Image src={idImagePreviews.license} alt="License" width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, license: null})); setIdImagePreviews(prev => ({...prev, license: ''})); }}
|
||||||
|
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
|
||||||
|
<X className="w-4 h-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">اضغط لرفع صورة الترخيص</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • حتى 5MB</p></>)}
|
||||||
|
</div>
|
||||||
|
{errors.license && <p className="text-red-500 text-sm mt-1">{errors.license}</p>}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return (
|
||||||
|
<motion.div variants={fadeInUp} className="space-y-4">
|
||||||
|
<div className="bg-white/5 rounded-xl p-4 border border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5 text-purple-400" />
|
||||||
|
المعلومات الأساسية
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div><span className="text-gray-400">الاسم الأول:</span> <span className="text-white">{formData.firstName}</span></div>
|
||||||
|
<div><span className="text-gray-400">اسم العائلة:</span> <span className="text-white">{formData.lastName}</span></div>
|
||||||
|
<div><span className="text-gray-400">البريد الإلكتروني:</span> <span className="text-white">{formData.email}</span></div>
|
||||||
|
<div><span className="text-gray-400">رقم الهاتف:</span> <span className="text-white">{formData.phone}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/5 rounded-xl p-4 border border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-purple-400" />
|
||||||
|
معلومات الهوية
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div><span className="text-gray-400">الرقم الوطني:</span> <span className="text-white">{formData.nationalNumber}</span></div>
|
||||||
|
<div><span className="text-gray-400">رقم الواتساب:</span> <span className="text-white">{formData.whatsapp}</span></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 mt-3">
|
||||||
|
{idImagePreviews.front && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Image src={idImagePreviews.front} alt="Front ID" width={100} height={60} className="rounded-lg object-cover mx-auto" />
|
||||||
|
<p className="text-xs text-gray-400 mt-1">الوجه الأمامي</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{idImagePreviews.back && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Image src={idImagePreviews.back} alt="Back ID" width={100} height={60} className="rounded-lg object-cover mx-auto" />
|
||||||
|
<p className="text-xs text-gray-400 mt-1">الوجه الخلفي</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/5 rounded-xl p-4 border border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
||||||
|
<Briefcase className="w-5 h-5 text-purple-400" />
|
||||||
|
معلومات الوكالة
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div><span className="text-gray-400">عنوان الوكالة:</span> <span className="text-white">{formData.agencyAddress}</span></div>
|
||||||
|
<div><span className="text-gray-400">رقم الترخيص:</span> <span className="text-white">{formData.licenseNumber}</span></div>
|
||||||
|
</div>
|
||||||
|
{idImagePreviews.license && (
|
||||||
|
<div className="text-center mt-3">
|
||||||
|
<Image src={idImagePreviews.license} alt="License" width={100} height={60} className="rounded-lg object-cover mx-auto" />
|
||||||
|
<p className="text-xs text-gray-400 mt-1">صورة الترخيص</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp} className="flex items-center gap-2 pt-2">
|
||||||
|
<input type="checkbox" id="terms" checked={formData.agreeTerms}
|
||||||
|
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
|
||||||
|
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-purple-500 focus:ring-purple-500" required />
|
||||||
|
<label htmlFor="terms" className="text-sm text-gray-300">
|
||||||
|
أوافق على <Link href="/terms" className="text-purple-400 hover:text-purple-300">شروط الاستخدام</Link> و <Link href="/privacy" className="text-purple-400 hover:text-purple-300">سياسة الخصوصية</Link>
|
||||||
|
</label>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="absolute inset-0 overflow-hidden">{backgroundElements}</div>
|
||||||
|
|
||||||
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }}
|
||||||
|
className="relative z-10 w-full max-w-2xl">
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
|
||||||
|
<motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
|
||||||
|
<span>العودة</span>
|
||||||
|
</Link>
|
||||||
|
<span className="text-sm text-gray-400">خطوة {step} من 4</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[1, 2, 3, 4].map((s) => (
|
||||||
|
<motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? 'bg-purple-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div key={step} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.3 }}
|
||||||
|
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
|
||||||
|
<div className="bg-gradient-to-r from-purple-500 to-purple-600 p-8 text-center relative overflow-hidden">
|
||||||
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }}
|
||||||
|
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||||
|
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
|
||||||
|
<motion.div animate={{ rotate: [0, 10, -10, 0] }} transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
|
||||||
|
<Briefcase className="w-10 h-10 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">{stepTitles[step - 1]}</h1>
|
||||||
|
<p className="text-purple-100">{stepDescriptions[step - 1]}</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8">
|
||||||
|
<motion.form variants={staggerContainer} initial="initial" animate="animate"
|
||||||
|
onSubmit={step < 4 ? (e) => { e.preventDefault(); handleNextStep(); } : handleSubmit}
|
||||||
|
className="space-y-6">
|
||||||
|
{renderStepFields()}
|
||||||
|
|
||||||
|
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
|
||||||
|
{step > 1 && (
|
||||||
|
<button type="button" onClick={() => { setStep(step - 1); window.scrollTo({ top: 0, behavior: 'smooth' }); }}
|
||||||
|
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">السابق</button>
|
||||||
|
)}
|
||||||
|
{step < 4 ? (
|
||||||
|
<button type="submit" className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 px-4 rounded-xl font-medium hover:from-purple-600 hover:to-purple-700 transition-all">
|
||||||
|
التالي
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="submit" disabled={isLoading || !formData.agreeTerms}
|
||||||
|
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 px-4 rounded-xl font-medium hover:from-purple-600 hover:to-purple-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{isLoading ? (<div className="flex items-center justify-center gap-2"><Loader2 className="w-5 h-5 animate-spin" /><span>جاري التسجيل...</span></div>) : 'إنشاء حساب وكيل'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.form>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showOtpModal && (
|
||||||
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
|
<motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
|
||||||
|
className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="w-16 h-16 bg-purple-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Shield className="w-8 h-8 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold text-white">التحقق من البريد</h2>
|
||||||
|
<p className="text-gray-400 text-sm mt-1">تم إرسال رمز التحقق إلى</p>
|
||||||
|
<p className="text-purple-400 font-medium text-sm">{formData.email}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">رمز التحقق</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<KeyRound className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input type="text" value={otpCode} maxLength={6}
|
||||||
|
onChange={(e) => setOtpCode(e.target.value)}
|
||||||
|
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 text-white text-center tracking-[0.5em] text-xl"
|
||||||
|
placeholder="------" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button onClick={handleVerifyOTP} disabled={isLoading || !otpCode}
|
||||||
|
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-xl font-medium hover:from-purple-600 hover:to-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
|
||||||
|
{isLoading ? <><Loader2 className="w-5 h-5 animate-spin" /><span>جاري التحقق...</span></> : 'تحقق'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleResendOTP} disabled={isLoading}
|
||||||
|
className="w-full text-center text-purple-400 hover:text-purple-300 text-sm mt-3 disabled:opacity-50">
|
||||||
|
إعادة إرسال الرمز
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
371
app/reports/page.js
Normal file
371
app/reports/page.js
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Calendar,
|
||||||
|
DollarSign,
|
||||||
|
Send,
|
||||||
|
Loader2,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
User,
|
||||||
|
MessageSquare,
|
||||||
|
Hash,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { submitReport, submitReservationReport, submitSaleReport } from '../utils/api';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'general', label: 'تقرير عام', icon: FileText },
|
||||||
|
{ id: 'reservation', label: 'تقرير حجز', icon: Calendar },
|
||||||
|
{ id: 'sale', label: 'تقرير بيع', icon: DollarSign },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ReportsPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!AuthService.isAuthenticated()) {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const [generalForm, setGeneralForm] = useState({ subject: '', body: '' });
|
||||||
|
const [reservationForm, setReservationForm] = useState({ reservationId: '', message: '', reporter: 'customer' });
|
||||||
|
const [saleForm, setSaleForm] = useState({ saleId: '', message: '', reporter: 'buyer' });
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
const validateGeneral = () => {
|
||||||
|
const e = {};
|
||||||
|
if (!generalForm.subject.trim()) e.subject = 'الموضوع مطلوب';
|
||||||
|
if (!generalForm.body.trim()) e.body = 'الوصف مطلوب';
|
||||||
|
setErrors(e);
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateReservation = () => {
|
||||||
|
const e = {};
|
||||||
|
if (!reservationForm.reservationId.trim()) e.reservationId = 'رقم الحجز مطلوب';
|
||||||
|
if (!reservationForm.message.trim()) e.message = 'الرسالة مطلوبة';
|
||||||
|
setErrors(e);
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSale = () => {
|
||||||
|
const e = {};
|
||||||
|
if (!saleForm.saleId.trim()) e.saleId = 'رقم البيع مطلوب';
|
||||||
|
if (!saleForm.message.trim()) e.message = 'الرسالة مطلوبة';
|
||||||
|
setErrors(e);
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGeneralSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateGeneral()) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await submitReport(generalForm.subject, generalForm.body);
|
||||||
|
setIsSuccess(true);
|
||||||
|
toast.success('تم إرسال التقرير بنجاح!', {
|
||||||
|
style: { background: '#dcfce7', color: '#166534' },
|
||||||
|
});
|
||||||
|
setGeneralForm({ subject: '', body: '' });
|
||||||
|
setTimeout(() => setIsSuccess(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'فشل إرسال التقرير', {
|
||||||
|
style: { background: '#fee2e2', color: '#991b1b' },
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReservationSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateReservation()) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await submitReservationReport(reservationForm);
|
||||||
|
setIsSuccess(true);
|
||||||
|
toast.success('تم إرسال تقرير الحجز!', {
|
||||||
|
style: { background: '#dcfce7', color: '#166534' },
|
||||||
|
});
|
||||||
|
setReservationForm({ reservationId: '', message: '', reporter: 'customer' });
|
||||||
|
setTimeout(() => setIsSuccess(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'فشل إرسال التقرير', {
|
||||||
|
style: { background: '#fee2e2', color: '#991b1b' },
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateSale()) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await submitSaleReport(saleForm);
|
||||||
|
setIsSuccess(true);
|
||||||
|
toast.success('تم إرسال تقرير البيع!', {
|
||||||
|
style: { background: '#dcfce7', color: '#166534' },
|
||||||
|
});
|
||||||
|
setSaleForm({ saleId: '', message: '', reporter: 'buyer' });
|
||||||
|
setTimeout(() => setIsSuccess(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'فشل إرسال التقرير', {
|
||||||
|
style: { background: '#fee2e2', color: '#991b1b' },
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabContent = {
|
||||||
|
general: (
|
||||||
|
<form onSubmit={handleGeneralSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">الموضوع</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<FileText className={`w-5 h-5 ${errors.subject ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={generalForm.subject}
|
||||||
|
onChange={(e) => { setGeneralForm({ ...generalForm, subject: e.target.value }); if (errors.subject) setErrors({ ...errors, subject: null }); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.subject ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
|
placeholder="أدخل موضوع التقرير"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.subject && <p className="text-red-500 text-sm mt-1">{errors.subject}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">الوصف</label>
|
||||||
|
<textarea
|
||||||
|
value={generalForm.body}
|
||||||
|
onChange={(e) => { setGeneralForm({ ...generalForm, body: e.target.value }); if (errors.body) setErrors({ ...errors, body: null }); }}
|
||||||
|
rows={5}
|
||||||
|
className={`w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all resize-none ${errors.body ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
|
placeholder="اشرح التفاصيل..."
|
||||||
|
/>
|
||||||
|
{errors.body && <p className="text-red-500 text-sm mt-1">{errors.body}</p>}
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<><Loader2 className="w-5 h-5 animate-spin" /> جاري الإرسال...</>
|
||||||
|
) : isSuccess ? (
|
||||||
|
<><CheckCircle className="w-5 h-5" /> تم الإرسال!</>
|
||||||
|
) : (
|
||||||
|
<><Send className="w-5 h-5" /> إرسال التقرير</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
),
|
||||||
|
reservation: (
|
||||||
|
<form onSubmit={handleReservationSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">رقم الحجز</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Hash className={`w-5 h-5 ${errors.reservationId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={reservationForm.reservationId}
|
||||||
|
onChange={(e) => { setReservationForm({ ...reservationForm, reservationId: e.target.value }); if (errors.reservationId) setErrors({ ...errors, reservationId: null }); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.reservationId ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
|
placeholder="أدخل رقم الحجز"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.reservationId && <p className="text-red-500 text-sm mt-1">{errors.reservationId}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">الرسالة</label>
|
||||||
|
<textarea
|
||||||
|
value={reservationForm.message}
|
||||||
|
onChange={(e) => { setReservationForm({ ...reservationForm, message: e.target.value }); if (errors.message) setErrors({ ...errors, message: null }); }}
|
||||||
|
rows={4}
|
||||||
|
className={`w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all resize-none ${errors.message ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
|
placeholder="اشرح المشكلة..."
|
||||||
|
/>
|
||||||
|
{errors.message && <p className="text-red-500 text-sm mt-1">{errors.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">المبلغ</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{['customer', 'owner'].map((val) => (
|
||||||
|
<button
|
||||||
|
key={val}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setReservationForm({ ...reservationForm, reporter: val })}
|
||||||
|
className={`flex-1 py-3 rounded-xl border-2 font-medium transition-all flex items-center justify-center gap-2 ${
|
||||||
|
reservationForm.reporter === val
|
||||||
|
? 'border-amber-500 bg-amber-50 text-amber-700'
|
||||||
|
: 'border-gray-200 text-gray-600 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
{val === 'customer' ? 'مستأجر' : 'مالك'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<><Loader2 className="w-5 h-5 animate-spin" /> جاري الإرسال...</>
|
||||||
|
) : isSuccess ? (
|
||||||
|
<><CheckCircle className="w-5 h-5" /> تم الإرسال!</>
|
||||||
|
) : (
|
||||||
|
<><Send className="w-5 h-5" /> إرسال التقرير</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
),
|
||||||
|
sale: (
|
||||||
|
<form onSubmit={handleSaleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">رقم البيع</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
<Hash className={`w-5 h-5 ${errors.saleId ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={saleForm.saleId}
|
||||||
|
onChange={(e) => { setSaleForm({ ...saleForm, saleId: e.target.value }); if (errors.saleId) setErrors({ ...errors, saleId: null }); }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all ${errors.saleId ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
|
placeholder="أدخل رقم البيع"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.saleId && <p className="text-red-500 text-sm mt-1">{errors.saleId}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">الرسالة</label>
|
||||||
|
<textarea
|
||||||
|
value={saleForm.message}
|
||||||
|
onChange={(e) => { setSaleForm({ ...saleForm, message: e.target.value }); if (errors.message) setErrors({ ...errors, message: null }); }}
|
||||||
|
rows={4}
|
||||||
|
className={`w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all resize-none ${errors.message ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
|
placeholder="اشرح المشكلة..."
|
||||||
|
/>
|
||||||
|
{errors.message && <p className="text-red-500 text-sm mt-1">{errors.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">المبلغ</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{['buyer', 'seller'].map((val) => (
|
||||||
|
<button
|
||||||
|
key={val}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSaleForm({ ...saleForm, reporter: val })}
|
||||||
|
className={`flex-1 py-3 rounded-xl border-2 font-medium transition-all flex items-center justify-center gap-2 ${
|
||||||
|
saleForm.reporter === val
|
||||||
|
? 'border-amber-500 bg-amber-50 text-amber-700'
|
||||||
|
: 'border-gray-200 text-gray-600 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
{val === 'buyer' ? 'مشتري' : 'بائع'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<><Loader2 className="w-5 h-5 animate-spin" /> جاري الإرسال...</>
|
||||||
|
) : isSuccess ? (
|
||||||
|
<><CheckCircle className="w-5 h-5" /> تم الإرسال!</>
|
||||||
|
) : (
|
||||||
|
<><Send className="w-5 h-5" /> إرسال التقرير</>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</form>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="container mx-auto px-4 max-w-3xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإبلاغ عن مشكلة</h1>
|
||||||
|
<p className="text-gray-600">أخبرنا عن المشكلة التي تواجهها وسنقوم بمعالجتها</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<div className="flex border-b border-gray-200">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => { setActiveTab(tab.id); setIsSuccess(false); setErrors({}); }}
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 py-4 text-sm font-medium transition-all relative ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'text-amber-600'
|
||||||
|
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
{tab.label}
|
||||||
|
{activeTab === tab.id && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="activeTab"
|
||||||
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-amber-500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeTab}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
{tabContent[activeTab]}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
247
app/settings/page.js
Normal file
247
app/settings/page.js
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Shield,
|
||||||
|
Trash2,
|
||||||
|
LogOut,
|
||||||
|
ChevronLeft,
|
||||||
|
Bell,
|
||||||
|
Lock,
|
||||||
|
Eye,
|
||||||
|
FileText,
|
||||||
|
HelpCircle,
|
||||||
|
MessageCircle,
|
||||||
|
Loader2,
|
||||||
|
AlertTriangle,
|
||||||
|
X
|
||||||
|
} from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
import { changePassword, deleteMyAccount } from '../utils/api';
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [deletePassword, setDeletePassword] = useState('');
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
|
const handleSignOut = () => {
|
||||||
|
AuthService.deleteToken();
|
||||||
|
toast.success('تم تسجيل الخروج بنجاح');
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAccount = async () => {
|
||||||
|
if (!deletePassword) {
|
||||||
|
toast.error('الرجاء إدخال كلمة المرور');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteMyAccount(deletePassword);
|
||||||
|
AuthService.deleteToken();
|
||||||
|
toast.success('تم حذف الحساب بنجاح');
|
||||||
|
router.push('/');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'فشل حذف الحساب');
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
setDeletePassword('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'الحساب',
|
||||||
|
items: [
|
||||||
|
{ icon: User, label: 'الملف الشخصي', href: '/profile', desc: 'عرض وتعديل معلوماتك الشخصية' },
|
||||||
|
{ icon: Lock, label: 'تغيير كلمة المرور', href: '/change-password', desc: 'تحديث كلمة المرور الخاصة بك' },
|
||||||
|
{ icon: Shield, label: 'التحقق من الحساب', href: '/account-verification', desc: 'تأكيد البريد الإلكتروني ورقم الهاتف' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'الإشعارات',
|
||||||
|
items: [
|
||||||
|
{ icon: Bell, label: 'الإشعارات', href: '/notifications', desc: 'إدارة تفضيلات الإشعارات' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'الدعم',
|
||||||
|
items: [
|
||||||
|
{ icon: HelpCircle, label: 'الأسئلة الشائعة', href: '/faq', desc: 'إجابات للأسئلة المتكررة' },
|
||||||
|
{ icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
||||||
|
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
||||||
|
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.08 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 max-w-2xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex items-center gap-3 mb-8"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">الإعدادات</h1>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{sections.map((section) => (
|
||||||
|
<motion.div
|
||||||
|
key={section.title}
|
||||||
|
variants={itemVariants}
|
||||||
|
className="bg-white rounded-2xl shadow-sm overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">{section.title}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-50">
|
||||||
|
{section.items.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
className="flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors">
|
||||||
|
<Icon className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{item.label}</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
<ChevronLeft className="w-4 h-4 text-gray-400" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants} className="space-y-4">
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">الأمان</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteDialog(true)}
|
||||||
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">حذف الحساب</span>
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-gray-500 mt-2 pr-12">
|
||||||
|
سيتم حذف جميع بياناتك بشكل دائم ولا يمكن التراجع عن هذا الإجراء
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
<div className="p-6">
|
||||||
|
<button
|
||||||
|
onClick={handleSignOut}
|
||||||
|
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">تسجيل الخروج</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDeleteDialog && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">حذف الحساب</h3>
|
||||||
|
<p className="text-sm text-gray-500">هذا الإجراء لا يمكن التراجع عنه</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
|
||||||
|
className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
أدخل كلمة المرور لتأكيد حذف حسابك نهائياً. سيتم حذف جميع بياناتك وملفاتك بشكل دائم.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={deletePassword}
|
||||||
|
onChange={(e) => setDeletePassword(e.target.value)}
|
||||||
|
placeholder="كلمة المرور"
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-red-500 focus:border-transparent outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
|
||||||
|
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteAccount}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="flex-1 px-4 py-3 rounded-xl bg-red-600 text-white hover:bg-red-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||||
|
{isDeleting ? 'جاري الحذف...' : 'تأكيد الحذف'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
165
app/support/page.js
Normal file
165
app/support/page.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { MessageCircle, Mail, Phone, MapPin, Send, Loader2 } from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import { submitReport } from '../utils/api';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
|
||||||
|
export default function SupportPage() {
|
||||||
|
const [subject, setSubject] = useState('');
|
||||||
|
const [body, setBody] = useState('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!subject.trim() || !body.trim()) {
|
||||||
|
toast.error('يرجى تعبئة جميع الحقول');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AuthService.isAuthenticated()) {
|
||||||
|
toast.error('يرجى تسجيل الدخول أولاً لإرسال طلب دعم');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await submitReport(subject, body);
|
||||||
|
toast.success('تم إرسال طلب الدعم بنجاح');
|
||||||
|
setSubject('');
|
||||||
|
setBody('');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('حدث خطأ أثناء إرسال الطلب. حاول مرة أخرى');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
<div className="container mx-auto px-4 max-w-5xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
|
<MessageCircle className="w-10 h-10 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">خدمة العملاء</h1>
|
||||||
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
|
نحن هنا لمساعدتك. تواصل معنا عبر النموذج أدناه أو من خلال معلومات الاتصال المباشرة
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="md:col-span-2"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">أرسل لنا رسالة</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
الموضوع
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={subject}
|
||||||
|
onChange={(e) => setSubject(e.target.value)}
|
||||||
|
placeholder="اكتب موضوع الرسالة..."
|
||||||
|
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
الرسالة
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={body}
|
||||||
|
onChange={(e) => setBody(e.target.value)}
|
||||||
|
rows={6}
|
||||||
|
placeholder="اكتب تفاصيل طلبك..."
|
||||||
|
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full py-3 bg-amber-500 text-white rounded-xl font-medium hover:bg-amber-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
{isSubmitting ? 'جاري الإرسال...' : 'إرسال'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 mb-4">معلومات الاتصال</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
|
<Mail className="w-5 h-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">البريد الإلكتروني</p>
|
||||||
|
<p className="font-medium text-gray-900">support@sweethome.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
|
<Phone className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">رقم الهاتف</p>
|
||||||
|
<p className="font-medium text-gray-900" dir="ltr">+963 11 234 5678</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
|
<MapPin className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">العنوان</p>
|
||||||
|
<p className="font-medium text-gray-900">دمشق، سورية</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 rounded-2xl border border-amber-200 p-6">
|
||||||
|
<h3 className="text-lg font-bold text-amber-800 mb-2">ساعات العمل</h3>
|
||||||
|
<div className="space-y-2 text-amber-700">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>السبت - الخميس</span>
|
||||||
|
<span>9:00 - 18:00</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>الجمعة</span>
|
||||||
|
<span>مغلق</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
app/terms/page.js
Normal file
114
app/terms/page.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { FileText, Shield, CheckCircle } from 'lucide-react';
|
||||||
|
import { getTerms } from '../utils/api';
|
||||||
|
|
||||||
|
const staticTerms = [
|
||||||
|
{
|
||||||
|
title: 'مقدمة',
|
||||||
|
content:
|
||||||
|
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة. تحتفظ المنصة بحق تعديل هذه الشروط في أي وقت مع إشعار المستخدمين.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'استخدام المنصة',
|
||||||
|
content:
|
||||||
|
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني أو مخالف للقوانين السارية. كما يلتزم المستخدم بعدم محاولة الوصول غير المصرح به إلى أي جزء من المنصة أو الخوادم أو الأنظمة المتصلة بها.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'حقوق ومسؤوليات المالك',
|
||||||
|
content:
|
||||||
|
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر. يلتزم المالك بتحديث معلومات العقار بشكل دوري. المنصة غير مسؤولة عن أي نزاعات تنشأ بين المالك والمستأجر. يجب على المالك الالتزام بجميع القوانين المحلية المتعلقة بتأجير العقارات.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'حقوق ومسؤوليات المستأجر',
|
||||||
|
content:
|
||||||
|
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات. يجب على المستأجر الالتزام بقوانين المنزل ومواعيد تسجيل الوصول والمغادرة. المنصة غير مسؤولة عن أي سلوك غير لائق من قبل المستأجرين.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'الدفع والعمولات',
|
||||||
|
content:
|
||||||
|
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز. جميع المدفوعات تتم عبر قنوات الدفع الآمنة في المنصة. أي رسوم إلغاء أو استرداد تخضع لسياسة الإلغاء المحددة في كل عقار.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'خصوصية البيانات',
|
||||||
|
content:
|
||||||
|
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا. نحن لا نشارك معلوماتك مع أطراف ثالثة دون موافقتك، إلا عندما يقتضي القانون ذلك.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function TermsPage() {
|
||||||
|
const [terms, setTerms] = useState(staticTerms);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchTerms() {
|
||||||
|
try {
|
||||||
|
const data = await getTerms();
|
||||||
|
if (data && Array.isArray(data) && data.length > 0) {
|
||||||
|
setTerms(data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall back to static terms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchTerms();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
|
<FileText className="w-10 h-10 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">شروط الاستخدام</h1>
|
||||||
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
|
يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{terms.map((term, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
|
||||||
|
<Shield className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
|
||||||
|
<p className="text-gray-600 leading-relaxed">{term.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.6 }}
|
||||||
|
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
|
||||||
|
<p className="text-amber-700">
|
||||||
|
تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
181
app/utils/api.js
181
app/utils/api.js
@ -664,6 +664,31 @@ export async function addRentProperty(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function editRentProperty(id, data) {
|
||||||
|
console.log('[API] Editing rent property:', id, data.PropertyInformation?.Address);
|
||||||
|
return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addSaleProperty(data) {
|
||||||
|
console.log('[API] Adding sale property');
|
||||||
|
return apiFetch('/SaleProperties/AddSaleProperty', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMySaleListings() {
|
||||||
|
console.log('[API] Fetching my sale listings');
|
||||||
|
return apiFetch('/SaleProperties/GetMySaleListings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSalePropertyById(id) {
|
||||||
|
return apiFetch(`/SaleProperties/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Currencies ───
|
// ─── Currencies ───
|
||||||
|
|
||||||
export async function getCurrencies() {
|
export async function getCurrencies() {
|
||||||
@ -921,3 +946,159 @@ export async function updateBookingStatus(bookingId, status) {
|
|||||||
body: JSON.stringify({ bookingId, status }),
|
body: JSON.stringify({ bookingId, status }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Owner / Reservations ───
|
||||||
|
|
||||||
|
export async function getOwnerReservationRequests() {
|
||||||
|
return apiFetch('/Reservations/GetOwnerResevationRequests');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOwnerReservationsByStatuses(filterStatuses) {
|
||||||
|
return apiFetch('/Reservations/GetAllReservationsByStateForOwner', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ filterStatuses }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserReservations() {
|
||||||
|
return apiFetch('/Reservations/GetUserResevations');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ownerConfirmReservation(id) {
|
||||||
|
return apiFetch(`/Reservations/OwnerConfirmReservation/owner-confirm/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Payments ───
|
||||||
|
|
||||||
|
export async function payDeposit(data) {
|
||||||
|
return apiFetch('/Reservations/PayDeposit/pay-deposit', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Owner Contact & Stats ───
|
||||||
|
|
||||||
|
export async function getOwnerContactInformation(propertyInformationId) {
|
||||||
|
return apiFetch(`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOwnerStatistics() {
|
||||||
|
return apiFetch('/Statistics/GetOwnerStatistics');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Agent Registration ───
|
||||||
|
|
||||||
|
export async function registerRealEstateAgent(formData) {
|
||||||
|
console.log('[API] Registering real estate agent (multipart)');
|
||||||
|
const token = AuthService.getToken();
|
||||||
|
const res = await fetch(`${API_BASE}/RealEstateAgent/Add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...(token && { Authorization: `Bearer ${token}` }) },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
let data = null;
|
||||||
|
try { data = text ? JSON.parse(text) : null; if (data && typeof data === 'object' && 'data' in data) data = data.data; } catch { data = text; }
|
||||||
|
return { status: res.status, data, ok: res.ok || res.status === 206, message: data?.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Change Password ───
|
||||||
|
|
||||||
|
export async function changePassword(oldPassword, newPassword) {
|
||||||
|
return apiFetch(`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Forget Password (OTP flow) ───
|
||||||
|
|
||||||
|
export async function requestForgetPasswordOtp(email) {
|
||||||
|
return apiFetch(`/User/ForgetPassword?email=${encodeURIComponent(email)}`, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyForgetPasswordOtp(email, code, newPassword) {
|
||||||
|
return apiFetch(`/User/VerifyForgetPasswordOTP?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}&newPassword=${encodeURIComponent(newPassword)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reset Password (token flow) ───
|
||||||
|
|
||||||
|
export async function resetPassword(token) {
|
||||||
|
return apiFetch(`/Auth/ResetPassword?token=${encodeURIComponent(token)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Delete Account ───
|
||||||
|
|
||||||
|
export async function deleteMyAccount(password) {
|
||||||
|
return apiFetch(`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Set FCM Token ───
|
||||||
|
|
||||||
|
export async function setFCMToken(token, deviceType = 2) {
|
||||||
|
return apiFetch('/User/SetFCMToken', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ token, deviceType }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Filter Rent Properties ───
|
||||||
|
|
||||||
|
export async function filterRentProperties(params = {}) {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([k, v]) => { if (v != null && v !== '') qs.set(k, v); });
|
||||||
|
const query = qs.toString();
|
||||||
|
return apiFetch(`/RentProperties/FilterRentProperties${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reports ───
|
||||||
|
|
||||||
|
export async function submitReport(subject, body) {
|
||||||
|
return apiFetch('/Reports', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ subject, body }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitReservationReport(data) {
|
||||||
|
return apiFetch('/ReservationReports', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateReservationReport(id, data) {
|
||||||
|
return apiFetch(`/ReservationReports/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitSaleReport(data) {
|
||||||
|
return apiFetch('/SaleReports', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSaleReport(id, data) {
|
||||||
|
return apiFetch(`/SaleReports/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Terms (Add) ───
|
||||||
|
|
||||||
|
export async function addTerm(name, description) {
|
||||||
|
return apiFetch('/Terms', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name, description }),
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user