Compare commits

..

76 Commits

Author SHA1 Message Date
01ac4f8d6c add report on reservation api
Some checks failed
Build frontend / build (push) Failing after 48s
2026-06-15 10:46:18 -07:00
f2724a5cd2 GetMyTransactions api
Some checks failed
Build frontend / build (push) Failing after 1m2s
2026-06-15 10:18:15 -07:00
bef133ad5b Fixing the map problem
Some checks failed
Build frontend / build (push) Failing after 1m1s
2026-06-14 18:58:06 +03:00
a9eb1cc684 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m18s
2026-06-14 08:33:02 -07:00
13b563e35e fixing SendGeneralReport 2026-06-14 08:22:59 -07:00
5d593d593f Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m30s
2026-06-14 18:04:26 +03:00
51850b85c2 Added blocked page with api 2026-06-14 18:04:05 +03:00
8cacf464d1 privacy screen api
Some checks failed
Build frontend / build (push) Failing after 1m27s
2026-06-14 07:45:13 -07:00
91de3d47b7 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m0s
2026-06-12 12:47:58 -07:00
71b1a71904 Delete admin
Some checks failed
Build frontend / build (push) Failing after 1m20s
2026-06-10 19:50:49 +03:00
34da1314d4 The footer only appears in homepage
All checks were successful
Build frontend / build (push) Successful in 1m1s
2026-06-09 23:37:47 +03:00
ce6caf08eb Added Active or DeActive for owner with API
All checks were successful
Build frontend / build (push) Successful in 1m10s
2026-06-09 18:21:52 +03:00
845ba2436a SendGeneralReport api 2026-06-09 03:38:37 -07:00
471332b59f fixing agent api
All checks were successful
Build frontend / build (push) Successful in 57s
2026-06-06 03:55:53 -07:00
53a83494b7 owner account
All checks were successful
Build frontend / build (push) Successful in 1m16s
2026-06-05 18:33:51 -07:00
6bc0c8ba27 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m31s
2026-06-04 23:33:25 +03:00
74dc12171d test
All checks were successful
Build frontend / build (push) Successful in 2m22s
2026-06-04 23:09:48 +03:00
9fdeadaa61 Added bottom navbar 2026-06-04 22:54:34 +03:00
d11f105dfc if the user role is admin dont show the booking section
All checks were successful
Build frontend / build (push) Successful in 50s
2026-05-30 13:45:32 +03:00
8d4ac3ddd6 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m16s
2026-05-30 13:24:31 +03:00
b4196c340d removed the contact information 2026-05-30 13:24:22 +03:00
ddf5367f92 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m17s
Co-authored-by: Copilot <copilot@github.com>
2026-05-28 18:30:39 +03:00
417b6cb393 Edit AddProperty 2026-05-28 17:42:51 +03:00
819d5ea802 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 23:04:00 +03:00
f1a14c2f7a added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 23:00:19 +03:00
29de55196a added a swicher on the properties
Some checks failed
Build frontend / build (push) Failing after 40s
2026-05-26 22:53:18 +03:00
01b966c1d2 added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 22:40:54 +03:00
e1204bf69e added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 21:54:22 +03:00
a0a78079c5 added the descption
All checks were successful
Build frontend / build (push) Successful in 52s
2026-05-26 21:49:22 +03:00
e201b1cd8a added the descption
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 21:28:14 +03:00
3466d4715a added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 21:16:10 +03:00
88884ed2d7 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 46s
2026-05-26 21:00:14 +03:00
e21dc53227 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 20:54:25 +03:00
82e39e6f90 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 20:28:51 +03:00
caddf55811 added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 46s
2026-05-26 20:24:37 +03:00
a3657e5c2f added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 19:53:28 +03:00
b8d779a1d0 added the descption
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 19:25:10 +03:00
c53a8eeb6d added the descption
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 19:18:49 +03:00
9979877e1c added the descption
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 19:04:57 +03:00
9fda2618c8 added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 18:43:47 +03:00
bf044cef45 added the descption
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 18:22:21 +03:00
3f24696c48 added the descption
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 18:01:18 +03:00
9c79e0648b added the descption
All checks were successful
Build frontend / build (push) Successful in 47s
2026-05-26 17:49:39 +03:00
13a0649806 added the descption
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 17:31:01 +03:00
1ae3a635f7 added the descption
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 17:06:01 +03:00
0e61dd5789 added the descption
All checks were successful
Build frontend / build (push) Successful in 46s
2026-05-26 17:00:57 +03:00
50a4816327 added the calendre and the price toggle
All checks were successful
Build frontend / build (push) Successful in 1m7s
2026-05-26 16:38:16 +03:00
09bbf07d8c edited the edit properrty
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 04:35:03 +03:00
439f69419f fixed the details
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 04:16:53 +03:00
71ce933c6a fixed the details
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-26 03:22:14 +03:00
5045e69191 fixed the details
All checks were successful
Build frontend / build (push) Successful in 1m36s
2026-05-26 02:59:20 +03:00
f91c78f759 added the allowedPaymentPeriod
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 02:35:41 +03:00
80ea9af86b fixed the details
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 02:34:18 +03:00
4c350d7589 fixed the details
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-26 01:48:12 +03:00
ca94c5fd87 fixed the details
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 01:44:23 +03:00
c40486b99e fixed the details
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-26 01:20:05 +03:00
9f6492cf41 fixed the details
All checks were successful
Build frontend / build (push) Successful in 43s
2026-05-26 00:20:20 +03:00
085c60ec33 editing the add rent property mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 50s
2026-05-25 23:55:02 +03:00
2c4a163cdb editing the add rent property mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 23:18:03 +03:00
8d48fcae99 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 23:07:29 +03:00
af54bded13 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 22:54:38 +03:00
4988302bc1 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 45s
2026-05-25 22:50:38 +03:00
427dc74e7f fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 44s
2026-05-25 22:42:53 +03:00
7b333f9b53 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-25 22:37:05 +03:00
291e82a1b4 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 42s
2026-05-25 22:28:26 +03:00
9d8ccbac3c fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 41s
2026-05-25 22:19:03 +03:00
7a3248aec5 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 49s
2026-05-25 22:14:37 +03:00
aff9912ac2 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 1m1s
2026-05-25 21:53:57 +03:00
50d636a577 fixed my propries page and fixed the sidebar again mouaz is the best in the west
All checks were successful
Build frontend / build (push) Successful in 1m11s
2026-05-25 21:37:37 +03:00
00ccf5f262 the best in the west is mouaz
All checks were successful
Build frontend / build (push) Successful in 55s
2026-05-25 21:27:39 +03:00
a5577765ed Edit register for tenant
All checks were successful
Build frontend / build (push) Successful in 1m19s
2026-05-10 16:21:32 +03:00
4bedd24315 Edit register
All checks were successful
Build frontend / build (push) Successful in 47s
2026-05-06 20:08:26 +03:00
378d2d0f37 Edit register for owner
All checks were successful
Build frontend / build (push) Successful in 1m19s
2026-05-06 18:54:05 +03:00
5936e0f553 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m23s
2026-05-05 17:03:06 -07:00
84f5bbe9db Fixed owner reservation approval and rejection API endpoints 2026-05-05 17:02:21 -07:00
46cbce1d88 Added API to map with markers in main page
All checks were successful
Build frontend / build (push) Successful in 53s
2026-05-02 17:20:35 +03:00
63 changed files with 12651 additions and 8689 deletions

View File

@ -7,7 +7,7 @@ import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks";
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
import FloatingSidebar from '@/app/components/FloatingSidebar';
import BottomNav from './components/BottomNav';
import {
Globe,
LogIn,
@ -25,7 +25,6 @@ import {
Mail,
MapPin,
Camera,
Shield,
Bell,
Home,
ChevronDown,
@ -34,8 +33,9 @@ import {
TrendingUp,
CalendarDays,
Clock,
Users,
DollarSign,
Star,
FileText,
} from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
@ -78,9 +78,7 @@ export default function ClientLayout({ children }) {
name: authUser.name || authUser.email,
email: authUser.email,
phone: authUser.phone,
role: AuthService.isAdmin() ? UserRole.ADMIN
: AuthService.isOwner() ? UserRole.OWNER
: UserRole.CUSTOMER,
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
});
} else {
setUser(null);
@ -128,6 +126,7 @@ export default function ClientLayout({ children }) {
const isAuthPage = [
"/login",
"/blocked",
"/register",
"/forgot-password",
"/auth/choose-role",
@ -136,7 +135,6 @@ export default function ClientLayout({ children }) {
const isProfilePage = pathname === "/profile";
const isOwner = user?.role === UserRole.OWNER;
const isAdmin = user?.role === UserRole.ADMIN;
const isCustomer = user?.role === UserRole.CUSTOMER;
const isAuthenticated = !!user;
@ -160,7 +158,7 @@ export default function ClientLayout({ children }) {
return (
<>
{!isAuthPage && (
{!isAuthPage && !isAuthenticated && (
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div
@ -232,14 +230,6 @@ export default function ClientLayout({ children }) {
<NavLink href="/">الرئيسية</NavLink>
<NavLink href="/properties">عقاراتنا</NavLink>
{isAdmin && (
<NavLink href="/admin">
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</NavLink>
)}
{isOwner && (
<>
@ -366,6 +356,48 @@ export default function ClientLayout({ children }) {
</div>
</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 && (
<>
<div className="border-t border-gray-100 my-2"></div>
@ -439,71 +471,9 @@ export default function ClientLayout({ children }) {
</p>
</div>
</Link>
</>
)}
{isAdmin && (
<>
<div className="border-t border-gray-100 my-2"></div>
<Link
href="/admin"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Shield 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="/admin/users"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Users 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="/admin/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)}
>
<Building 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="/admin/bookings"
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
onClick={() => setShowUserMenu(false)}
>
<Calendar 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="/admin/ledger"
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)}
>
@ -511,7 +481,7 @@ export default function ClientLayout({ children }) {
<div>
<p className="font-medium">دفتر الحسابات</p>
<p className="text-xs text-gray-500">
إدارة المعاملات المالية
سجل المعاملات المالية
</p>
</div>
</Link>
@ -535,6 +505,48 @@ export default function ClientLayout({ children }) {
</p>
</div>
</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>
</>
)}
@ -630,15 +642,6 @@ export default function ClientLayout({ children }) {
</div>
<div className="border-t border-gray-200 my-2"></div>
{isAdmin && (
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
<span className="flex items-center gap-2">
<Shield className="w-4 h-4" />
الإدارة
</span>
</MobileNavLink>
)}
{isOwner && (
<>
<MobileNavLink
@ -706,18 +709,21 @@ export default function ClientLayout({ children }) {
</nav>
)}
<main
className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
>
<NotificationsProvider>
<FavoritesProvider>
<main
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
>
{children}
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
</FavoritesProvider>
</NotificationsProvider>
</main>
{!isAuthPage && !isProfilePage && (
{isAuthenticated && !isAuthPage && (
<BottomNav isOwner={isOwner} />
)}
</FavoritesProvider>
</NotificationsProvider>
{pathname === "/" && (
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div
@ -768,16 +774,6 @@ export default function ClientLayout({ children }) {
{t("ourProducts")}
</Link>
</li>
{isAdmin && (
<li>
<Link
href="/admin"
className="text-gray-400 hover:text-white transition-colors block py-1"
>
الإدارة
</Link>
</li>
)}
</ul>
</div>
<div>

View 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>
);
}

View File

@ -1,113 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import AuthService from '@/app/services/AuthService';
import Link from 'next/link';
export default function AddAdminPage() {
const [isAdmin, setIsAdmin] = useState(false);
const [checked, setChecked] = useState(false);
const [formState, setFormState] = useState({ fullName: '', email: '', password: '' });
const [saved, setSaved] = useState(false);
useEffect(() => {
setIsAdmin(AuthService.isAuthenticated() && AuthService.isAdmin());
setChecked(true);
}, []);
const handleChange = (field) => (event) => {
setFormState((prev) => ({ ...prev, [field]: event.target.value }));
};
const handleSubmit = (event) => {
event.preventDefault();
setSaved(true);
console.log('Add admin payload', formState);
};
if (!checked) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-md text-center bg-white rounded-3xl shadow-lg border border-gray-200 p-8">
<Link href="/" className="inline-flex items-center justify-center px-6 py-3 rounded-full bg-amber-500 text-white hover:bg-amber-600 transition-colors">
العودة للرئيسية
</Link>
</div>
</div>
);
}
return (
<main className="min-h-screen bg-slate-50 p-6 md:p-10">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<p className="text-sm text-amber-600 uppercase tracking-[0.2em]">لوحة المدير</p>
<h1 className="text-3xl font-bold text-slate-900 mt-3">إضافة مدير جديد</h1>
<p className="text-slate-500 mt-2">انشئ حساب مسؤول جديد مع صلاحيات الإدارة.</p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-[1.5fr_0.8fr]">
<section className="rounded-[28px] bg-white p-8 shadow-sm border border-slate-200">
<h2 className="text-xl font-semibold mb-6">بيانات المدير</h2>
<form onSubmit={handleSubmit} className="space-y-5">
<label className="block">
<span className="text-sm font-medium text-slate-700">الاسم الكامل</span>
<input
type="text"
value={formState.fullName}
onChange={handleChange('fullName')}
className="mt-2 w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
placeholder="مثال: محمد الأحمد"
required
/>
</label>
<label className="block">
<span className="text-sm font-medium text-slate-700">البريد الإلكتروني</span>
<input
type="email"
value={formState.email}
onChange={handleChange('email')}
className="mt-2 w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
placeholder="admin@example.com"
required
/>
</label>
<label className="block">
<span className="text-sm font-medium text-slate-700">كلمة المرور</span>
<input
type="password"
value={formState.password}
onChange={handleChange('password')}
className="mt-2 w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
placeholder="••••••••"
required
/>
</label>
<button type="submit" className="inline-flex items-center justify-center rounded-2xl bg-amber-600 px-6 py-3 text-white font-semibold shadow-lg shadow-amber-100 transition hover:bg-amber-700">
حفظ المدير الجديد
</button>
</form>
{saved && (
<div className="mt-6 rounded-3xl bg-emerald-50 border border-emerald-200 p-4 text-emerald-700">
تم حفظ بيانات المدير بنجاح
</div>
)}
</section>
</div>
</div>
</main>
);
}

View File

@ -1,27 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
import Link from 'next/link';
export default function Error({ error, reset }) {
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">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center max-w-md">
<div className="w-20 h-20 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertTriangle className="w-10 h-10 text-red-500" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">حدث خطأ</h2>
<p className="text-gray-400 mb-8">نعتذر، حدث خطأ أثناء تحميل الصفحة</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
<RefreshCw className="w-5 h-5" /> إعادة المحاولة
</button>
<Link href="/" className="flex items-center gap-2 bg-white/10 text-gray-300 px-6 py-3 rounded-xl font-medium hover:bg-white/20 transition-colors">
<Home className="w-5 h-5" /> الرئيسية
</Link>
</div>
</motion.div>
</div>
);
}

View File

@ -1,14 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400 text-lg">جاري التحميل...</p>
</motion.div>
</div>
);
}

View File

@ -1,230 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import {
Home,
Calendar,
Users,
DollarSign,
TrendingUp,
Bell,
Frown
} from 'lucide-react';
import DashboardStats from '../components/admin/DashboardStats';
import PropertiesTable from '../components/admin/PropertiesTable';
import BookingRequests from '../components/admin/BookingRequests';
import UsersList from '../components/admin/UsersList';
import LedgerBook from '../components/admin/LedgerBook';
import AddPropertyForm from '../components/admin/AddPropertyForm';
import { PropertyProvider } from '../contexts/PropertyContext';
import AuthService from '../services/AuthService';
import '../i18n/config';
export default function AdminPage() {
const { t, i18n } = useTranslation();
const [activeTab, setActiveTab] = useState('dashboard');
const [showAddProperty, setShowAddProperty] = useState(false);
const [notifications, setNotifications] = useState(3);
const [isAdmin, setIsAdmin] = useState(false);
const [checked, setChecked] = useState(false);
useEffect(() => {
setIsAdmin(AuthService.isAuthenticated() && AuthService.isAdmin());
setChecked(true);
}, []);
// ─── 404 for non-admins ───
if (checked && !isAdmin) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center max-w-md"
>
<div className="mb-6">
<svg viewBox="0 0 200 180" className="w-72 h-52 mx-auto">
<circle cx="100" cy="70" r="60" fill="#fef3c7" />
<circle cx="80" cy="60" r="8" fill="#92400e" />
<circle cx="120" cy="60" r="8" fill="#92400e" />
<path d="M80 85 Q100 75 120 85" stroke="#92400e" strokeWidth="3" fill="none" strokeLinecap="round" />
<text x="100" y="140" textAnchor="middle" fontSize="16" fontWeight="bold" fill="#6b7280">عذراً!</text>
<text x="100" y="160" textAnchor="middle" fontSize="12" fill="#9ca3af">الصفحة غير موجودة</text>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">404 - الصفحة غير موجودة</h2>
<p className="text-gray-500 mb-8">عذراً، لا يمكنك الوصول إلى هذه الصفحة</p>
<Link
href="/"
className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors"
>
<Home className="w-5 h-5" />
العودة للرئيسية
</Link>
</motion.div>
</div>
);
}
if (!checked) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
const tabs = [
{ id: 'dashboard', label: 'لوحة التحكم', icon: Home },
{ id: 'properties', label: 'العقارات', icon: Home },
{ id: 'bookings', label: 'طلبات الحجز', icon: Calendar, badge: notifications },
{ id: 'users', label: 'المستخدمين', icon: Users },
{ id: 'ledger', label: 'دفتر الحسابات', icon: DollarSign },
// { id: 'reports', label: 'التقارير', icon: TrendingUp }
];
return (
<PropertyProvider>
<div className={`min-h-screen bg-gray-50 p-4 md:p-6 ${i18n.language === 'ar' ? 'text-right' : 'text-left'}`}>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2">
{t('adminDashboard')}
</h1>
<p className="text-gray-600">
إدارة العقارات، الحجوزات، والحسابات المالية
</p>
</div>
<button className="relative p-2 hover:bg-gray-100 rounded-lg">
<Bell className="w-6 h-6 text-gray-600" />
{notifications > 0 && (
<span className="absolute top-0 right-0 w-4 h-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
{notifications}
</span>
)}
</button>
</div>
</motion.div>
<div className="mb-6 border-b border-gray-200">
<div className="flex flex-wrap gap-2">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
px-4 py-3 font-medium text-sm rounded-t-lg transition-all relative
${activeTab === tab.id
? 'bg-white border-t border-x border-gray-300 text-blue-700'
: 'text-gray-700 hover:text-blue-600 hover:bg-gray-100'
}
`}
>
<div className="flex items-center gap-2">
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
{tab.badge && (
<span className="bg-red-500 text-white text-xs px-2 py-0.5 rounded-full">
{tab.badge}
</span>
)}
</div>
</button>
);
})}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
{activeTab === 'dashboard' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<DashboardStats />
</motion.div>
)}
{activeTab === 'properties' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-xl font-bold">إدارة العقارات</h2>
<p className="text-gray-600 text-sm">إضافة وتعديل العقارات مع تحديد نسب الأرباح</p>
</div>
<button
onClick={() => setShowAddProperty(true)}
className="bg-blue-700 text-white px-4 py-2 rounded-lg hover:bg-blue-800"
>
إضافة عقار جديد
</button>
</div>
<PropertiesTable />
</motion.div>
)}
{activeTab === 'bookings' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<BookingRequests />
</motion.div>
)}
{activeTab === 'users' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<UsersList />
</motion.div>
)}
{activeTab === 'ledger' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<LedgerBook userType="admin" />
</motion.div>
)}
{activeTab === 'reports' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="text-center py-12 text-gray-500">
قريباً... تقارير متقدمة
</div>
</motion.div>
)}
</div>
{showAddProperty && (
<AddPropertyForm
onClose={() => setShowAddProperty(false)}
onSuccess={() => {
setShowAddProperty(false);
}}
/>
)}
</div>
</PropertyProvider>
);
}

View File

@ -1,85 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import AuthService from '@/app/services/AuthService';
import Link from 'next/link';
const initialPolicy = `1. نحترم خصوصيتك ونلتزم بحماية بياناتك الشخصية.
2. يتم استخدام المعلومات لتحسين تجربة المستخدم وتأمين الخدمة.
3. لا نشارك البيانات مع أطراف خارجية بدون موافقتك.
4. يمكنك طلب حذف بياناتك من النظام في أي وقت.`;
export default function PrivacyPolicyAdminPage() {
const [isAdmin, setIsAdmin] = useState(false);
const [checked, setChecked] = useState(false);
const [policyText, setPolicyText] = useState(initialPolicy);
const [saved, setSaved] = useState(false);
useEffect(() => {
setIsAdmin(AuthService.isAuthenticated() && AuthService.isAdmin());
setChecked(true);
}, []);
const handleSave = (event) => {
event.preventDefault();
setSaved(true);
console.log('Privacy policy updated:', policyText);
};
if (!checked) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-14 h-14 border-4 border-amber-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="max-w-md text-center bg-white rounded-3xl shadow-lg border border-gray-200 p-8">
<p className="text-gray-600 mb-6">هذه الصفحة لتحرير سياسة الخصوصية ولا يمكن الوصول إليها إلا للمدير.</p>
<Link href="/" className="inline-flex items-center justify-center px-6 py-3 rounded-full bg-amber-500 text-white hover:bg-amber-600 transition-colors">
العودة للرئيسية
</Link>
</div>
</div>
);
}
return (
<main className="min-h-screen bg-slate-50 p-6 md:p-10">
<div className="max-w-4xl mx-auto">
<div className="mb-8 rounded-[28px] bg-white p-8 shadow-sm border border-slate-200">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-sm text-amber-600 uppercase tracking-[0.2em]">لوحة المدير</p>
<p className="text-slate-500 mt-2">قم بتحديث نص سياسة الخصوصية</p>
</div>
</div>
</div>
<form onSubmit={handleSave} className="space-y-6 rounded-[28px] bg-white p-8 shadow-sm border border-slate-200">
<div>
<label className="block text-sm font-medium text-slate-700 mb-3">نص سياسة الخصوصية</label>
<textarea
value={policyText}
onChange={(e) => setPolicyText(e.target.value)}
rows={12}
className="w-full rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4 text-slate-700 outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-100"
/>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<button type="submit" className="rounded-2xl bg-amber-600 px-6 py-3 text-white font-semibold shadow-lg shadow-amber-100 transition hover:bg-amber-700">
حفظ السياسة
</button>
</div>
{saved && (
<div className="rounded-3xl bg-emerald-50 border border-emerald-200 p-4 text-emerald-700">
تمت حفظ سياسة الخصوصية بنجاح
</div>
)}
</form>
</div>
</main>
);
}

View File

@ -2,7 +2,7 @@
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Home, Building, ArrowLeft } from 'lucide-react';
import { Home, Building, Briefcase, ArrowLeft } from 'lucide-react';
export default function ChooseRolePage() {
const containerVariants = {
@ -96,7 +96,7 @@ export default function ChooseRolePage() {
</p>
</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
variants={itemVariants}
whileHover={{ scale: 1.05, y: -5 }}
@ -212,6 +212,64 @@ export default function ChooseRolePage() {
</div>
</Link>
</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>
<motion.p

166
app/blocked/page.js Normal file
View File

@ -0,0 +1,166 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { sendGeneralReport } from '../utils/api';
export default function BlockedPage() {
const router = useRouter();
const [form, setForm] = useState({ subject: '', body: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSent, setIsSent] = useState(false);
const handleLogout = () => {
AuthService.deleteToken();
router.replace('/');
};
const updateField = (field, value) => {
setForm((current) => ({ ...current, [field]: value }));
if (isSent) setIsSent(false);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!form.subject.trim() || !form.body.trim()) {
toast.error('يرجى تعبئة الموضوع والرسالة');
return;
}
setIsSubmitting(true);
try {
await sendGeneralReport(form.subject.trim(), form.body.trim());
setIsSent(true);
setForm({ subject: '', body: '' });
toast.success('تم إرسال طلب الدعم بنجاح');
} catch (error) {
toast.error('حدث خطأ أثناء إرسال طلب الدعم. حاول مرة أخرى');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-b from-red-50 via-white to-amber-50 flex items-center justify-center p-4" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-5xl"
>
<div className="text-center mb-10">
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100"
>
<ShieldAlert className="w-12 h-12 text-red-600" />
</motion.div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">الحساب محظور</h1>
<p className="text-gray-600 text-lg max-w-2xl mx-auto">
تم تقييد وصولك إلى التطبيق. يمكنك تسجيل الخروج أو مراسلة دعم العملاء للمساعدة في حل المشكلة.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<motion.div
initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8 flex flex-col justify-between"
>
<div>
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
<LogOut className="w-7 h-7 text-red-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">تسجيل الخروج</h2>
<p className="text-gray-600 leading-7">
إنهاء الجلسة الحالية وإزالة بيانات الدخول من هذا الجهاز.
</p>
</div>
<button
type="button"
onClick={handleLogout}
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
>
<LogOut className="w-5 h-5" />
تسجيل الخروج
</button>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8"
>
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center">
<MessageSquare className="w-7 h-7 text-amber-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">مراسلة دعم العملاء</h2>
<p className="text-gray-600 mt-1">أرسل تفاصيل المشكلة وسنقوم بمراجعتها.</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">الموضوع</label>
<input
type="text"
value={form.subject}
onChange={(event) => updateField('subject', event.target.value)}
placeholder="اكتب موضوع الرسالة"
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl 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-bold text-gray-700 mb-2">الرسالة</label>
<textarea
value={form.body}
onChange={(event) => updateField('body', event.target.value)}
rows={6}
placeholder="اشرح المشكلة بالتفصيل"
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl 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 bg-amber-500 hover:bg-amber-600 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الإرسال...
</>
) : isSent ? (
<>
<Send className="w-5 h-5" />
تم الإرسال
</>
) : (
<>
<Send className="w-5 h-5" />
إرسال الرسالة
</>
)}
</button>
</form>
</motion.div>
</div>
</motion.div>
</div>
);
}

View 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
View 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>
);
}

View File

@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import { Home, Building, Calendar, Heart, Bell, Settings } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useNotifications } from "@/app/contexts/NotificationsContext";
export default function BottomNav({ isOwner }) {
const { unreadCount } = useNotifications();
const [isMounted, setIsMounted] = useState(false);
const bookingsHref = isOwner ? "/owner/reservations" : "/reservations";
useEffect(() => {
setIsMounted(true);
}, []);
const items = [
{ href: "/", label: "الرئيسية", icon: Home },
{ href: "/properties", label: "عقاراتنا", icon: Building },
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
{ href: "/favorites", label: "المفضلة", icon: Heart },
{ href: "/notifications", label: "الإشعارات", icon: Bell, badge: isMounted ? unreadCount : 0 },
{ href: "/settings", label: "الإعدادات", icon: Settings },
];
return (
<nav className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50">
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-3xl shadow-lg px-2 py-2 flex items-center gap-3">
{items.map((it) => {
const Icon = it.icon;
return (
<Link
key={it.href}
href={it.href}
className="relative group flex flex-col items-center justify-center px-2.5 py-2 text-gray-700 hover:text-amber-600 transition-colors"
>
<div className="relative">
<Icon className="w-6 h-6" />
{it.badge > 0 && (
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">
{it.badge > 9 ? "9+" : it.badge}
</div>
)}
</div>
<div className="absolute -top-10 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs rounded-lg px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg">
{it.label}
</div>
<div className="mt-1 text-xs text-center" aria-hidden>
{it.label}
</div>
</Link>
);
})}
</div>
</nav>
);
}

View File

@ -3,11 +3,12 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
import AuthService from '@/app/services/AuthService';
export default function FloatingSidebar({ isRTL, isAdmin }) {
export default function FloatingSidebar({ isRTL }) {
const { favorites } = useFavorites();
const { unreadCount } = useNotifications();
const [tooltip, setTooltip] = useState(null);
@ -24,15 +25,16 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
setTooltip(null);
};
const side = isRTL ? 'left' : 'right';
if (!AuthService.isAuthenticated()) return null;
const positionStyle = {
[side]: 0,
left: '16px',
top: '50%',
transform: 'translateY(-50%)',
};
const cardVariants = {
initial: { opacity: 0, x: isRTL ? -20 : 20 },
initial: { opacity: 0, x: 20 },
animate: { opacity: 1, x: 0, transition: { duration: 0.4, ease: 'easeOut' } },
};
@ -45,70 +47,21 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
const renderTooltip = (id, label) => {
if (tooltip !== id) return null;
return (
<div
className={`absolute ${isRTL ? 'right-full mr-2' : 'left-full ml-2'} top-1/2 -translate-y-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded-lg whitespace-nowrap z-20 shadow-lg flex items-center`}
>
<span className="relative">
<div className="absolute left-full mr-3 top-1/2 -translate-y-1/2 px-3 py-2 bg-gray-800 text-white text-sm rounded-lg whitespace-nowrap z-20 shadow-lg">
{label}
<span
className={`absolute ${isRTL ? 'right-full -mr-1' : 'left-full -ml-1'} top-1/2 -translate-y-1/2 w-0 h-0 border-t-4 border-b-4 border-transparent ${
isRTL ? 'border-r-4 border-r-gray-800' : 'border-l-4 border-l-gray-800'
}`}
></span>
</span>
</div>
);
};
return (
<motion.div
className="fixed z-50"
className="fixed z-40 pointer-events-none"
style={positionStyle}
variants={cardVariants}
initial="initial"
animate="animate"
>
<div className="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/60 py-3 px-2 flex flex-col gap-3 transition-all duration-300 hover:shadow-xl hover:bg-white/95">
{isAdmin ? (
<>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('addAdmin')}
onMouseLeave={hideTooltip}
>
<Link
href="/admin/add-admin"
className="flex items-center justify-center w-12 h-12 rounded-xl bg-amber-50 border border-amber-200 text-amber-600 hover:bg-amber-100 transition-colors"
>
<UserPlus className="w-6 h-6" />
</Link>
{renderTooltip('addAdmin', 'إضافة أدمن')}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('editPrivacy')}
onMouseLeave={hideTooltip}
>
<Link
href="/admin/privacy"
className="flex items-center justify-center w-12 h-12 rounded-xl bg-slate-50 border border-slate-200 text-slate-700 hover:bg-slate-100 transition-colors"
>
<Shield className="w-6 h-6" />
</Link>
{renderTooltip('editPrivacy', 'تعديل سياسة الخصوصية')}
</motion.div>
</>
) : (
<>
<div className="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/60 py-4 px-3 flex flex-col gap-4 transition-all duration-300 hover:shadow-xl hover:bg-white/95 max-h-[75vh] overflow-y-auto pointer-events-auto">
<motion.div
className="relative group"
variants={buttonVariants}
@ -120,15 +73,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/favorites"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<div className="relative">
<Heart className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<Heart className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{favorites.length > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
>
{favorites.length}
</motion.span>
@ -148,15 +101,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/notifications"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<div className="relative">
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<Bell className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{unreadCount > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
>
{unreadCount}
</motion.span>
@ -176,14 +129,29 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/payments"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<CreditCard className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('payments', 'المدفوعات')}
</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-14 h-14 rounded-xl transition-colors"
>
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('settings', 'الإعدادات')}
</motion.div>
</div>
</motion.div>
);

View File

@ -4,6 +4,7 @@ import { useEffect, useState, useRef } from "react";
import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import AuthService from "../services/AuthService";
import { setFCMToken } from "../utils/api";
const firebaseConfig = {
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
@ -71,21 +72,7 @@ export default function NotificationHandler() {
});
if (fcmToken) {
console.log("[FCM] Token:", fcmToken.substring(0, 20) + "...");
const authToken = AuthService.getToken();
if (authToken) {
const apiBase = "https://45.93.137.91.nip.io/api";
await fetch(`${apiBase}/User/SetFCMToken`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({ token: fcmToken, deviceType: 2 }),
});
console.log("[FCM] Token sent to backend");
}
await setFCMToken(fcmToken, 2);
}
onMessage(messaging, (payload) => {

View File

@ -0,0 +1,100 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
export default function PropertyMapWithMarkers({ properties = [], onPropertyClick }) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markersRef = useRef([]);
const [mapLoaded, setMapLoaded] = useState(false);
useEffect(() => {
if (!mapRef.current || mapInstanceRef.current) return;
const map = L.map(mapRef.current).setView([33.5138, 38.9968], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19,
}).addTo(map);
mapInstanceRef.current = map;
setMapLoaded(true);
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
useEffect(() => {
if (!mapInstanceRef.current || !mapLoaded) return;
markersRef.current.forEach(marker => marker.remove());
markersRef.current = [];
properties.forEach(property => {
if (property.lat && property.lng) {
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
const popupContent = `
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || 'عقار'}</h3>
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ''}</p>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<span style="font-weight: bold; font-size: 18px; color: #d97706;">${formatPrice(property)}</span>
</div>
${property.images && property.images.length > 0 ? `<img src="${property.images[0]}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ''}
<div style="font-size: 12px; color: #888;">
${property.type ? `<p>النوع: ${property.type}</p>` : ''}
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ''}
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ''}
</div>
</div>
`;
marker.bindPopup(popupContent);
marker.on('click', () => {
if (onPropertyClick) {
onPropertyClick(property);
}
});
markersRef.current.push(marker);
}
});
if (markersRef.current.length > 0) {
const group = L.featureGroup(markersRef.current);
mapInstanceRef.current.fitBounds(group.getBounds(), { padding: [50, 100] });
}
}, [properties, mapLoaded]);
const formatPrice = (property) => {
if (property.priceUnit === 'monthly') {
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
} else if (property.priceUnit === 'daily') {
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
} else {
return `${property.price?.toLocaleString() || 0} ل.س`;
}
};
return (
<div className="relative w-full h-[600px] rounded-xl overflow-hidden border-2 border-gray-200">
<div ref={mapRef} className="w-full h-full z-0" />
</div>
);
}

View File

@ -1,356 +0,0 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { useProperties } from '@/app/contexts/PropertyContext';
import { CommissionType, CitiesList } from '@/app/enums';
import { X, MapPin, Home, DollarSign, Percent } from 'lucide-react';
export default function AddPropertyForm({ onClose, onSuccess }) {
const { addProperty } = useProperties();
const [formData, setFormData] = useState({
title: '',
description: '',
city: '',
district: '',
address: '',
latitude: '',
longitude: '',
type: 'apartment',
bedrooms: 1,
bathrooms: 1,
area: 0,
floor: 1,
dailyPrice: 0,
commissionRate: 5,
commissionType: CommissionType.FROM_OWNER,
securityDeposit: 0,
images: [],
features: [],
status: 'available'
});
const [selectedFeatures, setSelectedFeatures] = useState([]);
const featuresList = [
'مسبح',
'حديقة خاصة',
'موقف سيارات',
'مطبخ مجهز',
'تدفئة مركزية',
'بلكونة',
'نظام أمني',
'حديقة كبيرة',
'صالة استقبال',
'غرفة خادمة',
'كراج',
'إطلالة بحرية',
'تكييف مركزي',
'مخزن'
];
const handleSubmit = async (e) => {
e.preventDefault();
const propertyData = {
...formData,
features: selectedFeatures,
priceDisplay: {
daily: formData.dailyPrice,
monthly: formData.dailyPrice * 30,
withCommission: calculateCommissionPrice(formData)
},
location: {
lat: formData.latitude,
lng: formData.longitude,
address: formData.address
}
};
try {
await addProperty(propertyData);
onSuccess?.();
onClose();
} catch (error) {
console.error('Error adding property:', error);
}
};
const calculateCommissionPrice = (data) => {
const { dailyPrice, commissionRate, commissionType } = data;
const commission = (dailyPrice * commissionRate) / 100;
switch(commissionType) {
case CommissionType.FROM_TENANT:
return dailyPrice + commission;
case CommissionType.FROM_OWNER:
return dailyPrice;
case CommissionType.FROM_BOTH:
return dailyPrice + (commission / 2);
default:
return dailyPrice;
}
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
className="bg-white rounded-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto"
>
<div className="sticky top-0 bg-white border-b p-4 flex justify-between items-center">
<h2 className="text-xl font-bold">إضافة عقار جديد</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<MapPin className="w-4 h-4" />
موقع العقار (سيظهر على الخريطة)
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">المدينة</label>
<select
value={formData.city}
onChange={(e) => setFormData({...formData, city: e.target.value})}
className="w-full p-2 border rounded-lg"
required
>
<option value="">اختر المدينة</option>
{CitiesList.map(city => (
<option key={city} value={city}>{city}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">الحي</label>
<input
type="text"
value={formData.district}
onChange={(e) => setFormData({...formData, district: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">العنوان بالتفصيل</label>
<input
type="text"
value={formData.address}
onChange={(e) => setFormData({...formData, address: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">خط العرض (Latitude)</label>
<input
type="number"
step="any"
value={formData.latitude}
onChange={(e) => setFormData({...formData, latitude: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">خط الطول (Longitude)</label>
<input
type="number"
step="any"
value={formData.longitude}
onChange={(e) => setFormData({...formData, longitude: e.target.value})}
className="w-full p-2 border rounded-lg"
required
/>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<DollarSign className="w-4 h-4" />
السعر ونسبة الربح
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
السعر اليومي (ل.س)
</label>
<input
type="number"
value={formData.dailyPrice}
onChange={(e) => setFormData({...formData, dailyPrice: Number(e.target.value)})}
className="w-full p-2 border rounded-lg"
required
min="0"
/>
<p className="text-xs text-gray-500 mt-1">
هذا السعر سيظهر على الخريطة
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">
نسبة ربح المنصة (%)
</label>
<div className="flex items-center gap-2">
<input
type="number"
value={formData.commissionRate}
onChange={(e) => setFormData({...formData, commissionRate: Number(e.target.value)})}
className="w-full p-2 border rounded-lg"
min="0"
max="100"
step="0.1"
required
/>
<Percent className="w-4 h-4 text-gray-400" />
</div>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-2">
مصدر العمولة (بموافقة الأدمن)
</label>
<div className="grid grid-cols-3 gap-3">
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="commissionType"
value={CommissionType.FROM_OWNER}
checked={formData.commissionType === CommissionType.FROM_OWNER}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/>
<span>من المالك</span>
</label>
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="commissionType"
value={CommissionType.FROM_TENANT}
checked={formData.commissionType === CommissionType.FROM_TENANT}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/>
<span>من المستأجر</span>
</label>
<label className="flex items-center gap-2 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="commissionType"
value={CommissionType.FROM_BOTH}
checked={formData.commissionType === CommissionType.FROM_BOTH}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/>
<span>من الاثنين</span>
</label>
</div>
</div>
<div className="col-span-2 bg-white p-3 rounded-lg">
<h4 className="font-medium mb-2">تفاصيل السعر بعد العمولة:</h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-600">السعر الأصلي:</span>
<span className="block font-bold">{formData.dailyPrice} ل.س</span>
</div>
<div>
<span className="text-gray-600">العمولة:</span>
<span className="block font-bold">
{(formData.dailyPrice * formData.commissionRate / 100)} ل.س
</span>
</div>
<div>
<span className="text-gray-600">السعر النهائي:</span>
<span className="block font-bold text-green-600">
{calculateCommissionPrice(formData)} ل.س
</span>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">نوع العقار</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full p-2 border rounded-lg"
>
<option value="apartment">شقة</option>
<option value="house">بيت</option>
<option value="villa">فيلا</option>
<option value="studio">استوديو</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">مبلغ الضمان (ل.س)</label>
<input
type="number"
value={formData.securityDeposit}
onChange={(e) => setFormData({...formData, securityDeposit: Number(e.target.value)})}
className="w-full p-2 border rounded-lg"
min="0"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">المميزات</label>
<div className="grid grid-cols-3 gap-2">
{featuresList.map(feature => (
<label key={feature} className="flex items-center gap-2 p-2 border rounded-lg">
<input
type="checkbox"
checked={selectedFeatures.includes(feature)}
onChange={(e) => {
if (e.target.checked) {
setSelectedFeatures([...selectedFeatures, feature]);
} else {
setSelectedFeatures(selectedFeatures.filter(f => f !== feature));
}
}}
/>
<span className="text-sm">{feature}</span>
</label>
))}
</div>
</div>
<div className="flex gap-3 pt-4 border-t">
<button
type="submit"
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
إضافة العقار
</button>
<button
type="button"
onClick={onClose}
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
إلغاء
</button>
</div>
</form>
</motion.div>
</motion.div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,139 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { Users, Home, Calendar, DollarSign } from 'lucide-react';
import { useEffect, useState } from 'react';
export default function DashboardStats() {
const [stats, setStats] = useState({
totalUsers: 0,
totalProperties: 0,
activeBookings: 0,
totalRevenue: 0,
pendingRequests: 0,
availableProperties: 0
});
useEffect(() => {
setStats({
totalUsers: 156,
totalProperties: 89,
activeBookings: 34,
totalRevenue: 12500000,
pendingRequests: 12,
availableProperties: 45
});
}, []);
const formatNumber = (num) => {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const formatCurrency = (amount) => {
return `${formatNumber(amount)} ل.س`;
};
const cards = [
{
title: 'إجمالي المستخدمين',
value: stats.totalUsers,
icon: Users,
color: 'from-blue-600 to-blue-700',
bgColor: 'bg-blue-100',
iconColor: 'text-blue-600'
},
{
title: 'إجمالي العقارات',
value: stats.totalProperties,
icon: Home,
color: 'from-emerald-600 to-emerald-700',
bgColor: 'bg-emerald-100',
iconColor: 'text-emerald-600'
},
{
title: 'الحجوزات النشطة',
value: stats.activeBookings,
icon: Calendar,
color: 'from-purple-600 to-purple-700',
bgColor: 'bg-purple-100',
iconColor: 'text-purple-600'
},
{
title: 'الإيرادات',
value: formatCurrency(stats.totalRevenue),
icon: DollarSign,
color: 'from-amber-600 to-amber-700',
bgColor: 'bg-amber-100',
iconColor: 'text-amber-600'
}
];
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{cards.map((card, index) => {
const Icon = card.icon;
return (
<motion.div
key={card.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className={`bg-gradient-to-br ${card.color} text-white rounded-xl shadow-lg p-5`}
>
<div className="flex items-center justify-between mb-4">
<div className={`p-3 ${card.bgColor} bg-opacity-20 rounded-lg`}>
<Icon className="w-6 h-6" />
</div>
<div className="text-right">
<div className="text-2xl font-bold">{card.value}</div>
<div className="text-sm opacity-90">{card.title}</div>
</div>
</div>
<div className="text-xs opacity-75">
آخر تحديث: الآن
</div>
</motion.div>
);
})}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="bg-white border rounded-lg p-4"
>
<div className="text-sm text-gray-600 mb-1">طلبات حجز معلقة</div>
<div className="text-2xl font-bold text-yellow-600">{stats.pendingRequests}</div>
<div className="text-xs text-gray-500">بحاجة لموافقة</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="bg-white border rounded-lg p-4"
>
<div className="text-sm text-gray-600 mb-1">عقارات متاحة</div>
<div className="text-2xl font-bold text-green-600">{stats.availableProperties}</div>
<div className="text-xs text-gray-500">جاهزة للإيجار</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="bg-white border rounded-lg p-4"
>
<div className="text-sm text-gray-600 mb-1">نسبة الإشغال</div>
<div className="text-2xl font-bold text-blue-600">
{Math.round((stats.activeBookings / stats.totalProperties) * 100)}%
</div>
<div className="text-xs text-gray-500">من إجمالي العقارات</div>
</motion.div>
</div>
</div>
);
}

View File

@ -1,607 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
DollarSign,
Calendar,
User,
Home,
Download,
Filter,
Search,
TrendingUp,
TrendingDown,
Wallet,
Shield,
FileText,
Printer,
X,
CheckCircle
} from 'lucide-react';
import { formatCurrency } from '@/app/utils/calculations';
import toast, { Toaster } from 'react-hot-toast';
import * as XLSX from 'xlsx';
export default function LedgerBook({ userType = 'admin' }) {
const [transactions, setTransactions] = useState([]);
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [dateRange, setDateRange] = useState({ start: '', end: '' });
const [searchTerm, setSearchTerm] = useState('');
const [summary, setSummary] = useState({
totalRevenue: 0,
pendingPayments: 0,
securityDeposits: 0,
commissionEarned: 0
});
const [isExporting, setIsExporting] = useState(false);
useEffect(() => {
loadTransactions();
}, []);
useEffect(() => {
filterTransactions();
calculateSummary();
}, [transactions, dateRange, searchTerm]);
const loadTransactions = async () => {
const mockTransactions = [
{
id: 'T001',
date: '2024-02-20',
type: 'rent_payment',
description: 'دفعة إيجار - فيلا في دمشق',
amount: 500000,
commission: 25000,
fromUser: 'أحمد محمد',
toUser: 'مالك العقار',
propertyId: 1,
propertyName: 'luxuryVillaDamascus',
status: 'completed',
paymentMethod: 'cash'
},
{
id: 'T002',
date: '2024-02-19',
type: 'security_deposit',
description: 'سلفة ضمان - شقة في حلب',
amount: 250000,
commission: 0,
fromUser: 'سارة أحمد',
toUser: 'مالك العقار',
propertyId: 2,
propertyName: 'modernApartmentAleppo',
status: 'pending_refund',
paymentMethod: 'cash'
},
{
id: 'T003',
date: '2024-02-18',
type: 'commission',
description: 'عمولة منصة - فيلا في درعا',
amount: 30000,
commission: 30000,
fromUser: 'محمد الحلبي',
toUser: 'المنصة',
propertyId: 5,
propertyName: 'villaDaraa',
status: 'completed',
paymentMethod: 'cash'
}
];
setTransactions(mockTransactions);
};
const filterTransactions = () => {
let filtered = [...transactions];
if (dateRange.start && dateRange.end) {
filtered = filtered.filter(t =>
t.date >= dateRange.start && t.date <= dateRange.end
);
}
if (searchTerm) {
filtered = filtered.filter(t =>
t.description.includes(searchTerm) ||
t.fromUser.includes(searchTerm) ||
t.toUser.includes(searchTerm)
);
}
setFilteredTransactions(filtered);
};
const calculateSummary = () => {
const summary = filteredTransactions.reduce((acc, t) => {
if (t.type === 'rent_payment' || t.type === 'commission') {
acc.totalRevenue += t.amount;
}
if (t.type === 'security_deposit' && t.status === 'pending_refund') {
acc.securityDeposits += t.amount;
}
if (t.commission) {
acc.commissionEarned += t.commission;
}
if (t.status === 'pending') {
acc.pendingPayments += t.amount;
}
return acc;
}, {
totalRevenue: 0,
pendingPayments: 0,
securityDeposits: 0,
commissionEarned: 0
});
setSummary(summary);
};
const getTransactionIcon = (type) => {
switch(type) {
case 'rent_payment':
return <Home className="w-4 h-4 text-blue-600" />;
case 'security_deposit':
return <Shield className="w-4 h-4 text-green-600" />;
case 'commission':
return <TrendingUp className="w-4 h-4 text-amber-600" />;
default:
return <DollarSign className="w-4 h-4" />;
}
};
const exportToExcel = async () => {
if (filteredTransactions.length === 0) {
toast.error('لا توجد معاملات للتصدير');
return;
}
setIsExporting(true);
toast.loading('جاري تصدير البيانات...', { id: 'export' });
try {
const exportData = filteredTransactions.map(t => ({
'رقم العملية': t.id,
'التاريخ': t.date,
'نوع العملية': t.type === 'rent_payment' ? 'دفعة إيجار' :
t.type === 'security_deposit' ? 'سلفة ضمان' :
t.type === 'commission' ? 'عمولة' : 'أخرى',
'الوصف': t.description,
'من': t.fromUser,
'إلى': t.toUser,
'المبلغ (ل.س)': t.amount,
'العمولة (ل.س)': t.commission || 0,
'الحالة': t.status === 'completed' ? 'مكتمل' :
t.status === 'pending' ? 'معلق' :
t.status === 'pending_refund' ? 'بإنتظار الاسترداد' : 'مؤكد',
}));
const summaryRow = {
'رقم العملية': '',
'التاريخ': '',
'نوع العملية': '',
'الوصف': '',
'من': '',
'إلى': '',
'المبلغ (ل.س)': summary.totalRevenue,
'العمولة (ل.س)': summary.commissionEarned,
'الحالة': ''
};
exportData.push(summaryRow);
const worksheet = XLSX.utils.json_to_sheet(exportData);
const columnWidths = [
{ wch: 12 }, // رقم العملية
{ wch: 12 }, // التاريخ
{ wch: 12 }, // نوع العملية
{ wch: 30 }, // الوصف
{ wch: 20 }, // من
{ wch: 20 }, // إلى
{ wch: 15 }, // المبلغ
{ wch: 15 }, // العمولة
{ wch: 12 }, // الحالة
];
worksheet['!cols'] = columnWidths;
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let C = range.s.c; C <= range.e.c; ++C) {
const address = XLSX.utils.encode_col(C) + '1';
if (!worksheet[address]) continue;
worksheet[address].s = {
font: { bold: true, sz: 12 },
fill: { fgColor: { rgb: "F59E0B" } },
alignment: { horizontal: "center", vertical: "center" }
};
}
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'دفتر الحسابات');
const fileName = `دفتر_الحسابات_${new Date().toISOString().split('T')[0]}.xlsx`;
XLSX.writeFile(workbook, fileName);
toast.success(`تم تصدير ${filteredTransactions.length} معاملة بنجاح!`, { id: 'export' });
} catch (error) {
console.error('Error exporting to Excel:', error);
toast.error('حدث خطأ أثناء تصدير البيانات', { id: 'export' });
} finally {
setIsExporting(false);
}
};
const printReport = () => {
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html dir="rtl">
<head>
<meta charset="UTF-8">
<title>تقرير دفتر الحسابات</title>
<style>
body {
font-family: 'Cairo', Arial, sans-serif;
padding: 20px;
direction: rtl;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f59e0b;
}
.title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
}
.subtitle {
color: #6b7280;
margin-top: 5px;
}
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.summary-card {
background: #f9fafb;
padding: 15px;
border-radius: 12px;
text-align: center;
}
.summary-value {
font-size: 20px;
font-weight: bold;
color: #f59e0b;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #e5e7eb;
padding: 10px;
text-align: right;
}
th {
background: #f59e0b;
color: white;
font-weight: bold;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
color: #9ca3af;
font-size: 12px;
}
@media print {
.no-print {
display: none;
}
}
</style>
</head>
<body>
<div class="header">
<div class="title">تقرير دفتر الحسابات</div>
<div class="subtitle">الفترة: ${dateRange.start || 'بداية السجلات'} - ${dateRange.end || 'حتى الآن'}</div>
<div class="subtitle">تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')}</div>
</div>
<div class="summary">
<div class="summary-card">
<div>إجمالي الإيرادات</div>
<div class="summary-value">${formatCurrency(summary.totalRevenue)}</div>
</div>
<div class="summary-card">
<div>أرباح المنصة</div>
<div class="summary-value">${formatCurrency(summary.commissionEarned)}</div>
</div>
<div class="summary-card">
<div>سلف الضمان</div>
<div class="summary-value">${formatCurrency(summary.securityDeposits)}</div>
</div>
<div class="summary-card">
<div>المدفوعات المعلقة</div>
<div class="summary-value">${formatCurrency(summary.pendingPayments)}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>التاريخ</th>
<th>الوصف</th>
<th>من</th>
<th>إلى</th>
<th>المبلغ</th>
<th>العمولة</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
${filteredTransactions.map(t => `
<tr>
<td>${t.date}</td>
<td>${t.description}</td>
<td>${t.fromUser}</td>
<td>${t.toUser}</td>
<td>${formatCurrency(t.amount)}</td>
<td>${t.commission ? formatCurrency(t.commission) : '-'}</td>
<td>${t.status === 'completed' ? 'مكتمل' : t.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="footer">
<p>تقرير صادر عن نظام SweetHome لإدارة العقارات</p>
<p>جميع الحقوق محفوظة © ${new Date().getFullYear()}</p>
</div>
<div class="no-print" style="text-align: center; margin-top: 20px;">
<button onclick="window.print()" style="padding: 10px 20px; background: #f59e0b; color: white; border: none; border-radius: 8px; cursor: pointer;">
طباعة التقرير
</button>
</div>
</body>
</html>
`);
printWindow.document.close();
};
return (
<div className="space-y-6">
<Toaster position="top-center" reverseOrder={false} />
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<Wallet className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">إجمالي الإيرادات</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.totalRevenue)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-gradient-to-br from-amber-600 to-amber-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<TrendingUp className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">أرباح المنصة</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.commissionEarned)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-gradient-to-br from-green-600 to-green-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<Shield className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">سلف الضمان</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.securityDeposits)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-red-600 to-red-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<TrendingDown className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">المدفوعات المعلقة</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.pendingPayments)}</div>
</motion.div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -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 pl-12 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<input
type="date"
value={dateRange.start}
onChange={(e) => setDateRange({...dateRange, start: e.target.value})}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<span className="text-gray-500 self-center">إلى</span>
<input
type="date"
value={dateRange.end}
onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={exportToExcel}
disabled={isExporting || filteredTransactions.length === 0}
className="px-5 py-3 bg-green-600 text-white rounded-xl flex items-center gap-2 hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isExporting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
جاري التصدير...
</>
) : (
<>
<Download className="w-5 h-5" />
تصدير Excel
</>
)}
</button>
<button
onClick={printReport}
disabled={filteredTransactions.length === 0}
className="px-5 py-3 bg-blue-600 text-white rounded-xl flex items-center gap-2 hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Printer className="w-5 h-5" />
طباعة
</button>
</div>
</div>
{(dateRange.start || dateRange.end || searchTerm) && (
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<div className="text-sm text-gray-500">
<span className="font-medium">{filteredTransactions.length}</span> معاملة من إجمالي <span className="font-medium">{transactions.length}</span>
</div>
<button
onClick={() => {
setDateRange({ start: '', end: '' });
setSearchTerm('');
}}
className="text-sm text-red-500 hover:text-red-600 flex items-center gap-1"
>
<X className="w-4 h-4" />
إلغاء الفلترة
</button>
</div>
)}
</div>
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التاريخ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الوصف</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">من</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">إلى</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المبلغ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">العمولة</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الحالة</th>
</tr>
</thead>
<tbody className="divide-y">
{filteredTransactions.map((transaction, index) => (
<motion.tr
key={transaction.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50"
>
<td className="px-6 py-4 text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
{transaction.date}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
{getTransactionIcon(transaction.type)}
<span className="text-sm font-medium">{transaction.description}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">{transaction.fromUser}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">{transaction.toUser}</span>
</div>
</td>
<td className="px-6 py-4 text-sm font-bold text-green-600">
{formatCurrency(transaction.amount)}
</td>
<td className="px-6 py-4 text-sm text-amber-600">
{transaction.commission ? formatCurrency(transaction.commission) : '-'}
</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
transaction.status === 'completed' ? 'bg-green-100 text-green-800' :
transaction.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-blue-100 text-blue-800'
}`}>
{transaction.status === 'completed' ? 'مكتمل' :
transaction.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}
</span>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
{filteredTransactions.length === 0 && (
<div className="text-center py-12">
<Wallet className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">لا توجد معاملات في هذه الفترة</p>
</div>
)}
</div>
{userType === 'owner' && (
<div className="bg-blue-50 rounded-xl p-5">
<h3 className="font-bold mb-4 flex items-center gap-2">
<User className="w-5 h-5" />
أرصدة المستأجرين
</h3>
<div className="space-y-3">
<p className="text-gray-500 text-sm">لا توجد أرصدة حالياً</p>
</div>
</div>
)}
</div>
);
}

View File

@ -1,636 +0,0 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Edit,
Trash2,
Eye,
MapPin,
Bed,
Bath,
Square,
DollarSign,
Percent,
MoreVertical,
X,
CheckCircle,
AlertCircle,
Calendar,
User,
Home,
Building,
Clock
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
if (!isOpen) return null;
return (
<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={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="text-center mb-4">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
<AlertCircle className="w-8 h-8 text-red-600" />
</div>
<h3 className="text-xl font-bold text-gray-900">تأكيد الحذف</h3>
<p className="text-sm text-gray-500 mt-2">
هل أنت متأكد من حذف العقار: <span className="font-bold text-gray-700">"{propertyTitle}"</span>؟
</p>
<p className="text-xs text-red-500 mt-1">هذا الإجراء لا يمكن التراجع عنه</p>
</div>
<div className="flex gap-3 pt-3">
<button
onClick={onClose}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إلغاء
</button>
<button
onClick={onConfirm}
className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-colors"
>
نعم، احذف
</button>
</div>
</motion.div>
</motion.div>
);
};
const PropertyViewModal = ({ property, isOpen, onClose }) => {
if (!isOpen || !property) return null;
const formatCurrency = (amount) => {
return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س';
};
return (
<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={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold">{property.title}</h2>
<p className="text-amber-100 text-sm mt-1">{property.location}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Home className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت'}</div>
<div className="text-xs text-gray-500">نوع العقار</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<DollarSign className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">السعر اليومي</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Percent className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.commission}%</div>
<div className="text-xs text-gray-500">نسبة العمولة</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.bookings || 0}</div>
<div className="text-xs text-gray-500">عدد الحجوزات</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-5 h-5 text-amber-500" />
الموقع
</h3>
<p className="text-gray-700">{property.location}</p>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">المواصفات</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<Bed className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.bedrooms}</div>
<div className="text-xs text-gray-500">غرف نوم</div>
</div>
<div className="text-center">
<Bath className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.bathrooms}</div>
<div className="text-xs text-gray-500">حمامات</div>
</div>
<div className="text-center">
<Square className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.area}</div>
<div className="text-xs text-gray-500">م²</div>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2">
<Percent className="w-5 h-5" />
معلومات العمولة
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">نسبة العمولة</label>
<div className="font-bold text-amber-600">{property.commission}%</div>
</div>
<div>
<label className="text-xs text-gray-500">مصدر العمولة</label>
<div className="font-bold text-amber-600">{property.commissionType}</div>
</div>
<div>
<label className="text-xs text-gray-500">قيمة العمولة</label>
<div className="font-bold text-amber-600">
{formatCurrency((property.price * property.commission) / 100)}
</div>
</div>
<div>
<label className="text-xs text-gray-500">حالة العقار</label>
<div className={`inline-block px-2 py-1 rounded-lg text-xs font-medium ${
property.status === 'available'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{property.status === 'available' ? 'متاح' : 'محجوز'}
</div>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
);
};
const PropertyEditModal = ({ property, isOpen, onClose, onSave }) => {
const [formData, setFormData] = useState({ ...property });
const [isSaving, setIsSaving] = useState(false);
const formatCurrency = (amount) => {
return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const handleSave = () => {
setIsSaving(true);
setTimeout(() => {
onSave(formData);
setIsSaving(false);
onClose();
toast.success('تم تحديث العقار بنجاح');
}, 1000);
};
if (!isOpen || !property) return null;
return (
<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={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تعديل العقار</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
<p className="text-amber-100 text-sm mt-1">يمكنك تعديل معلومات العقار</p>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
اسم العقار
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نوع العقار
</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="villa">فيلا</option>
<option value="apartment">شقة</option>
<option value="house">بيت</option>
<option value="studio">استوديو</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
الموقع
</label>
<input
type="text"
value={formData.location}
onChange={(e) => setFormData({...formData, location: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
السعر اليومي (ل.س)
</label>
<input
type="number"
value={formData.price}
onChange={(e) => setFormData({...formData, price: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نسبة العمولة (%)
</label>
<input
type="number"
step="0.1"
value={formData.commission}
onChange={(e) => setFormData({...formData, commission: parseFloat(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مصدر العمولة
</label>
<select
value={formData.commissionType}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="من المالك">من المالك</option>
<option value="من المستأجر">من المستأجر</option>
<option value="من الاثنين">من الاثنين</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الغرف
</label>
<input
type="number"
value={formData.bedrooms}
onChange={(e) => setFormData({...formData, bedrooms: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحمامات
</label>
<input
type="number"
value={formData.bathrooms}
onChange={(e) => setFormData({...formData, bathrooms: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
المساحة (م²)
</label>
<input
type="number"
value={formData.area}
onChange={(e) => setFormData({...formData, area: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
حالة العقار
</label>
<select
value={formData.status}
onChange={(e) => setFormData({...formData, status: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="available">متاح</option>
<option value="booked">محجوز</option>
<option value="maintenance">صيانة</option>
</select>
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
<button
onClick={onClose}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إلغاء
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="flex-1 bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors disabled:opacity-50"
>
{isSaving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</div>
</motion.div>
</motion.div>
);
};
const MoreActionsMenu = ({ property, isOpen, onClose, onViewBookings, onViewReports }) => {
if (!isOpen) return null;
return (
<>
<div className="fixed inset-0 z-40" onClick={onClose} />
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="absolute left-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50"
>
</motion.div>
</>
);
};
export default function PropertiesTable() {
const [properties, setProperties] = useState([
{
id: 1,
title: 'فيلا فاخرة في المزة',
type: 'villa',
location: 'دمشق, المزة',
price: 500000,
commission: 5,
commissionType: 'من المالك',
bedrooms: 5,
bathrooms: 4,
area: 450,
status: 'available',
bookings: 3
},
{
id: 2,
title: 'شقة حديثة في الشهباء',
type: 'apartment',
location: 'حلب, الشهباء',
price: 250000,
commission: 7,
commissionType: 'من المستأجر',
bedrooms: 3,
bathrooms: 2,
area: 180,
status: 'booked',
bookings: 1
}
]);
const [viewModal, setViewModal] = useState({ isOpen: false, property: null });
const [editModal, setEditModal] = useState({ isOpen: false, property: null });
const [deleteModal, setDeleteModal] = useState({ isOpen: false, property: null });
const [moreMenu, setMoreMenu] = useState({ isOpen: false, property: null, anchorEl: null });
const formatCurrency = (amount) => {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س';
};
const getStatusBadge = (status) => {
const styles = {
available: 'bg-green-100 text-green-800',
booked: 'bg-red-100 text-red-800',
maintenance: 'bg-yellow-100 text-yellow-800'
};
const labels = {
available: 'متاح',
booked: 'محجوز',
maintenance: 'صيانة'
};
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${styles[status]}`}>
{labels[status]}
</span>
);
};
const handleView = (property) => {
setViewModal({ isOpen: true, property });
};
const handleEdit = (property) => {
setEditModal({ isOpen: true, property });
};
const handleDelete = (property) => {
setDeleteModal({ isOpen: true, property });
};
const confirmDelete = () => {
if (deleteModal.property) {
setProperties(prev => prev.filter(p => p.id !== deleteModal.property.id));
setDeleteModal({ isOpen: false, property: null });
toast.success('تم حذف العقار بنجاح');
}
};
const handleSaveEdit = (updatedProperty) => {
setProperties(prev => prev.map(p =>
p.id === updatedProperty.id ? updatedProperty : p
));
toast.success('تم تحديث العقار بنجاح');
};
const handleMoreClick = (event, property) => {
event.stopPropagation();
setMoreMenu({ isOpen: true, property, anchorEl: event.currentTarget });
};
const handleViewBookings = (property) => {
toast.success(`جاري عرض حجوزات ${property.title}`);
};
const handleViewReports = (property) => {
toast.success(`جاري عرض تقرير أرباح ${property.title}`);
};
return (
<div className="overflow-x-auto">
<Toaster position="top-center" reverseOrder={false} />
<table className="w-full">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">العقار</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">الموقع</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">السعر/يوم</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">العمولة</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">المصدر</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">التفاصيل</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-900">الحالة</th>
<th className="px-4 py-3 text-center text-sm font-semibold text-gray-900">الإجراءات</th>
</tr>
</thead>
<tbody className="divide-y">
{properties.map((property, index) => (
<motion.tr
key={property.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50"
>
<td className="px-4 py-3">
<div className="font-medium">{property.title}</div>
<div className="text-xs text-gray-500">
{property.type === 'villa' ? 'فيلا' :
property.type === 'apartment' ? 'شقة' :
property.type === 'house' ? 'بيت' : 'استوديو'}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 text-sm">
<MapPin className="w-3 h-3 text-gray-400" />
{property.location}
</div>
</td>
<td className="px-4 py-3 font-bold text-blue-600">
{formatCurrency(property.price)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<Percent className="w-3 h-3 text-amber-500" />
{property.commission}%
</div>
</td>
<td className="px-4 py-3 text-sm">{property.commissionType}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2 text-xs">
<Bed className="w-3 h-3" /> {property.bedrooms}
<Bath className="w-3 h-3 mr-2" /> {property.bathrooms}
<Square className="w-3 h-3 mr-2" /> {property.area}
</div>
</td>
<td className="px-4 py-3">
{getStatusBadge(property.status)}
</td>
<td className="px-4 py-3 relative">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => handleView(property)}
className="p-1 hover:bg-blue-100 rounded text-blue-600 transition-colors"
title="عرض التفاصيل"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(property)}
className="p-1 hover:bg-amber-100 rounded text-amber-600 transition-colors"
title="تعديل العقار"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(property)}
className="p-1 hover:bg-red-100 rounded text-red-600 transition-colors"
title="حذف العقار"
>
<Trash2 className="w-4 h-4" />
</button>
{moreMenu.isOpen && moreMenu.property?.id === property.id && (
<MoreActionsMenu
property={property}
isOpen={moreMenu.isOpen}
onClose={() => setMoreMenu({ isOpen: false, property: null, anchorEl: null })}
onViewBookings={handleViewBookings}
onViewReports={handleViewReports}
/>
)}
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
{properties.length === 0 && (
<div className="text-center py-12">
<Home className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">لا توجد عقارات مضافة بعد</p>
</div>
)}
<PropertyViewModal
property={viewModal.property}
isOpen={viewModal.isOpen}
onClose={() => setViewModal({ isOpen: false, property: null })}
/>
<PropertyEditModal
property={editModal.property}
isOpen={editModal.isOpen}
onClose={() => setEditModal({ isOpen: false, property: null })}
onSave={handleSaveEdit}
/>
<DeleteConfirmationModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal({ isOpen: false, property: null })}
onConfirm={confirmDelete}
propertyTitle={deleteModal.property?.title}
/>
</div>
);
}

View File

@ -1,773 +0,0 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
User,
Mail,
Phone,
Calendar,
Home,
DollarSign,
Search,
Filter,
Eye,
X,
CheckCircle,
XCircle,
ChevronDown,
Users,
Award,
Clock,
TrendingUp,
CalendarDays,
Shield
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
const FilterDialog = ({ isOpen, onClose, filters, onApplyFilters, onResetFilters }) => {
const [localFilters, setLocalFilters] = useState({ ...filters });
const identityTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
];
const bookingRanges = [
{ id: 'all', label: 'الكل' },
{ id: '0-5', label: '0 - 5 حجوزات' },
{ id: '5-10', label: '5 - 10 حجوزات' },
{ id: '10-20', label: '10 - 20 حجوزات' },
{ id: '20+', label: 'أكثر من 20 حجز' }
];
const spendingRanges = [
{ id: 'all', label: 'الكل' },
{ id: '0-500000', label: 'أقل من 500,000 ل.س' },
{ id: '500000-1000000', label: '500,000 - 1,000,000 ل.س' },
{ id: '1000000-5000000', label: '1,000,000 - 5,000,000 ل.س' },
{ id: '5000000+', label: 'أكثر من 5,000,000 ل.س' }
];
const dateRanges = [
{ id: 'all', label: 'الكل' },
{ id: 'today', label: 'اليوم' },
{ id: 'week', label: 'آخر 7 أيام' },
{ id: 'month', label: 'آخر 30 يوم' },
{ id: 'year', label: 'آخر 12 شهر' }
];
const applyFilters = () => {
onApplyFilters(localFilters);
onClose();
toast.success('تم تطبيق الفلاتر بنجاح');
};
const resetFilters = () => {
const resetData = {
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
};
setLocalFilters(resetData);
onResetFilters();
onClose();
toast.success('تم إعادة تعيين الفلاتر');
};
if (!isOpen) return null;
return (
<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={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Filter className="w-5 h-5" />
تصفية متقدمة
</h2>
<p className="text-blue-100 text-sm mt-1">حدد معايير التصفية المطلوبة</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نوع الهوية
</label>
<div className="grid grid-cols-3 gap-2">
{identityTypes.map((type) => (
<button
key={type.id}
onClick={() => setLocalFilters({...localFilters, identityType: type.id})}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
localFilters.identityType === type.id
? 'bg-blue-600 text-white shadow-md'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{type.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحجوزات
</label>
<div className="grid grid-cols-2 gap-2 mb-3">
<input
type="number"
placeholder="من"
value={localFilters.minBookings}
onChange={(e) => setLocalFilters({...localFilters, minBookings: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="إلى"
value={localFilters.maxBookings}
onChange={(e) => setLocalFilters({...localFilters, maxBookings: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{bookingRanges.slice(1).map((range) => (
<button
key={range.id}
onClick={() => {
const [min, max] = range.id.split('-');
setLocalFilters({
...localFilters,
minBookings: min,
maxBookings: max === '5' ? '5' : max === '10' ? '10' : max === '20' ? '20' : '1000'
});
}}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
>
{range.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
إجمالي الإنفاق (ل.س)
</label>
<div className="grid grid-cols-2 gap-2 mb-3">
<input
type="number"
placeholder="من"
value={localFilters.minSpending}
onChange={(e) => setLocalFilters({...localFilters, minSpending: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="إلى"
value={localFilters.maxSpending}
onChange={(e) => setLocalFilters({...localFilters, maxSpending: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{spendingRanges.slice(1).map((range) => (
<button
key={range.id}
onClick={() => {
const [min, max] = range.id.split('-');
setLocalFilters({
...localFilters,
minSpending: min,
maxSpending: max === '500000' ? '500000' : max === '1000000' ? '1000000' : max === '5000000' ? '5000000' : '999999999'
});
}}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
>
{range.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
فترة التسجيل
</label>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{dateRanges.map((range) => (
<button
key={range.id}
onClick={() => setLocalFilters({...localFilters, dateRange: range.id})}
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all ${
localFilters.dateRange === range.id
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{range.label}
</button>
))}
</div>
</div>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={localFilters.activeOnly}
onChange={(e) => setLocalFilters({...localFilters, activeOnly: e.target.checked, inactiveOnly: false})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">مستخدمون لديهم حجوزات نشطة فقط</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={localFilters.inactiveOnly}
onChange={(e) => setLocalFilters({...localFilters, inactiveOnly: e.target.checked, activeOnly: false})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">مستخدمون بدون حجوزات نشطة</span>
</label>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
<button
onClick={resetFilters}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إعادة تعيين
</button>
<button
onClick={applyFilters}
className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-colors"
>
تطبيق الفلاتر
</button>
</div>
</motion.div>
</motion.div>
);
};
const UserDetailsModal = ({ user, isOpen, onClose }) => {
if (!isOpen || !user) return null;
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const userBookings = [
{
id: 'BK001',
property: 'فيلا فاخرة في المزة',
startDate: '2024-03-10',
endDate: '2024-03-15',
amount: 2500000,
status: 'completed'
},
{
id: 'BK002',
property: 'شقة حديثة في الشهباء',
startDate: '2024-02-20',
endDate: '2024-02-25',
amount: 1250000,
status: 'completed'
},
{
id: 'BK003',
property: 'بيت عائلي في بابا عمرو',
startDate: '2024-04-01',
endDate: '2024-04-10',
amount: 3500000,
status: 'confirmed'
}
];
return (
<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={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<User className="w-5 h-5" />
تفاصيل المستخدم
</h2>
<p className="text-blue-100 text-sm mt-1">{user.name}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<User className="w-4 h-4 text-blue-500" />
معلومات شخصية
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">الاسم الكامل:</span>
<span className="font-medium">{user.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">البريد الإلكتروني:</span>
<span className="font-medium">{user.email}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">رقم الهاتف:</span>
<span className="font-medium">{user.phone}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">تاريخ التسجيل:</span>
<span className="font-medium">{user.joinDate}</span>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-500" />
معلومات الهوية
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">نوع الهوية:</span>
<span className="font-medium">
{user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">رقم الهوية:</span>
<span className="font-medium">{user.identityNumber}</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-blue-600">{user.totalBookings}</div>
<div className="text-sm text-gray-600">إجمالي الحجوزات</div>
</div>
<div className="bg-green-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-green-600">{user.activeBookings}</div>
<div className="text-sm text-gray-600">حجوزات نشطة</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-amber-600">{formatCurrency(user.totalSpent)}</div>
<div className="text-sm text-gray-600">إجمالي المنصرف</div>
</div>
</div>
<div>
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Calendar className="w-4 h-4 text-blue-500" />
سجل الحجوزات
</h3>
<div className="space-y-3">
{userBookings.map((booking) => (
<div key={booking.id} className="bg-gray-50 p-4 rounded-xl flex flex-col md:flex-row justify-between items-start md:items-center gap-3">
<div>
<p className="font-medium text-gray-900">{booking.property}</p>
<div className="flex items-center gap-2 text-sm text-gray-500 mt-1">
<CalendarDays className="w-3 h-3" />
{booking.startDate} - {booking.endDate}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="text-lg font-bold text-amber-600">{formatCurrency(booking.amount)}</div>
<div className="text-xs text-gray-500">المبلغ الإجمالي</div>
</div>
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
booking.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{booking.status === 'completed' ? 'مكتمل' : 'مؤكد'}
</span>
</div>
</div>
))}
{userBookings.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-2" />
<p>لا توجد حجوزات سابقة</p>
</div>
)}
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
</div>
</motion.div>
</motion.div>
);
};
export default function UsersList() {
const [users, setUsers] = useState([
{
id: 1,
name: 'أحمد محمد',
email: 'ahmed@example.com',
phone: '0938123456',
identityType: 'syrian',
identityNumber: '123456789',
joinDate: '2024-01-15',
totalBookings: 3,
activeBookings: 1,
totalSpent: 1500000
},
{
id: 2,
name: 'سارة أحمد',
email: 'sara@example.com',
phone: '0945123789',
identityType: 'passport',
identityNumber: 'AB123456',
joinDate: '2024-02-10',
totalBookings: 2,
activeBookings: 0,
totalSpent: 500000
}
]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedUser, setSelectedUser] = useState(null);
const [showFilterDialog, setShowFilterDialog] = useState(false);
const [filters, setFilters] = useState({
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
});
const applyFilters = (newFilters) => {
setFilters(newFilters);
};
const resetFilters = () => {
setFilters({
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
});
setSearchTerm('');
};
const filteredUsers = users.filter(user => {
if (searchTerm && !user.name.includes(searchTerm) && !user.email.includes(searchTerm) && !user.phone.includes(searchTerm)) {
return false;
}
if (filters.identityType !== 'all' && user.identityType !== filters.identityType) {
return false;
}
if (filters.minBookings && user.totalBookings < parseInt(filters.minBookings)) {
return false;
}
if (filters.maxBookings && user.totalBookings > parseInt(filters.maxBookings)) {
return false;
}
if (filters.minSpending && user.totalSpent < parseInt(filters.minSpending)) {
return false;
}
if (filters.maxSpending && user.totalSpent > parseInt(filters.maxSpending)) {
return false;
}
if (filters.activeOnly && user.activeBookings === 0) {
return false;
}
if (filters.inactiveOnly && user.activeBookings > 0) {
return false;
}
if (filters.dateRange !== 'all') {
const joinDate = new Date(user.joinDate);
const today = new Date();
const diffDays = Math.floor((today - joinDate) / (1000 * 60 * 60 * 24));
switch(filters.dateRange) {
case 'today':
if (joinDate.toDateString() !== today.toDateString()) return false;
break;
case 'week':
if (diffDays > 7) return false;
break;
case 'month':
if (diffDays > 30) return false;
break;
case 'year':
if (diffDays > 365) return false;
break;
}
}
return true;
});
const filterStats = {
total: filteredUsers.length,
filtered: filteredUsers.length !== users.length
};
const getActiveFiltersCount = () => {
let count = 0;
if (filters.identityType !== 'all') count++;
if (filters.minBookings || filters.maxBookings) count++;
if (filters.minSpending || filters.maxSpending) count++;
if (filters.dateRange !== 'all') count++;
if (filters.activeOnly || filters.inactiveOnly) count++;
return count;
};
return (
<div className="space-y-4">
<Toaster position="top-center" reverseOrder={false} />
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute right-3 top-1/2 transform -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 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowFilterDialog(true)}
className={`px-5 py-3 rounded-xl font-medium flex items-center gap-2 transition-all ${
getActiveFiltersCount() > 0
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<Filter className="w-5 h-5" />
تصفية متقدمة
{getActiveFiltersCount() > 0 && (
<span className="ml-1 bg-white text-blue-600 rounded-full w-5 h-5 text-xs flex items-center justify-center">
{getActiveFiltersCount()}
</span>
)}
</button>
{filterStats.filtered && (
<button
onClick={resetFilters}
className="px-5 py-3 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2"
>
<X className="w-4 h-4" />
إعادة تعيين
</button>
)}
</div>
</div>
{getActiveFiltersCount() > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-blue-50 rounded-xl">
<span className="text-sm text-blue-800 font-medium">الفلاتر النشطة:</span>
{filters.identityType !== 'all' && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
{filters.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</span>
)}
{(filters.minBookings || filters.maxBookings) && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
الحجوزات: {filters.minBookings || '0'} - {filters.maxBookings || '∞'}
</span>
)}
{(filters.minSpending || filters.maxSpending) && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
الإنفاق: {parseInt(filters.minSpending || 0).toLocaleString()} - {parseInt(filters.maxSpending || '∞').toLocaleString()} ل.س
</span>
)}
{filters.dateRange !== 'all' && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
{filters.dateRange === 'today' ? 'اليوم' :
filters.dateRange === 'week' ? 'آخر 7 أيام' :
filters.dateRange === 'month' ? 'آخر 30 يوم' : 'آخر 12 شهر'}
</span>
)}
{filters.activeOnly && (
<span className="px-2 py-1 bg-green-100 text-green-700 rounded-lg text-xs">
لديهم حجوزات نشطة
</span>
)}
{filters.inactiveOnly && (
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs">
بدون حجوزات نشطة
</span>
)}
</div>
)}
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
عرض <span className="font-bold text-gray-900">{filteredUsers.length}</span> مستخدم
{filterStats.filtered && (
<span className="text-gray-500 mr-1">(من {users.length})</span>
)}
</div>
</div>
<div className="space-y-3">
{filteredUsers.map((user, index) => (
<motion.div
key={user.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-all"
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white text-xl font-bold shadow-lg">
{user.name.charAt(0).toUpperCase()}
</div>
<div>
<h3 className="font-bold text-gray-900 text-lg">{user.name}</h3>
<div className="flex flex-wrap gap-3 mt-1 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Mail className="w-4 h-4" />
{user.email}
</div>
<div className="flex items-center gap-1">
<Phone className="w-4 h-4" />
{user.phone}
</div>
<div className="flex items-center gap-1">
<Shield className="w-4 h-4" />
{user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</div>
</div>
</div>
</div>
<div className="flex gap-6">
<div className="text-center min-w-[80px]">
<div className="text-xl font-bold text-blue-600">{user.totalBookings}</div>
<div className="text-xs text-gray-500">إجمالي الحجوزات</div>
</div>
<div className="text-center min-w-[80px]">
<div className={`text-xl font-bold ${user.activeBookings > 0 ? 'text-green-600' : 'text-gray-400'}`}>
{user.activeBookings}
</div>
<div className="text-xs text-gray-500">حجوزات نشطة</div>
</div>
<div className="text-center min-w-[100px]">
<div className="text-xl font-bold text-amber-600">
{user.totalSpent.toLocaleString()}
</div>
<div className="text-xs text-gray-500">إجمالي المنصرف</div>
</div>
</div>
<button
onClick={() => setSelectedUser(user)}
className="px-4 py-2 bg-blue-600 text-white rounded-xl text-sm font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<Eye className="w-4 h-4" />
عرض التفاصيل
</button>
</div>
</motion.div>
))}
</div>
{filteredUsers.length === 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-16 bg-white rounded-2xl border-2 border-dashed border-gray-300"
>
<Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد نتائج</h3>
<p className="text-gray-500">لا يوجد مستخدمون يطابقون معايير البحث</p>
{(searchTerm || getActiveFiltersCount() > 0) && (
<button
onClick={resetFilters}
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-xl text-sm font-medium hover:bg-blue-700"
>
إعادة تعيين الفلاتر
</button>
)}
</motion.div>
)}
<FilterDialog
isOpen={showFilterDialog}
onClose={() => setShowFilterDialog(false)}
filters={filters}
onApplyFilters={applyFilters}
onResetFilters={resetFilters}
/>
<UserDetailsModal
user={selectedUser}
isOpen={!!selectedUser}
onClose={() => setSelectedUser(null)}
/>
</div>
);
}

View File

@ -67,7 +67,9 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
setActiveTab(tab);
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
setShowLoginDialog(true);
return;
}
handleSearch();
};
const handleSearch = () => {

View 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>
);
}

View File

@ -255,7 +255,7 @@ export default function PropertyRatingList({ propertyId }) {
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<div className="flex justify-between items-center mb-4 pb-3 border-b border-gray-100">
<h3 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h3>
{average !== null && (
{average !== null && average > 0 && (
<div className="flex items-center gap-1 bg-amber-50 px-3 py-1 rounded-full">
<Star className="w-5 h-5 text-amber-500 fill-amber-500" />
<span className="font-bold text-lg">{average.toFixed(1)}</span>

View File

@ -17,7 +17,11 @@ export const useFavorites = () => {
function mapApiFavorite(item) {
const info = item.propertyInformation || {};
let details = {};
if (typeof info.detailsJSON === 'object' && info.detailsJSON) {
details = info.detailsJSON;
} else {
try { details = JSON.parse(info.detailsJSON || '{}'); } catch {}
}
const price = item.monthlyRent || item.dailyRent || 0;
const priceUnit = item.monthlyRent ? 'monthly' : 'daily';

View File

@ -1,38 +1,29 @@
/**
* BookingStatus Enum
* Backend values are strings
* Used in: Reservation workflow
*/
const BookingStatus = Object.freeze({
PENDING: 'pending',
OWNER_APPROVED: 'owner_approved',
ADMIN_APPROVED: 'admin_approved',
ACTIVE: 'active',
COMPLETED: 'completed',
REJECTED: 'rejected',
CANCELLED: 'cancelled',
ownerConfirmed: 'ownerConfirmed',
depositPaid: 'depositPaid',
depositConfirmed: 'depositConfirmed',
completed: 'completed',
cancelled: 'cancelled',
});
// Map status → Arabic label
const BookingStatusLabels = Object.freeze({
[BookingStatus.PENDING]: 'بانتظار الموافقة',
[BookingStatus.OWNER_APPROVED]: وافقة المالك',
[BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة',
[BookingStatus.ACTIVE]: 'إيجار نشط',
[BookingStatus.COMPLETED]: 'منتهي',
[BookingStatus.REJECTED]: رفوض',
[BookingStatus.CANCELLED]: 'ملغي',
[BookingStatus.PENDING]: 'قيد الانتظار',
[BookingStatus.ownerConfirmed]: ؤكد من المالك',
[BookingStatus.depositPaid]: 'تم دفع السلفة',
[BookingStatus.depositConfirmed]: 'تم تأكيد الدفع',
[BookingStatus.completed]: 'منتهي',
[BookingStatus.cancelled]: لغي',
});
// Map status → color class (Tailwind bg)
const BookingStatusColors = Object.freeze({
[BookingStatus.PENDING]: 'yellow',
[BookingStatus.OWNER_APPROVED]: 'blue',
[BookingStatus.ADMIN_APPROVED]: 'green',
[BookingStatus.ACTIVE]: 'purple',
[BookingStatus.COMPLETED]: 'gray',
[BookingStatus.REJECTED]: 'red',
[BookingStatus.CANCELLED]: 'red',
[BookingStatus.ownerConfirmed]: 'blue',
[BookingStatus.depositPaid]: 'orange',
[BookingStatus.depositConfirmed]: 'green',
[BookingStatus.completed]: 'teal',
[BookingStatus.cancelled]: 'red',
});
export { BookingStatus, BookingStatusLabels, BookingStatusColors };

View File

@ -1,33 +1,59 @@
/**
* BuildingType Enum
* Backend values are numeric (0, 1, 2)
* Backend values are numeric
* Used in: PropertyInformation.buildingType
*/
const BuildingType = Object.freeze({
APARTMENT: 0,
VILLA: 1,
HOUSE: 2,
SWEET: 3,
ROOM: 4,
STUDIO: 5,
OFFICE: 6,
FARMS: 7,
SHOP: 8,
WAREHOUSE: 9,
});
// Map numeric value → Arabic label
const BuildingTypeLabels = Object.freeze({
[BuildingType.APARTMENT]: 'شقة',
[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({
[BuildingType.APARTMENT]: 'apartment',
[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({
apartment: BuildingType.APARTMENT,
villa: BuildingType.VILLA,
house: BuildingType.HOUSE,
sweet: BuildingType.SWEET,
suite: 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 };

View 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 };

View File

@ -5,16 +5,22 @@
const Currency = Object.freeze({
SYP: 1,
USD: 2,
EUR: 3,
TRY: 4,
});
const CurrencyLabels = Object.freeze({
[Currency.SYP]: 'ليرة سورية',
[Currency.USD]: 'دولار أمريكي',
[Currency.EUR]: 'يورو',
[Currency.TRY]: 'ليرة تركية',
});
const CurrencySymbols = Object.freeze({
[Currency.SYP]: 'SYP',
[Currency.USD]: 'USD',
[Currency.EUR]: 'EUR',
[Currency.TRY]: 'TRY',
});
export { Currency, CurrencyLabels, CurrencySymbols };

7
app/enums/DeviceType.js Normal file
View File

@ -0,0 +1,7 @@
const DeviceType = Object.freeze({
ANDROID: 'Android',
IOS: 'Ios',
WEB: 'Web',
});
export { DeviceType };

11
app/enums/Language.js Normal file
View 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 };

View File

@ -5,29 +5,26 @@
*/
const PropertyStatus = Object.freeze({
AVAILABLE: 0,
BOOKED: 1,
MAINTENANCE: 2,
NOT_AVAILABLE: 1,
BOOKED: 2,
});
// Map numeric value → Arabic label
const PropertyStatusLabels = Object.freeze({
[PropertyStatus.AVAILABLE]: 'متاح',
[PropertyStatus.NOT_AVAILABLE]: 'غير متاح',
[PropertyStatus.BOOKED]: 'محجوز',
[PropertyStatus.MAINTENANCE]: 'صيانة',
});
// Map numeric value → English key (for UI filters)
const PropertyStatusKeys = Object.freeze({
[PropertyStatus.AVAILABLE]: 'available',
[PropertyStatus.NOT_AVAILABLE]: 'notAvailable',
[PropertyStatus.BOOKED]: 'booked',
[PropertyStatus.MAINTENANCE]: 'maintenance',
});
// Reverse map: English key → numeric value
const PropertyStatusByKey = Object.freeze({
available: PropertyStatus.AVAILABLE,
notAvailable: PropertyStatus.NOT_AVAILABLE,
booked: PropertyStatus.BOOKED,
maintenance: PropertyStatus.MAINTENANCE,
});
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey };

View File

@ -0,0 +1,11 @@
const SalePropertiesStatus = Object.freeze({
PENDING: 0,
CONFIRMED: 1,
});
const SalePropertiesStatusLabels = Object.freeze({
[SalePropertiesStatus.PENDING]: 'قيد الانتظار',
[SalePropertiesStatus.CONFIRMED]: 'مؤكد',
});
export { SalePropertiesStatus, SalePropertiesStatusLabels };

View File

@ -0,0 +1,11 @@
const TransactionType = Object.freeze({
ONLINE: 0,
MANUAL: 1,
});
const TransactionTypeLabels = Object.freeze({
[TransactionType.ONLINE]: 'أونلاين',
[TransactionType.MANUAL]: 'يدوي',
});
export { TransactionType, TransactionTypeLabels };

View File

@ -7,21 +7,18 @@ const UserRole = Object.freeze({
GUEST: 'guest',
CUSTOMER: 'customer',
OWNER: 'owner',
ADMIN: 'admin',
});
const UserRoleLabels = Object.freeze({
[UserRole.GUEST]: 'زائر',
[UserRole.CUSTOMER]: 'مستأجر',
[UserRole.OWNER]: 'مالك عقار',
[UserRole.ADMIN]: 'مدير النظام',
});
const UserRoleColors = Object.freeze({
[UserRole.GUEST]: 'gray',
[UserRole.CUSTOMER]: 'blue',
[UserRole.OWNER]: 'amber',
[UserRole.ADMIN]: 'red',
});
export { UserRole, UserRoleLabels, UserRoleColors };

View File

@ -19,3 +19,8 @@ export { RentType, RentTypeLabels } from './RentType';
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
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
View 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>
);
}

View File

@ -12,14 +12,14 @@ import AuthService from '@/app/services/AuthService';
export default function FavoritesPage() {
const router = useRouter();
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
if (AuthService.isAdmin()) {
router.push('/');
return;
}
setIsAdmin(AuthService.isAdmin());
// Admin check removed
// if (AuthService.isAdmin()) {
// router.push('/');
// return;
// }
// setIsAdmin(AuthService.isAdmin());
}, [router]);
const formatCurrency = (amount) => {

View File

@ -1,30 +1,74 @@
'use client';
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 { 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() {
const router = useRouter();
const [step, setStep] = useState(1);
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [newPassword, setNewPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = async (e) => {
const handleRequestOtp = async (e) => {
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);
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 (
<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 -top-40 -right-40 w-80 h-80 bg-purple-500 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 -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-orange-400 rounded-full opacity-20 blur-3xl animate-pulse delay-1000"></div>
</div>
<motion.div
@ -38,14 +82,14 @@ export default function ForgotPasswordPage() {
animate={{ opacity: 1, x: 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">
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<Link href="/login" className="flex items-center gap-2 text-gray-600 hover:text-amber-600 transition-colors group">
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
<span>العودة لتسجيل الدخول</span>
</Link>
</motion.div>
<div className="bg-white/10 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
<div className="bg-gradient-to-r from-purple-500 to-pink-500 p-8 text-center">
<div className="bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/60 overflow-hidden">
<div className="bg-gradient-to-l from-amber-500 to-amber-600 p-8 text-center">
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
@ -57,38 +101,49 @@ export default function ForgotPasswordPage() {
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-purple-100"
className="text-amber-100"
>
سنرسل لك رابط لإعادة تعيين كلمة المرور
{step === 1 ? 'أدخل بريدك الإلكتروني لاستلام رمز التحقق' : 'أدخل رمز التحقق وكلمة المرور الجديدة'}
</motion.p>
</div>
<div className="p-8">
{!isSubmitted ? (
<form onSubmit={handleSubmit} className="space-y-6">
<AnimatePresence mode="wait">
{step === 1 && (
<motion.form
key="step1"
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 30 }}
onSubmit={handleRequestOtp}
className="space-y-6"
>
<div>
<label className="block text-sm font-medium text-gray-200 mb-2">
<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-purple-500 transition-colors" />
<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="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"
className={inputClass}
placeholder="أدخل بريدك الإلكتروني"
dir="ltr"
required
/>
</div>
</div>
<button
<motion.button
type="submit"
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">
@ -96,31 +151,91 @@ export default function ForgotPasswordPage() {
<span>جاري الإرسال...</span>
</div>
) : (
'إرسال رابط الاستعادة'
'إرسال رمز التحقق'
)}
</button>
</form>
) : (
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="text-center py-6"
</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"
>
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-10 h-10 text-green-500" />
<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">
<Key className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
</div>
<h3 className="text-xl font-bold text-white mb-2">تم الإرسال بنجاح!</h3>
<p className="text-gray-300 mb-6">
تم إرسال رابط استعادة كلمة المرور إلى {email}
</p>
<Link
href="/login"
className="inline-block px-6 py-3 bg-white/10 text-white rounded-xl hover:bg-white/20 transition-colors"
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
className={inputClass}
placeholder="أدخل رمز التحقق"
dir="ltr"
required
/>
</div>
</div>
<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">
<Lock className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500 transition-colors" />
</div>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={inputClass}
placeholder="أدخل كلمة المرور الجديدة"
required
minLength={6}
/>
</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"
>
العودة لتسجيل الدخول
</Link>
</motion.div>
{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>
</motion.div>

View File

@ -126,11 +126,7 @@ export default function LoginPage() {
}
}
const userRole = AuthService.isAdmin()
? "admin"
: AuthService.isOwner()
? "owner"
: "customer";
const userRole = AuthService.isOwner() ? "owner" : "customer";
console.log("[Login] User role:", userRole);
setIsSuccess(true);
@ -139,11 +135,7 @@ export default function LoginPage() {
});
setTimeout(() => {
if (userRole === "admin") {
router.push("/admin");
} else {
router.push("/");
}
}, 1500);
} else if (result.status === 206) {
console.log("[Login] 206 — OTP required");

152
app/my-rates/page.js Normal file
View 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>
);
}

View File

@ -2,13 +2,16 @@
import { useEffect, useState } from 'react';
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 { useNotifications } from '@/app/contexts/NotificationsContext';
import { getUserNotifications } from '@/app/utils/api';
export default function NotificationsPage() {
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);
useEffect(() => {
@ -16,23 +19,48 @@ export default function NotificationsPage() {
router.push('/login');
return;
}
fetchNotifications();
}, [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) => {
// 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 = () => {
// This will be handled by context if needed
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<motion.div
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>
</div>
</motion.div>
</div>
);
}
@ -40,11 +68,23 @@ export default function NotificationsPage() {
if (error) {
return (
<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" />
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
<p className="text-gray-500">{error}</p>
</div>
<p className="text-gray-500 mb-4">{error}</p>
<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>
);
}
@ -52,48 +92,99 @@ export default function NotificationsPage() {
return (
<div className="min-h-screen bg-gray-50 py-8">
<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>
<h1 className="text-3xl font-bold text-gray-900 mb-2">الإشعارات</h1>
<p className="text-gray-600">
{unreadCount > 0 ? `لديك ${unreadCount} إشعار غير مقروء` : 'جميع الإشعارات مقروءة'}
</p>
</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 ? (
<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" />
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد إشعارات</h3>
<p className="text-gray-500">ستظهر هنا الإشعارات المتعلقة بحجوزاتك ومدفوعاتك</p>
</div>
</motion.div>
) : (
<div className="space-y-4">
<AnimatePresence>
{notifications.map((notification, index) => (
<div
key={index}
className="bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md border-gray-200"
<motion.div
key={notification.id || index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ delay: index * 0.05, type: 'spring', stiffness: 100 }}
whileHover={{ scale: 1.01 }}
onClick={() => markAsRead(notification.id)}
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md cursor-pointer ${
!notification.read ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'
}`}
>
<div className="p-5 flex gap-4">
<div className="w-12 h-12 bg-blue-50 rounded-full flex items-center justify-center shrink-0">
<Bell className="w-6 h-6 text-blue-600" />
<div className={`w-12 h-12 rounded-full flex items-center justify-center shrink-0 ${
!notification.read ? 'bg-amber-100' : 'bg-gray-100'
}`}>
{!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">
<div className="flex justify-between items-start">
<div>
<h3 className="font-bold text-gray-900">{notification.title}</h3>
<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-600 text-sm mt-1">{notification.message}</p>
<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 && (
<p className="text-xs text-gray-400 mt-2">{notification.date}</p>
<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>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</div>

256
app/onboarding/page.js Normal file
View 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>
);
}

View 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>
);
}

View File

@ -2,7 +2,7 @@
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import dynamic from 'next/dynamic';
@ -48,10 +48,11 @@ import {
Minus,
Save,
Wind,
Move
Move,
Trees
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import { addRentProperty, getCurrencies, uploadPicture } from '../../../utils/api';
import { addRentProperty, addSaleProperty, getCurrencies, uploadPicture } from '../../../utils/api';
import {
BuildingType,
RentPropertyCondition,
@ -85,18 +86,24 @@ function MapClickHandler({ onMapClick }) {
export default function AddPropertyPage() {
const router = useRouter();
const searchParams = useSearchParams();
const purpose = searchParams.get('purpose') || 'rent';
const [step, setStep] = useState(1);
const totalSteps = 4;
const totalSteps = purpose === 'sale' ? 4 : 4;
const [formData, setFormData] = useState({
propertyType: 'apartment', // apartment, villa, suite, room
propertyType: 'apartment',
furnished: false,
bedrooms: 1,
bathrooms: 1,
livingRooms: 1,
floorNumber: '',
salons: '',
balconies: '',
space: '',
services: {
[PropertyService.ELECTRICITY]: false,
@ -120,6 +127,8 @@ export default function AddPropertyPage() {
dailyPrice: '',
monthlyPrice: '',
deposit: '',
allowedPaymentPeriod: '',
city: '',
district: '',
@ -129,11 +138,37 @@ export default function AddPropertyPage() {
description: '',
images: []
images: [],
nearbySchool: '',
nearbyHospital: '',
nearbyRestaurant: '',
nearbyUniversity: '',
nearbyPark: '',
nearbyMall: '',
roomAreaType: 'Private room',
roomPeopleAllowed: '',
roomFurniture: '',
roomEntrance: 'Shared entrance',
roomBathroom: 'Shared',
roomKitchen: 'Not available',
roomRestrictedAreas: false,
roomResidents: '',
roomGender: 'Family',
roomLanguage: '',
roomChildren: false,
roomPets: false,
roomDedicatedTo: 'Everyone',
roomVisitors: true,
roomQuietTimes: false,
roomQuietTimesDetails: '',
});
const [imagePreviews, setImagePreviews] = useState([]);
const [uploadedImagePaths, setUploadedImagePaths] = useState([]);
const [customTerms, setCustomTerms] = useState([]);
const [customTermInput, setCustomTermInput] = useState('');
const [selectedLocation, setSelectedLocation] = useState(null);
const [mapCenter, setMapCenter] = useState([33.5138, 36.2765]);
@ -152,8 +187,13 @@ export default function AddPropertyPage() {
const propertyTypes = [
{ id: 'apartment', label: 'شقة', icon: Building },
{ id: 'villa', label: 'فيلا', icon: Home },
{ id: 'suite', label: 'سويت', icon: Sofa },
{ id: 'room', label: 'غرفة ضمن شقة', icon: DoorOpen }
{ id: 'sweet', label: 'سويت', icon: Sofa },
{ 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 = [
@ -422,6 +462,18 @@ const handleMapClick = async (coords) => {
});
};
const addCustomTerm = () => {
const val = customTermInput.trim();
if (!val) return;
if (customTerms.includes(val)) return;
setCustomTerms(prev => [...prev, val]);
setCustomTermInput('');
};
const removeCustomTerm = (term) => {
setCustomTerms(prev => prev.filter(t => t !== term));
};
const incrementBedrooms = () => {
setFormData({
...formData,
@ -493,6 +545,9 @@ const handleMapClick = async (coords) => {
break;
case 3:
if (purpose === 'sale') {
if (!formData.salePrice) newErrors.salePrice = 'سعر البيع مطلوب';
} else {
if (formData.offerType === 'daily' && !formData.dailyPrice) {
newErrors.dailyPrice = 'السعر اليومي مطلوب';
}
@ -503,6 +558,7 @@ const handleMapClick = async (coords) => {
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
}
}
break;
case 4:
@ -535,83 +591,112 @@ const handleMapClick = async (coords) => {
if (!validateStep()) return;
setIsLoading(true);
console.log('[AddProperty] Building RentPropertyDto payload...');
// 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)
.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)
.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 allTerms = [...new Set([...selectedTerms, ...customTerms])];
const isRent = purpose === 'rent';
const details = {
description: formData.description || '',
services: selectedServices,
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: 'in general' }), {}),
terms: selectedTerms,
displayType: formData.offerType === 'both' ? 'Both' : formData.offerType === 'daily' ? 'Daily' : 'Monthly',
propertyCondition: formData.furnished ? 'Furnished' : 'Unfurnished',
photos: imagePreviews.map((_, i) => `photo_${i}.jpg`),
room: {
areaType: formData.propertyType === 'room' ? 'Shared room' : 'Private room',
peopleAllowed: String(formData.bedrooms),
entranceType: formData.propertyType === 'room' ? 'Shared entrance' : 'Private entrance',
bathroomType: formData.bathrooms > 1 ? 'Private' : 'Shared',
kitchenType: 'Not available',
hasRestrictedOwnerAreas: false,
languageDialect: '',
hasChildren: false,
hasPets: false,
dedicatedTo: 'Everyone',
visitorsAllowed: true,
quietTimesEnabled: false,
quietTimes: '',
}
});
serviceDetails: selectedServices.reduce((acc, s) => ({ ...acc, [s]: formData.serviceDetails[s] || 'in general' }), {}),
propertyCondition: formData.furnished ? 'WithFurniture' : 'WithoutFurniture',
floorNumber: parseInt(formData.floorNumber) || 0,
numberOfSalons: parseInt(formData.salons) || 0,
numberOfBalconies: parseInt(formData.balconies) || 0,
nearbyDistances: {
school: formData.nearbySchool || '',
hospital: formData.nearbyHospital || '',
restaurant: formData.nearbyRestaurant || '',
university: formData.nearbyUniversity || '',
park: formData.nearbyPark || '',
mall: formData.nearbyMall || '',
},
};
const payload = {
propertyInformation: {
if (isRent) {
details.terms = allTerms.reduce((acc, k) => ({ ...acc, [k]: true }), {});
details.displayType = formData.offerType === 'both' ? 'Both' : formData.offerType === 'daily' ? 'Daily' : 'Monthly';
}
if (isRent && formData.propertyType === 'room') {
details.room = {
areaType: formData.roomAreaType || 'Private room',
peopleAllowed: formData.roomPeopleAllowed || String(formData.bedrooms),
furnitureDetails: formData.roomFurniture || '',
entranceType: formData.roomEntrance || 'Shared entrance',
bathroomType: formData.roomBathroom || 'Shared',
kitchenType: formData.roomKitchen || 'Not available',
hasRestrictedOwnerAreas: formData.roomRestrictedAreas || false,
homeResidentsCount: formData.roomResidents || '',
currentPopulationGender: formData.roomGender || 'Family',
languageDialect: formData.roomLanguage || '',
hasChildren: formData.roomChildren || false,
hasPets: formData.roomPets || false,
dedicatedTo: formData.roomDedicatedTo || 'Everyone',
visitorsAllowed: formData.roomVisitors ?? true,
quietTimesEnabled: formData.roomQuietTimes ?? false,
quietTimes: formData.roomQuietTimesDetails || '',
};
}
const detailsJSON = JSON.stringify(details);
const propInfo = {
cordsX: formData.lat ? String(formData.lat) : '',
cordsY: formData.lng ? String(formData.lng) : '',
images: uploadedImagePaths,
address: `${formData.city} - ${formData.district} - ${formData.address}`.trim(),
description: formData.description || '',
numberOfBathRooms: formData.bathrooms || 0,
numberOfRooms: (formData.bedrooms || 0) + (formData.livingRooms || 0),
numberOfRooms: formData.bedrooms || 0,
numberOfBedRooms: formData.bedrooms || 0,
space: parseFloat(formData.space) || 0,
detailsJSON,
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
status: 0,
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
images: uploadedImagePaths,
},
};
try {
if (purpose === 'sale') {
const payload = {
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,
rating: 1,
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,
allowedPaymentPeriod: formData.allowedPaymentPeriod || '',
};
console.log('[AddProperty] Payload:', JSON.stringify(payload, null, 2));
try {
console.log('[AddProperty] Rent payload:', JSON.stringify(payload, null, 2));
const res = await addRentProperty(payload);
console.log('[AddProperty] API response:', res);
toast.success('تم إضافة العقار بنجاح!');
console.log('[AddProperty] Rent API response:', res);
toast.success('تم إضافة عقار للإيجار بنجاح!');
}
setTimeout(() => {
router.push('/owner/properties');
}, 1500);
@ -663,7 +748,7 @@ const handleMapClick = async (coords) => {
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>معلومات العقار</span>
<span>التفاصيل والخدمات</span>
<span>السعر</span>
<span>{purpose === 'sale' ? 'سعر البيع' : 'السعر'}</span>
<span>الموقع والصور</span>
</div>
</div>
@ -768,7 +853,23 @@ const handleMapClick = async (coords) => {
<p className="text-gray-600">أدخل التفاصيل والخدمات المتاحة</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
المساحة (م²)
</label>
<div className="relative">
<Square className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="number"
value={formData.space}
onChange={(e) => setFormData({...formData, space: e.target.value})}
className="w-full pr-10 pl-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
placeholder="مثال: 120"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الغرف <span className="text-red-500">*</span>
@ -854,6 +955,39 @@ const handleMapClick = async (coords) => {
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">رقم الطابق</label>
<input
type="number"
value={formData.floorNumber}
onChange={(e) => setFormData({...formData, floorNumber: 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"
placeholder="مثال: 3"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">عدد الصالونات</label>
<input
type="number"
value={formData.salons}
onChange={(e) => setFormData({...formData, salons: 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"
placeholder="مثال: 1"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">عدد البلكونات</label>
<input
type="number"
value={formData.balconies}
onChange={(e) => setFormData({...formData, balconies: 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"
placeholder="مثال: 1"
/>
</div>
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 mb-4">الخدمات المتوفرة <span className="text-red-500">*</span></h3>
<div className="space-y-3">
@ -891,6 +1025,7 @@ const handleMapClick = async (coords) => {
</div>
</div>
{purpose === 'rent' && (
<div>
<h3 className="text-lg font-bold text-gray-900 mb-4">شروط استخدام العقار</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
@ -923,11 +1058,100 @@ const handleMapClick = async (coords) => {
);
})}
</div>
{/* Custom Terms */}
<div className="mt-4 p-4 border border-dashed border-gray-300 rounded-xl">
<p className="text-sm font-medium text-gray-700 mb-2">إضافة شرط مخصص</p>
<div className="flex gap-2">
<input
type="text"
value={customTermInput}
onChange={(e) => setCustomTermInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustomTerm(); } }}
placeholder="اكتب شرطاً مخصصاً..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
/>
<button
type="button"
onClick={addCustomTerm}
disabled={!customTermInput.trim()}
className="px-4 py-2 bg-amber-500 text-white rounded-lg text-sm font-medium hover:bg-amber-600 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
>
إضافة
</button>
</div>
{customTerms.length > 0 && (
<div className="flex flex-wrap gap-2 mt-3">
{customTerms.map((term) => (
<span
key={term}
className="inline-flex items-center gap-1 px-3 py-1 bg-amber-100 text-amber-800 rounded-full text-sm"
>
{term}
<button
type="button"
onClick={() => removeCustomTerm(term)}
className="hover:text-red-600 transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</span>
))}
</div>
)}
</div>
</div>
)}
</motion.div>
)}
{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 && (
{step === 3 && purpose === 'rent' && (
<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">
@ -998,6 +1222,25 @@ const handleMapClick = async (coords) => {
</div>
</div>
{/* Payment period picker */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مدة السداد
</label>
<select
value={formData.allowedPaymentPeriod}
onChange={(e) => setFormData({...formData, allowedPaymentPeriod: 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"
>
<option value="">اختر المدة</option>
<option value="1.00:00:00">يومي</option>
<option value="7.00:00:00">أسبوعي</option>
<option value="30.00:00:00">شهري</option>
<option value="90.00:00:00">ربع سنوي</option>
<option value="365.00:00:00">سنوي</option>
</select>
</div>
<AnimatePresence mode="wait">
{(formData.offerType === 'daily' || formData.offerType === 'both') && (
<motion.div

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,15 +26,13 @@ import {
MessageCircle
} from 'lucide-react';
import HeroSearch from './components/home/HeroSearch';
import PropertyMap from './components/home/PropertyMap';
import PropertyMapWithMarkers from './components/PropertyMapWithMarkers';
import Link from 'next/link';
import Image from 'next/image';
import { getRentProperties, getSaleProperties } from './utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
import AuthService from './services/AuthService';
// Map API property data to the format the UI expects
// API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... }
function mapApiProperty(item, index) {
const info = item.propertyInformation || {};
@ -56,7 +54,6 @@ function mapApiProperty(item, index) {
if (info.numberOfBedRooms) features.push(`${info.numberOfBedRooms} غرف نوم`);
if (info.numberOfBathRooms) features.push(`${info.numberOfBathRooms} حمامات`);
// Extract images from API and build full URLs
const apiBase = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api') : '';
const rawImages = Array.isArray(info.images) ? info.images : [];
const images = rawImages.length > 0
@ -105,10 +102,6 @@ function mapApiProperty(item, index) {
};
}
// extractCity is now imported from @/app/enums
// API-only — no fallback data
export default function HomePage() {
const mapSectionRef = useRef(null);
const [searchFilters, setSearchFilters] = useState(null);
@ -121,9 +114,10 @@ export default function HomePage() {
const pathname = usePathname();
const [allProperties, setAllProperties] = useState([]);
const [rentProperties, setRentProperties] = useState([]);
const [saleProperties, setSaleProperties] = useState([]);
const [loading, setLoading] = useState(true);
// Re-read user from JWT on every route change
useEffect(() => {
const authUser = AuthService.getUser();
if (authUser) {
@ -137,7 +131,6 @@ export default function HomePage() {
}
}, [pathname]);
// Fetch properties from API on mount
useEffect(() => {
async function fetchProperties() {
@ -150,15 +143,12 @@ export default function HomePage() {
const rentList = Array.isArray(rentData) ? rentData : [];
const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [
...rentList.map((p, i) => mapApiProperty(p, i)),
...saleList.map((p, i) => mapApiProperty(p, rentList.length + i)),
];
const mappedRent = rentList.map((p, i) => mapApiProperty(p, i));
const mappedSale = saleList.map((p, i) => mapApiProperty(p, rentList.length + i));
if (mapped.length > 0) {
setAllProperties(mapped);
}
// If API returns empty, keep fallback
setRentProperties(mappedRent);
setSaleProperties(mappedSale);
setAllProperties([...mappedRent, ...mappedSale]);
} catch (err) {
console.error('[Home] Failed to fetch properties:', err);
} finally {
@ -170,14 +160,10 @@ export default function HomePage() {
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShowUserMenu(false);
if (searchFilters) {
applyFilters(searchFilters);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
}, [rentProperties, saleProperties, searchFilters]);
const logout = () => {
AuthService.deleteToken();
@ -188,17 +174,16 @@ export default function HomePage() {
const applyFilters = (filters) => {
setSearchFilters(filters);
const filtered = allProperties.filter(property => {
if (filters.mode === 'rent' && property.listingType !== 'rent') {
return false;
}
if (filters.mode === 'sell' && property.listingType !== 'sale') {
return false;
}
if (filters.mode === 'buy' && property.listingType !== 'sale') {
return false;
let propertiesToFilter = [];
if (filters.mode === 'rent') {
propertiesToFilter = rentProperties;
} else if (filters.mode === 'buy' || filters.mode === 'sell') {
propertiesToFilter = saleProperties;
} else {
propertiesToFilter = allProperties;
}
const filtered = propertiesToFilter.filter(property => {
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
return false;
}
@ -467,9 +452,16 @@ export default function HomePage() {
transition={{ delay: 0.3, type: "spring" }}
>
{filteredProperties.length > 0 ? (
<PropertyMap
properties={filteredProperties}
userIdentity={searchFilters?.identityType || 'syrian'}
<PropertyMapWithMarkers
properties={filteredProperties.map(p => ({
...p,
lat: p.location.lat,
lng: p.location.lng,
address: p.location.address
}))}
onPropertyClick={(property) => {
console.log('Property clicked:', property);
}}
/>
) : (
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">

View File

@ -1,93 +1,380 @@
// 'use client';
// import { useEffect, useState, useCallback } from 'react';
// import { useRouter } from 'next/navigation';
// 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 { getUserReservations, payDeposit } from '@/app/utils/api';
// const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
// const STATUS_CONFIG = {
// pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300', depositPaid: false },
// ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300', depositPaid: false },
// depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300', depositPaid: true },
// depositConfirmed: { label: 'تم تأكيد الدفع', color: 'bg-green-100 text-green-800 border-green-300', depositPaid: true },
// 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 },
// };
// export default function PaymentsPage() {
// const router = useRouter();
// const [reservations, setReservations] = useState([]);
// const [loading, setLoading] = useState(true);
// const [payingId, setPayingId] = useState(null);
// useEffect(() => {
// // Admin check removed
// // if (AuthService.isAdmin()) {
// // router.push('/');
// // return;
// // }
// loadReservations();
// }, [router]);
// const loadReservations = useCallback(async () => {
// try {
// const data = await getUserReservations();
// setReservations(Array.isArray(data) ? data : []);
// } catch (err) {
// console.error(err);
// toast.error('فشل تحميل المدفوعات');
// } finally {
// setLoading(false);
// }
// }, []);
// 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 (
// <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 canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
// 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-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" />
// <h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
// <p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
// </motion.div>
// ) : (
// <div className="space-y-4">
// {reservations.map((r, i) => {
// const statusKey = STATUS_MAP[r.status] || 'pending';
// const cfg = STATUS_CONFIG[statusKey];
// const amount = r.depositAmount || r.totalPrice || 0;
// 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>
// );
// }
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
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 Link from 'next/link';
import { payDeposit } from '@/app/utils/api';
const mockPayments = [
{
id: 1,
property: 'فيلا فاخرة في المزة',
amount: 2500000,
date: '2024-03-10',
status: 'completed',
invoiceId: 'INV-001'
},
{
id: 2,
property: 'شقة حديثة في الشهباء',
amount: 750000,
date: '2024-03-05',
status: 'completed',
invoiceId: 'INV-002'
}
];
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
const STATUS_CONFIG = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300', depositPaid: false },
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300', depositPaid: false },
depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300', depositPaid: true },
depositConfirmed: { label: 'تم تأكيد الدفع', color: 'bg-green-100 text-green-800 border-green-300', depositPaid: true },
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 },
};
export default function PaymentsPage() {
const router = useRouter();
const [payments, setPayments] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
const [payingId, setPayingId] = useState(null);
const getAuthToken = () => {
if (typeof window === 'undefined') return '';
return (
AuthService?.getToken?.() ||
AuthService?.getAccessToken?.() ||
localStorage.getItem('token') ||
localStorage.getItem('accessToken') ||
localStorage.getItem('authToken') ||
''
);
};
const loadReservations = useCallback(async () => {
try {
const token = getAuthToken();
const res = await fetch('http://45.93.137.91/api/Customer/GetMyTransaction', {
method: 'GET',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (!res.ok) {
throw new Error('فشل تحميل المدفوعات');
}
const json = await res.json();
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
const mapped = items.map((item) => {
const deposit = item?.diposit || item?.deposit || {};
const reservation = deposit?.reservation || {};
const transaction = deposit?.transaction || {};
const currency = item?.currency || {};
return {
id: reservation.id ?? deposit.reservationId ?? deposit?.reservation?.id ?? item?.reservationId ?? deposit?.id,
reservationId: reservation.id ?? deposit.reservationId ?? item?.reservationId ?? deposit?.id,
status: reservation.status ?? 0,
startDate: reservation.startDate,
endDate: reservation.endDate,
totalPrice: reservation.totalPrice ?? transaction.amount ?? 0,
depositAmount: transaction.amount ?? reservation.totalPrice ?? 0,
currencySign: currency.sign || 'ل.س',
currencyName: currency.name || '',
currencyRate: currency.rate,
_deposit: deposit,
};
});
setReservations(mapped);
} catch (err) {
console.error(err);
toast.error('فشل تحميل المدفوعات');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (AuthService.isAdmin()) {
router.push('/');
return;
// Admin check removed
// if (AuthService.isAdmin()) {
// router.push('/');
// return;
// }
loadReservations();
}, [router, loadReservations]);
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);
}
setTimeout(() => {
setPayments(mockPayments);
setIsLoading(false);
}, 500);
}, [router]);
};
const formatCurrency = (amount) => amount?.toLocaleString() + ' ل.س';
const formatCurrency = (v, sign = 'ل.س') => `${sign} ${Number(v ?? 0).toLocaleString()}`;
if (isLoading) {
const formatDate = (date) => {
if (!date) return '';
const d = new Date(date);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleDateString('en-GB');
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<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 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">
<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>
const canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
{payments.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
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-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" />
<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>
</div>
</motion.div>
) : (
<div className="space-y-4">
{payments.map((payment) => (
<div key={payment.id} 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>
<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">
مكتمل
{reservations.map((r, i) => {
const statusKey = STATUS_MAP[r.status] || 'pending';
const cfg = STATUS_CONFIG[statusKey];
const amount = r.depositAmount || r.totalPrice || 0;
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 gap-4">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
<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 className="flex items-start justify-between gap-4">
<div className="flex items-center 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>
<div className="text-sm text-gray-500">Reservation</div>
<div className="font-bold text-gray-900">#{r.reservationId || r.id}</div>
</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-400 mb-1">{r.currencyName}</div>
<div className="text-2xl md:text-3xl font-bold text-gray-900">
{formatCurrency(amount, r.currencySign)}
</div>
))}
</div>
</div>
<div className="border-t border-gray-100 pt-4">
<div className="flex items-center justify-between text-sm text-gray-700">
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
الفترة
</span>
<span className="font-medium">
{formatDate(r.startDate)} - {formatDate(r.endDate)}
</span>
</div>
<div className="flex items-center justify-between text-sm text-gray-700 mt-3">
<span className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-400" />
Reservation
</span>
<span className="font-medium">#{r.reservationId || r.id}</span>
</div>
</div>
{canPay(r.status) && (
<div className="pt-2 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>
)}
</div>
</motion.div>
);
})}
</div>
)}
</div>

217
app/privacy/page.js Normal file
View File

@ -0,0 +1,217 @@
// '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>
// );
// }
'use client';
import { useEffect, useState } from 'react';
import { Languages, Loader2, Shield } from 'lucide-react';
const API_BASE = 'http://45.93.137.91/api';
const ENDPOINTS = {
ar: '/Configuration/GetARPrivacyPolicy',
en: '/Configuration/GetENPrivacyPolicy',
};
export default function PrivacyPage() {
const [language, setLanguage] = useState('ar');
const [policyText, setPolicyText] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
const controller = new AbortController();
const loadPolicy = async () => {
try {
setLoading(true);
setError('');
const response = await fetch(`${API_BASE}${ENDPOINTS[language]}`, {
method: 'GET',
cache: 'no-store',
signal: controller.signal,
headers: {
Accept: 'text/plain',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const text = await response.text();
console.log('API RESPONSE:', text);
setPolicyText(text.trim());
} catch (err) {
if (err.name !== 'AbortError') {
setPolicyText('');
setError(
language === 'ar'
? 'تعذر تحميل النص من الخادم.'
: 'Failed to load text from the server.'
);
}
} finally {
setLoading(false);
}
};
loadPolicy();
return () => controller.abort();
}, [language]);
return (
<div
dir={language === 'ar' ? 'rtl' : 'ltr'}
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
>
<div className="mx-auto max-w-3xl px-4">
<div className="mb-8 flex items-center justify-between">
<div className="flex items-center gap-2 text-gray-900">
<Shield className="h-6 w-6 text-amber-600" />
<span className="text-lg font-bold">
{language === 'ar' ? 'سياسة الخصوصية' : 'Privacy Policy'}
</span>
</div>
<button
type="button"
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
>
<Languages className="h-4 w-4" />
{language === 'ar' ? 'English' : 'العربية'}
</button>
</div>
{loading && (
<div className="mb-6 flex items-center gap-3 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800">
<Loader2 className="h-5 w-5 animate-spin" />
<span>{language === 'ar' ? 'جاري التحميل...' : 'Loading...'}</span>
</div>
)}
{error && (
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-red-700">
{error}
</div>
)}
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="whitespace-pre-wrap leading-8 text-gray-800">
{policyText}
</div>
</div>
</div>
</div>
);
}

View File

@ -43,10 +43,10 @@ function mapApiProperty(item, index) {
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 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 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 features = [];
@ -80,7 +80,7 @@ function mapApiProperty(item, index) {
features,
images,
status,
rating: item.rating || 4.5,
rating: item.rating || 0,
isNew: false,
_raw: item,
};
@ -299,10 +299,12 @@ const PropertyCard = ({ property, viewMode = 'grid', onLoginRequired }) => {
<span>{property.area}م²</span>
</div>
</div>
{property.rating > 0 && (
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-gray-400 text-gray-400" />
<span className="text-sm font-medium text-gray-700">{property.rating || 4.5}</span>
<Star className="w-4 h-4 fill-amber-500 text-amber-500" />
<span className="text-sm font-medium text-gray-700">{property.rating.toFixed(1)}</span>
</div>
)}
</div>
<Link
@ -489,6 +491,7 @@ const FilterBar = ({ filters, onFilterChange }) => {
};
export default function PropertiesPage() {
const [purposeTab, setPurposeTab] = useState('rent');
const [viewMode, setViewMode] = useState('grid');
const [sortBy, setSortBy] = useState('newest');
const [properties, setProperties] = useState([]);
@ -517,8 +520,8 @@ export default function PropertiesPage() {
const saleList = Array.isArray(saleData) ? saleData : [];
const mapped = [
...rentList.map((p, i) => mapApiProperty(p, i)),
...saleList.map((p, i) => mapApiProperty(p, rentList.length + i)),
...rentList.map((p, i) => ({ ...mapApiProperty(p, i), purpose: 'rent' })),
...saleList.map((p, i) => ({ ...mapApiProperty(p, rentList.length + i), purpose: 'sale' })),
];
if (mapped.length > 0) {
@ -535,6 +538,7 @@ export default function PropertiesPage() {
}, []);
const filteredProperties = properties
.filter(p => p.purpose === purposeTab)
.filter(property => {
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
return false;
@ -578,8 +582,22 @@ export default function PropertiesPage() {
animate={{ opacity: 1, y: 0 }}
className="text-center mb-8"
>
<h1 className="text-4xl font-bold text-gray-900 mb-2">عقارات للإيجار</h1>
<h1 className="text-4xl font-bold text-gray-900 mb-2">{purposeTab === 'rent' ? 'عقارات للإيجار' : 'عقارات للبيع'}</h1>
<p className="text-gray-500">أفضل العقارات في سوريا</p>
{/* Purpose Toggle */}
<div className="flex justify-center mt-4">
<div className="inline-flex bg-gray-100 rounded-xl p-1">
<button onClick={() => setPurposeTab('rent')}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${purposeTab === 'rent' ? 'bg-white text-amber-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>
للإيجار
</button>
<button onClick={() => setPurposeTab('sale')}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-all ${purposeTab === 'sale' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>
للبيع
</button>
</div>
</div>
{loading && (
<div className="mt-4">
<div className="inline-block w-6 h-6 border-2 border-gray-200 border-t-gray-800 rounded-full animate-spin"></div>

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,13 @@ async function fetchPropertyForMeta(id) {
function mapProperty(item) {
const info = item.propertyInformation || item.PropertyInformation || {};
let details = {};
if (typeof info.detailsJSON === 'object' && info.detailsJSON) {
details = info.detailsJSON;
} else if (typeof info.DetailsJSON === 'object' && info.DetailsJSON) {
details = info.DetailsJSON;
} else {
try { details = JSON.parse(info.detailsJSON || info.DetailsJSON || '{}'); } catch {}
}
const price = item.monthlyRent || item.MonthlyRent || item.dailyRent || item.DailyRent || 0;
const priceUnit = item.monthlyRent || item.MonthlyRent ? 'monthly' : 'daily';

1180
app/register/agent/page.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
// // app/register/owner/page.js
// 'use client';
// import { useState, useRef, useMemo } from 'react';
@ -8,7 +10,7 @@
// import {
// User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
// Camera, X, CheckCircle, XCircle, ArrowLeft, Building,
// Loader2, Shield, KeyRound
// Loader2, Shield, KeyRound, FileText
// } from 'lucide-react';
// import toast, { Toaster } from 'react-hot-toast';
// import { addOwner, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
@ -17,7 +19,7 @@
// export default function OwnerRegisterPage() {
// const router = useRouter();
// const [step, setStep] = useState(1); // 1=form, 2=id images
// const [step, setStep] = useState(1);
// const [showOtpModal, setShowOtpModal] = useState(false);
// const [showPassword, setShowPassword] = useState(false);
// const [showConfirmPassword, setShowConfirmPassword] = useState(false);
@ -37,13 +39,16 @@
// agreeTerms: false
// });
// const [idImages, setIdImages] = useState({ front: null, back: null });
// const [idImagePreviews, setIdImagePreviews] = useState({ front: '', back: '' });
// 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 isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
// const handleImageUpload = (side, file) => {
// if (!file) return;
@ -61,7 +66,6 @@
// };
// reader.readAsDataURL(file);
// setIdImages(prev => ({ ...prev, [side]: file }));
// console.log('[OwnerRegister] Image uploaded:', side);
// toast.success('تم رفع الصورة بنجاح', { style: { background: '#dcfce7', color: '#166534' } });
// };
@ -72,21 +76,14 @@
// 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.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب';
// else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
// if (formData.phone && !validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح';
// 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;
// };
@ -101,7 +98,6 @@
// const handleNextStep = () => {
// if (validateStep1()) {
// console.log('[OwnerRegister] Step 1 valid, moving to step 2');
// setStep(2);
// window.scrollTo({ top: 0, behavior: 'smooth' });
// } else {
@ -109,10 +105,8 @@
// }
// };
// // ─── Main signup handler ───
// const handleSubmit = async (e) => {
// e.preventDefault();
// if (!validateStep2()) {
// toast.error('يرجى إكمال جميع الصور المطلوبة');
// return;
@ -123,8 +117,6 @@
// }
// setIsLoading(true);
// console.log('[OwnerRegister] Submitting owner registration...');
// const payload = {
// firstName: formData.firstName,
// lastName: formData.lastName,
@ -138,29 +130,19 @@
// };
// try {
// const res = await addOwner(payload, idImages.front, idImages.back);
// console.log('[OwnerRegister] addOwner response:', res);
// // تمرير صورة الرخصة فقط إذا كان نوع المالك وكالة عقارية
// const licenseImage = isCompany ? idImages.license : null;
// const res = await addOwner(payload, idImages.front, idImages.back, licenseImage);
// if (res.status === 200 || res.ok) {
// const tempToken = res.data;
// if (tempToken) {
// AuthService.addToken(tempToken);
// console.log('[OwnerRegister] Temp token stored for OTP');
// }
// if (tempToken) AuthService.addToken(tempToken);
// toast.success(res.message || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
// const apiMessage = res.message || res.data?.message;
// toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
// // Auto-login to trigger OTP
// console.log('[OwnerRegister] Auto-login to send OTP...');
// const loginRes = await loginWithEmail(formData.email, formData.password);
// console.log('[OwnerRegister] login response:', loginRes);
// if (loginRes.status === 206) {
// const otpToken = loginRes.data;
// if (otpToken) AuthService.addToken(otpToken);
// const loginMsg = loginRes.message || loginRes.data?.message;
// toast(loginMsg || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
// toast(loginRes.message || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
// setShowOtpModal(true);
// } else if (loginRes.status === 200) {
// const loginToken = loginRes.data;
@ -169,45 +151,32 @@
// router.push('/');
// }
// } else {
// const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب';
// console.error('[OwnerRegister] Registration failed:', errMsg);
// toast.error(errMsg);
// toast.error(res.message || res.data?.message || 'فشل في إنشاء الحساب');
// }
// } catch (err) {
// console.error('[OwnerRegister] Error:', err);
// toast.error(err.message || 'حدث خطأ أثناء التسجيل');
// } finally {
// setIsLoading(false);
// }
// };
// // ─── OTP verification handler ───
// const handleVerifyOTP = async () => {
// if (!otpCode || otpCode.length < 4) {
// toast.error('يرجى إدخال رمز التحقق');
// return;
// }
// setIsLoading(true);
// console.log('[OwnerRegister] Verifying OTP:', otpCode);
// try {
// const res = await verifyEmail(otpCode);
// console.log('[OwnerRegister] VerifyEmail response:', res);
// if (res.status === 200) {
// AuthService.deleteToken();
// console.log('[OwnerRegister] Temp token removed after verification');
// toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!', { duration: 3000 });
// toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!');
// setShowOtpModal(false);
// setTimeout(() => router.push('/login'), 1500);
// } else {
// const errMsg = res.message || res.data?.message || 'رمز التحقق غير صحيح';
// console.error('[OwnerRegister] Verification failed:', errMsg);
// toast.error(errMsg);
// toast.error(res.message || res.data?.message || 'رمز التحقق غير صحيح');
// }
// } catch (err) {
// console.error('[OwnerRegister] Verify error:', err);
// toast.error(err.message || 'حدث خطأ أثناء التحقق');
// } finally {
// setIsLoading(false);
@ -216,12 +185,10 @@
// const handleResendOTP = async () => {
// setIsLoading(true);
// console.log('[OwnerRegister] Resending email OTP...');
// try {
// await sendEmailOTP();
// toast.success('تم إرسال رمز تحقق جديد');
// } catch (err) {
// console.error('[OwnerRegister] Resend OTP error:', err);
// toast.error('فشل في إرسال الرمز');
// } finally {
// setIsLoading(false);
@ -238,68 +205,36 @@
// animate: { transition: { staggerChildren: 0.1 } }
// };
// const backgroundElements = useMemo(() => {
// const circles = [
// { style: { top: '20%', right: '20%', width: '256px', height: '256px' }, className: 'bg-amber-500/5' },
// { style: { bottom: '20%', left: '20%', width: '320px', height: '320px' }, className: 'bg-blue-500/5' },
// { style: { top: '50%', left: '50%', width: '384px', height: '384px', transform: 'translate(-50%, -50%)' }, className: 'bg-purple-500/5' },
// ];
// const dots = [
// { left: '5%', top: '10%', size: '120px' },
// { left: '15%', top: '70%', size: '80px' },
// { left: '25%', top: '30%', size: '150px' },
// { left: '35%', top: '85%', size: '100px' },
// { left: '45%', top: '15%', size: '90px' },
// { left: '55%', top: '60%', size: '130px' },
// { left: '65%', top: '40%', size: '70px' },
// { left: '75%', top: '80%', size: '110px' },
// { left: '85%', top: '20%', size: '140px' },
// { left: '95%', top: '50%', size: '85px' },
// ];
// 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}
// />
// <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-amber-500/10"
// style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }}
// />
// <div key={`dot-${i}`} className="absolute rounded-full bg-amber-500/10" style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }} />
// ))}
// </>
// );
// }, []);
// 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">
// {[...Array(20)].map((_, i) => (
// <motion.div key={i} className="absolute rounded-full bg-amber-500/10"
// style={{ left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, width: Math.random() * 200 + 50, height: Math.random() * 200 + 50 }}
// animate={{ x: [0, Math.random() * 100 - 50, 0], y: [0, Math.random() * 100 - 50, 0] }}
// transition={{ duration: Math.random() * 15 + 15, repeat: Infinity, ease: "linear" }} />
// ))}
// </div> */}
// <div className="absolute inset-0 overflow-hidden">
// {backgroundElements}
// </div>
// <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">
// {/* Progress */}
// <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">
@ -325,12 +260,8 @@
// className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
// <Building className="w-10 h-10 text-white" />
// </motion.div>
// <h1 className="text-3xl font-bold text-white mb-2">
// {step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'}
// </h1>
// <p className="text-amber-100">
// {step === 1 ? 'أدخل معلوماتك الأساسية' : 'يرجى رفع صور الهوية للتحقق'}
// </p>
// <h1 className="text-3xl font-bold text-white mb-2">{step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'}</h1>
// <p className="text-amber-100">{step === 1 ? 'أدخل معلوماتك الأساسية' : 'يرجى رفع صور الهوية للتحقق'}</p>
// </motion.div>
// </div>
@ -338,8 +269,6 @@
// <motion.form variants={staggerContainer} initial="initial" animate="animate"
// onSubmit={step === 1 ? (e) => { e.preventDefault(); handleNextStep(); } : handleSubmit}
// className="space-y-6">
// {/* ─── STEP 1: Form ─── */}
// {step === 1 && (
// <>
// <motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
@ -438,8 +367,11 @@
// <motion.div variants={fadeInUp}>
// <label className="block text-sm font-medium text-gray-300 mb-2">نوع المالك <span className="text-red-500">*</span></label>
// <select value={formData.ownerType}
// onChange={(e) => setFormData({...formData, ownerType: e.target.value})}
// <select value={formData.ownerType.toString()}
// onChange={(e) => {
// const selectedType = parseInt(e.target.value, 10);
// setFormData((prev) => ({ ...prev, ownerType: selectedType }));
// }}
// className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white appearance-none cursor-pointer">
// {Object.entries(OwnerTypeLabels).map(([value, label]) => (
// <option key={value} value={value} className="bg-gray-900 text-white">{label}</option>
@ -447,6 +379,35 @@
// </select>
// </motion.div>
// {/* حقل رفع صورة الرخصة - يظهر فقط للوكالات العقارية في الخطوة الأولى */}
// <AnimatePresence>
// {isCompany && (
// <motion.div
// key="license-upload-step1"
// initial={{ opacity: 0, y: -10 }}
// animate={{ opacity: 1, y: 0 }}
// exit={{ opacity: 0, y: -10 }}
// transition={{ duration: 0.2 }}
// >
// <label className="block text-sm font-medium text-gray-300 mb-2">صورة الرخصة العقارية <span className="text-gray-400">(اختياري)</span></label>
// <div className="mb-2 text-xs text-gray-400">رفع صورة الرخصة يساعد في تسريع عملية التحقق.</div>
// <div onClick={() => fileInputLicenseRef.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' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
// <input ref={fileInputLicenseRef} 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>
// </motion.div>
// )}
// </AnimatePresence>
// <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">
@ -488,7 +449,6 @@
// </>
// )}
// {/* ─── STEP 2: ID Images ─── */}
// {step === 2 && (
// <>
// <motion.div variants={fadeInUp}>
@ -538,7 +498,6 @@
// </>
// )}
// {/* ─── Buttons ─── */}
// <motion.div variants={fadeInUp} className="flex gap-3 pt-4">
// {step === 1 ? (
// <>
@ -563,7 +522,6 @@
// </motion.div>
// </motion.div>
// {/* ─── OTP Modal ─── */}
// <AnimatePresence>
// {showOtpModal && (
// <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
@ -578,7 +536,6 @@
// <p className="text-gray-400 text-sm mt-1">تم إرسال رمز التحقق إلى</p>
// <p className="text-amber-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">
@ -591,14 +548,12 @@
// placeholder="------" />
// </div>
// </div>
// <div className="flex gap-3">
// <button onClick={handleVerifyOTP} disabled={isLoading || !otpCode}
// className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl font-medium hover:from-amber-600 hover:to-amber-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-amber-400 hover:text-amber-300 text-sm mt-3 disabled:opacity-50">
// إعادة إرسال الرمز
@ -612,9 +567,7 @@
// }
// app/register/owner/page.js
'use client';
@ -626,7 +579,7 @@ import Image from 'next/image';
import {
User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
Camera, X, CheckCircle, XCircle, ArrowLeft, Building,
Loader2, Shield, KeyRound, Briefcase, FileText, MapPin
Loader2, Shield, KeyRound, FileText
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import { addOwner, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
@ -652,8 +605,6 @@ export default function OwnerRegisterPage() {
password: '',
confirmPassword: '',
ownerType: OwnerType.PERSON,
licenseNumber: '',
companyAddress: '',
agreeTerms: false
});
@ -666,7 +617,7 @@ export default function OwnerRegisterPage() {
const fileInputBackRef = useRef(null);
const fileInputLicenseRef = useRef(null);
const isCompany = formData.ownerType === OwnerType.REAL_ESTATE_AGENCY;
const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
const handleImageUpload = (side, file) => {
if (!file) return;
@ -684,7 +635,6 @@ export default function OwnerRegisterPage() {
};
reader.readAsDataURL(file);
setIdImages(prev => ({ ...prev, [side]: file }));
console.log('[OwnerRegister] Image uploaded:', side);
toast.success('تم رفع الصورة بنجاح', { style: { background: '#dcfce7', color: '#166534' } });
};
@ -695,25 +645,14 @@ export default function OwnerRegisterPage() {
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.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب';
else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
if (formData.phone && !validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح';
if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة';
else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
if (isCompany) {
if (!formData.licenseNumber) newErrors.licenseNumber = 'رقم الرخصة/السجل التجاري مطلوب';
if (!formData.companyAddress) newErrors.companyAddress = 'عنوان المكتب مطلوب';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@ -722,14 +661,12 @@ export default function OwnerRegisterPage() {
const newErrors = {};
if (!idImages.front) newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
if (!idImages.back) newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
if (isCompany && !idImages.license) newErrors.license = 'صورة الرخصة/السجل التجاري مطلوبة';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNextStep = () => {
if (validateStep1()) {
console.log('[OwnerRegister] Step 1 valid, moving to step 2');
setStep(2);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
@ -739,7 +676,6 @@ export default function OwnerRegisterPage() {
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateStep2()) {
toast.error('يرجى إكمال جميع الصور المطلوبة');
return;
@ -750,49 +686,31 @@ export default function OwnerRegisterPage() {
}
setIsLoading(true);
console.log('[OwnerRegister] Submitting owner registration...');
const payload = {
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
phoneNumber: formData.phone || '',
whatsAppNumber: formData.whatsapp,
phone: formData.phone2,
whatsAppNumber: formData.whatsapp,
nationalNumber: formData.nationalNumber,
password: formData.password,
ownerType: formData.ownerType,
type: formData.ownerType,
};
if (isCompany) {
payload.licenseNumber = formData.licenseNumber;
payload.companyAddress = formData.companyAddress;
}
try {
const res = await addOwner(payload, idImages.front, idImages.back, isCompany ? idImages.license : null);
console.log('[OwnerRegister] addOwner response:', res);
// تمرير صورة الرخصة فقط إذا كان نوع المالك وكالة عقارية
const licenseImage = isCompany ? idImages.license : null;
const res = await addOwner(payload, idImages.front, idImages.back, licenseImage);
if (res.status === 200 || res.ok) {
const tempToken = res.data;
if (tempToken) {
AuthService.addToken(tempToken);
console.log('[OwnerRegister] Temp token stored for OTP');
}
if (tempToken) AuthService.addToken(tempToken);
toast.success(res.message || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
const apiMessage = res.message || res.data?.message;
toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
// Auto-login to trigger OTP
console.log('[OwnerRegister] Auto-login to send OTP...');
const loginRes = await loginWithEmail(formData.email, formData.password);
console.log('[OwnerRegister] login response:', loginRes);
if (loginRes.status === 206) {
const otpToken = loginRes.data;
if (otpToken) AuthService.addToken(otpToken);
const loginMsg = loginRes.message || loginRes.data?.message;
toast(loginMsg || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
toast(loginRes.message || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
setShowOtpModal(true);
} else if (loginRes.status === 200) {
const loginToken = loginRes.data;
@ -801,45 +719,32 @@ export default function OwnerRegisterPage() {
router.push('/');
}
} else {
const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب';
console.error('[OwnerRegister] Registration failed:', errMsg);
toast.error(errMsg);
toast.error(res.message || res.data?.message || 'فشل في إنشاء الحساب');
}
} catch (err) {
console.error('[OwnerRegister] Error:', err);
toast.error(err.message || 'حدث خطأ أثناء التسجيل');
} finally {
setIsLoading(false);
}
};
// OTP verification handler
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error('يرجى إدخال رمز التحقق');
return;
}
setIsLoading(true);
console.log('[OwnerRegister] Verifying OTP:', otpCode);
try {
const res = await verifyEmail(otpCode);
console.log('[OwnerRegister] VerifyEmail response:', res);
if (res.status === 200) {
AuthService.deleteToken();
console.log('[OwnerRegister] Temp token removed after verification');
toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!', { duration: 3000 });
toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!');
setShowOtpModal(false);
setTimeout(() => router.push('/login'), 1500);
} else {
const errMsg = res.message || res.data?.message || 'رمز التحقق غير صحيح';
console.error('[OwnerRegister] Verification failed:', errMsg);
toast.error(errMsg);
toast.error(res.message || res.data?.message || 'رمز التحقق غير صحيح');
}
} catch (err) {
console.error('[OwnerRegister] Verify error:', err);
toast.error(err.message || 'حدث خطأ أثناء التحقق');
} finally {
setIsLoading(false);
@ -848,12 +753,10 @@ export default function OwnerRegisterPage() {
const handleResendOTP = async () => {
setIsLoading(true);
console.log('[OwnerRegister] Resending email OTP...');
try {
await sendEmailOTP();
toast.success('تم إرسال رمز تحقق جديد');
} catch (err) {
console.error('[OwnerRegister] Resend OTP error:', err);
toast.error('فشل في إرسال الرمز');
} finally {
setIsLoading(false);
@ -876,35 +779,18 @@ export default function OwnerRegisterPage() {
{ style: { bottom: '20%', left: '20%', width: '320px', height: '320px' }, className: 'bg-blue-500/5' },
{ style: { top: '50%', left: '50%', width: '384px', height: '384px', transform: 'translate(-50%, -50%)' }, className: 'bg-purple-500/5' },
];
const dots = [
{ left: '5%', top: '10%', size: '120px' },
{ left: '15%', top: '70%', size: '80px' },
{ left: '25%', top: '30%', size: '150px' },
{ left: '35%', top: '85%', size: '100px' },
{ left: '45%', top: '15%', size: '90px' },
{ left: '55%', top: '60%', size: '130px' },
{ left: '65%', top: '40%', size: '70px' },
{ left: '75%', top: '80%', size: '110px' },
{ left: '85%', top: '20%', size: '140px' },
{ left: '95%', top: '50%', size: '85px' },
];
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}
/>
<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-amber-500/10"
style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }}
/>
<div key={`dot-${i}`} className="absolute rounded-full bg-amber-500/10" style={{ left: dot.left, top: dot.top, width: dot.size, height: dot.size }} />
))}
</>
);
@ -913,14 +799,10 @@ export default function OwnerRegisterPage() {
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>
<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">
{/* Progress */}
<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">
@ -946,12 +828,8 @@ export default function OwnerRegisterPage() {
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
<Building className="w-10 h-10 text-white" />
</motion.div>
<h1 className="text-3xl font-bold text-white mb-2">
{step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'}
</h1>
<p className="text-amber-100">
{step === 1 ? 'أدخل معلوماتك الأساسية' : 'يرجى رفع صور الهوية للتحقق'}
</p>
<h1 className="text-3xl font-bold text-white mb-2">{step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'}</h1>
<p className="text-amber-100">{step === 1 ? 'أدخل معلوماتك الأساسية' : 'يرجى رفع صور الهوية للتحقق'}</p>
</motion.div>
</div>
@ -959,7 +837,6 @@ export default function OwnerRegisterPage() {
<motion.form variants={staggerContainer} initial="initial" animate="animate"
onSubmit={step === 1 ? (e) => { e.preventDefault(); handleNextStep(); } : handleSubmit}
className="space-y-6">
{step === 1 && (
<>
<motion.div variants={fadeInUp} className="grid grid-cols-2 gap-3">
@ -1058,8 +935,11 @@ export default function OwnerRegisterPage() {
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">نوع المالك <span className="text-red-500">*</span></label>
<select value={formData.ownerType}
onChange={(e) => setFormData({...formData, ownerType: e.target.value})}
<select value={formData.ownerType.toString()}
onChange={(e) => {
const selectedType = parseInt(e.target.value, 10);
setFormData((prev) => ({ ...prev, ownerType: selectedType }));
}}
className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white appearance-none cursor-pointer">
{Object.entries(OwnerTypeLabels).map(([value, label]) => (
<option key={value} value={value} className="bg-gray-900 text-white">{label}</option>
@ -1067,41 +947,31 @@ export default function OwnerRegisterPage() {
</select>
</motion.div>
{/* حقل رفع صورة الرخصة - يظهر فقط للوكالات العقارية في الخطوة الأولى */}
<AnimatePresence>
{isCompany && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="space-y-4 overflow-hidden"
key="license-upload-step1"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<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">
<FileText className={`w-5 h-5 ${errors.licenseNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
<label className="block text-sm font-medium text-gray-300 mb-2">صورة الرخصة العقارية <span className="text-gray-400">(اختياري)</span></label>
<div className="mb-2 text-xs text-gray-400">رفع صورة الرخصة يساعد في تسريع عملية التحقق.</div>
<div onClick={() => fileInputLicenseRef.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' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
<input ref={fileInputLicenseRef} 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>
<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-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.licenseNumber ? 'border-red-500' : 'border-gray-700'}`}
placeholder="أدخل رقم الرخصة أو السجل التجاري" />
) : (<><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.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 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.companyAddress ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type="text" value={formData.companyAddress}
onChange={(e) => { setFormData({...formData, companyAddress: e.target.value}); setErrors({...errors, companyAddress: null}); }}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.companyAddress ? 'border-red-500' : 'border-gray-700'}`}
placeholder="أدخل عنوان المكتب" />
</div>
{errors.companyAddress && <p className="text-red-500 text-sm mt-1">{errors.companyAddress}</p>}
</motion.div>
</motion.div>
)}
</AnimatePresence>
@ -1185,35 +1055,6 @@ export default function OwnerRegisterPage() {
{errors.back && <p className="text-red-500 text-sm mt-1">{errors.back}</p>}
</motion.div>
<AnimatePresence>
{isCompany && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<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={() => fileInputLicenseRef.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-amber-500 hover:bg-white/5'}`}>
<input ref={fileInputLicenseRef} 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>
</motion.div>
)}
</AnimatePresence>
<motion.div variants={fadeInUp} className="flex items-center gap-2">
<input type="checkbox" id="terms" checked={formData.agreeTerms}
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
@ -1225,7 +1066,6 @@ export default function OwnerRegisterPage() {
</>
)}
{/* Buttons */}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? (
<>
@ -1250,7 +1090,6 @@ export default function OwnerRegisterPage() {
</motion.div>
</motion.div>
{/* OTP Modal */}
<AnimatePresence>
{showOtpModal && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
@ -1265,7 +1104,6 @@ export default function OwnerRegisterPage() {
<p className="text-gray-400 text-sm mt-1">تم إرسال رمز التحقق إلى</p>
<p className="text-amber-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">
@ -1278,14 +1116,12 @@ export default function OwnerRegisterPage() {
placeholder="------" />
</div>
</div>
<div className="flex gap-3">
<button onClick={handleVerifyOTP} disabled={isLoading || !otpCode}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 rounded-xl font-medium hover:from-amber-600 hover:to-amber-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-amber-400 hover:text-amber-300 text-sm mt-3 disabled:opacity-50">
إعادة إرسال الرمز

File diff suppressed because it is too large Load Diff

371
app/reports/page.js Normal file
View 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>
);
}

View File

@ -1,3 +1,398 @@
// 'use client';
// import { useState, useEffect, useCallback } from 'react';
// import { motion } from 'framer-motion';
// import { useRouter } from 'next/navigation';
// import {
// Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
// MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star,
// } from 'lucide-react';
// import toast, { Toaster } from 'react-hot-toast';
// import AuthService from '../services/AuthService';
// import { getRentProperties, getUserReservations, payDeposit } from '../utils/api';
// import { addPropertyRating } from '../utils/ratings';
// const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
// const STATUS_MAP = ['pending','ownerConfirmed','depositPaid','depositConfirmed','completed','cancelled'];
// const STATUS_UI = {
// pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
// ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
// depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
// depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
// completed: { label: 'منتهي', color: 'bg-green-100 text-green-800', icon: CheckCircle },
// cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
// };
// function statusLabel(code) { return STATUS_UI[STATUS_MAP[code]]?.label ?? String(code); }
// function statusColor(code) { return STATUS_UI[STATUS_MAP[code]]?.color ?? 'bg-gray-100 text-gray-700'; }
// function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
// function StatusBadge({ code }) {
// const Icon = statusIcon(code);
// return (
// <span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${statusColor(code)}`}>
// <Icon className="w-3 h-3" /> {statusLabel(code)}
// </span>
// );
// }
// const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? '';
// const propImages = (p, r) => {
// if (p?.images && Array.isArray(p.images)) return p.images;
// if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images;
// return [];
// };
// const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
// const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
// function parseTimeSpan(str) {
// if (!str) return 0;
// const clean = str.replace(/-/g, '');
// const dotIdx = clean.indexOf('.');
// let days = 0, timePart = clean;
// if (dotIdx !== -1) {
// days = parseInt(clean.substring(0, dotIdx), 10) || 0;
// timePart = clean.substring(dotIdx + 1);
// }
// const parts = timePart.split(':');
// if (parts.length < 2) return days * 86400000;
// const hh = parseInt(parts[0], 10) || 0;
// const mm = parseInt(parts[1], 10) || 0;
// const ss = parts.length > 2 ? (parseInt(parts[2], 10) || 0) : 0;
// return ((days * 86400) + (hh * 3600) + (mm * 60) + ss) * 1000;
// }
// function formatWindowDuration(str) {
// if (!str) return '';
// const clean = str.replace(/-/g, '');
// const dotIdx = clean.indexOf('.');
// let totalHours = 0, timePart = clean;
// if (dotIdx !== -1) {
// const days = parseInt(clean.substring(0, dotIdx), 10) || 0;
// totalHours += days * 24;
// timePart = clean.substring(dotIdx + 1);
// }
// const parts = timePart.split(':');
// if (parts.length >= 2) {
// totalHours += parseInt(parts[0], 10) || 0;
// }
// if (totalHours > 0) return `${String(totalHours).padStart(2, '0')}:00:00`;
// return timePart.substring(0, 8);
// }
// function CountdownTimer({ deadline }) {
// const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
// useEffect(() => {
// if (!deadline) return;
// const tick = () => setRemaining(Math.max(0, deadline - Date.now()));
// tick();
// const id = setInterval(tick, 1000);
// return () => clearInterval(id);
// }, [deadline]);
// if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
// const h = Math.floor(remaining / 3600000);
// const m = Math.floor((remaining % 3600000) / 60000);
// const s = Math.floor((remaining % 60000) / 1000);
// const pad = (n) => String(n).padStart(2, '0');
// return <span className="text-amber-600 text-sm font-mono font-bold" dir="ltr">{pad(h)}:{pad(m)}:{pad(s)}</span>;
// }
// function ReservationCard({ r, onViewDetails, onPay, payingId }) {
// const p = r._prop;
// const imgs = propImages(p, r);
// const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
// const addr = propAddr(p, r);
// const beds = propBeds(p, r);
// const baths = propBaths(p, r);
// const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
// const canRate = STATUS_MAP[r.status] === 'depositPaid' || STATUS_MAP[r.status] === 'completed';
// const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
// const deadline = hasTimeWindow
// ? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
// : null;
// const isExpired = deadline ? Date.now() > deadline : false;
// const isPaying = payingId === r.id;
// const [showRating, setShowRating] = useState(false);
// const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
// const [ratingComment, setRatingComment] = useState('');
// const [submittingRating, setSubmittingRating] = useState(false);
// 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">
// {img && <div className="mb-4 w-full h-40 rounded-xl overflow-hidden"><img src={img} alt="" className="w-full h-full object-cover" /></div>}
// <div className="flex justify-between items-start mb-3">
// <div>
// <StatusBadge code={r.status} />
// {addr && <div className="flex items-center gap-1 text-gray-500 text-sm mt-1"><MapPin className="w-4 h-4"/>{addr}</div>}
// </div>
// <div className="text-left">
// <div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
// <div className="text-xs text-gray-500">السعر الإجمالي</div>
// </div>
// </div>
// {(beds||baths) && <div className="flex gap-3 mb-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 mb-4 text-center">
// <div className="bg-gray-50 p-2 rounded-lg">
// <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-2 rounded-lg">
// <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>
// {isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-3 rounded-xl mb-3">
// <div className="flex items-center justify-between mb-1">
// <span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> متبقي للدفع:</span>
// <CountdownTimer deadline={deadline} />
// </div>
// <div className="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
// </div>}
// <div className="flex gap-3 pt-3 border-t border-gray-100">
// <button onClick={() => onViewDetails(r)}
// className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
// <Eye className="w-4 h-4"/> التفاصيل
// </button>
// {isOwnerConfirmed && !isExpired && <button onClick={() => onPay(r)} disabled={isPaying}
// className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
// {isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
// </button>}
// </div>
// {canRate && !showRating && <button onClick={() => setShowRating(true)}
// className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
// <Star className="w-4 h-4"/> قيّم هذا العقار
// </button>}
// {canRate && showRating && <div className="mt-3 bg-amber-50 p-3 rounded-xl">
// <div className="space-y-2 mb-3">
// {[
// { key: 'clean', label: 'النظافة' },
// { key: 'services', label: 'الخدمات' },
// { key: 'ownerBehavior', label: 'تعامل المالك' },
// { key: 'experience', label: 'التجربة العامة' },
// ].map(cat => <div key={cat.key} className="flex items-center justify-between">
// <span className="text-sm text-gray-700">{cat.label}</span>
// <div className="flex gap-0.5">
// {[1,2,3,4,5].map(n => (
// <button key={n} onClick={() => setRatings(p => ({...p, [cat.key]: n}))}
// className={`p-0.5 rounded-full transition-colors ${n <= ratings[cat.key] ? 'text-amber-500' : 'text-gray-300'}`}>
// <Star className={`w-4 h-4 ${n <= ratings[cat.key] ? 'fill-amber-500' : ''}`} />
// </button>
// ))}
// </div>
// </div>)}
// </div>
// <textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
// placeholder="أكتب تعليقك (اختياري)"
// className="w-full p-2 text-sm border border-amber-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 mb-2" rows={2} />
// <div className="flex gap-2">
// <button onClick={async () => {
// if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error('قيّم جميع الفئات');
// setSubmittingRating(true);
// try {
// await addPropertyRating({ reservationId: r.id, cleanRating: ratings.clean, servicesRating: ratings.services, ownerBehaviorRating: ratings.ownerBehavior, experienceRating: ratings.experience, comment: ratingComment || null });
// toast.success('تم إرسال التقييم');
// setShowRating(false);
// setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
// setRatingComment('');
// } catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
// finally { setSubmittingRating(false); }
// }} disabled={submittingRating}
// className="flex-1 bg-amber-500 text-white py-1.5 rounded-lg text-sm font-medium hover:bg-amber-600 transition-colors disabled:bg-gray-300">
// {submittingRating ? 'جاري الإرسال...' : 'إرسال التقييم'}
// </button>
// <button onClick={() => { setShowRating(false); setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 }); setRatingComment(''); }}
// className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">إلغاء</button>
// </div>
// </div>}
// </div>
// </motion.div>
// );
// }
// function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
// if (!isOpen || !r) return null;
// const p = r._prop;
// const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
// const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
// const deadline = hasTimeWindow
// ? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
// : null;
// const isExpired = deadline ? Date.now() > deadline : false;
// const isPaying = payingId === r.id;
// return (
// <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={onClose}>
// <motion.div initial={{scale:0.9,y:20}} animate={{scale:1,y:0}} exit={{scale:0.9,y:20}}
// className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e=>e.stopPropagation()}>
// <div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
// <div className="flex justify-between items-center">
// <h2 className="text-xl font-bold">تفاصيل الحجز</h2>
// <button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
// </div>
// <p className="text-amber-100 text-sm mt-1">رقم الحجز: #{r.id}</p>
// </div>
// <div className="p-6 space-y-6">
// {p && <div className="bg-gray-50 p-4 rounded-xl">
// <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Home className="w-5 h-5 text-amber-500"/> معلومات العقار</h3>
// <p><span className="text-gray-500">العنوان:</span> {propAddr(p, r)||'—'}</p>
// {(propBeds(p, r)||propBaths(p, r)) && <div className="flex gap-3 mt-2">
// {propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} غرف</span>}
// {propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} حمامات</span>}
// </div>}
// </div>}
// <div className="bg-gray-50 p-4 rounded-xl">
// <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> تفاصيل الحجز</h3>
// <div className="grid grid-cols-2 gap-4">
// <div><p className="text-gray-500">تاريخ البداية</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
// <div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
// <div><p className="text-gray-500">الحالة</p><StatusBadge code={r.status}/></div>
// <div><p className="text-gray-500">تاريخ الإنشاء</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
// </div>
// </div>
// <div className="bg-amber-50 p-4 rounded-xl">
// <h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
// <div className="flex justify-between font-bold"><span className="text-gray-900">الإجمالي</span><span className="text-amber-600 text-lg">{r.totalPrice?.toLocaleString()??'—'}</span></div>
// </div>
// {isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-4 rounded-xl">
// <div className="flex items-center justify-between mb-2">
// <span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
// <CountdownTimer deadline={deadline} />
// </div>
// <div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
// {!isExpired && <button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
// className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
// {isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
// </button>}
// </div>}
// </div>
// </motion.div>
// </motion.div>
// );
// }
// export default function UserReservationsPage() {
// const router = useRouter();
// const [reservations, setReservations] = useState([]);
// const [filtered, setFiltered] = useState([]);
// const [loading, setLoading] = useState(true);
// const [selected, setSelected] = useState(null);
// const [filterStatus, setFilterStatus] = useState('all');
// const [searchTerm, setSearchTerm] = useState('');
// const [payingId, setPayingId] = useState(null);
// useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
// const loadReservations = useCallback(async () => {
// try {
// const [data, rentProps] = await Promise.all([
// getUserReservations(),
// getRentProperties().catch(() => []),
// ]);
// const list = Array.isArray(data) ? data : [];
// const propsList = Array.isArray(rentProps) ? rentProps : [];
// const propMap = {};
// propsList.forEach(rp => {
// const info = rp?.propertyInformation ?? {};
// if (rp?.allowedPaymentPeriod) info.allowedPaymentPeriod = rp.allowedPaymentPeriod;
// propMap[rp.propertyInformationId] = info;
// propMap[rp.propertyInformation?.id] = info;
// });
// const enriched = list.map(r => {
// if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId];
// return r;
// });
// setReservations(enriched);
// setFiltered(enriched);
// } catch (err) {
// console.error(err);
// toast.error('فشل تحميل الحجوزات');
// setReservations([]);
// setFiltered([]);
// }
// setLoading(false);
// }, []);
// useEffect(() => {
// let r = reservations;
// if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
// if (searchTerm) { const q = searchTerm.toLowerCase(); r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q)); }
// setFiltered(r);
// }, [reservations, filterStatus, searchTerm]);
// const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
// const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
// const handlePay = async (r) => {
// setPayingId(r.id);
// try {
// await payDeposit({
// reservationId: r.id,
// paymentTypeId: 1,
// transactionType: 1,
// comment: null,
// });
// toast.success('تم دفع السلفة بنجاح!');
// loadReservations();
// } catch (err) {
// toast.error(err?.message || 'فشل عملية الدفع');
// } finally {
// setPayingId(null);
// }
// };
// if (loading) 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>;
// return (
// <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
// <Toaster position="top-center" reverseOrder={false} />
// <DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} onPay={handlePay} payingId={payingId} />
// <div className="container mx-auto px-4">
// <motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
// <button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"><ArrowLeft className="w-5 h-5"/> الرجوع</button>
// <h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
// <p className="text-gray-600">لديك {reservations.length} حجز</p>
// </motion.div>
// <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
// {Object.entries(counts).map(([s, c]) => (
// <motion.div key={s} initial={{opacity:0,y:20}} animate={{opacity:1,y:0}}
// className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${filterStatus===s?'border-amber-500 bg-amber-50':'border-gray-200'}`}
// onClick={() => setFilterStatus(s)}>
// <div className="text-2xl font-bold text-amber-600">{c}</div>
// <div className="text-sm text-gray-600">{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}</div>
// </motion.div>
// ))}
// </div>
// <div className="mb-6 relative">
// <Search className="absolute left-3 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 pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"/>
// </div>
// {filtered.length === 0 ? (
// <div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
// <Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4"/>
// <h3 className="text-xl font-bold text-gray-900 mb-2">لا توجد حجوزات</h3>
// <p className="text-gray-600">لم تقم بأي حجز حتى الآن</p>
// </div>
// ) : (
// <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
// {filtered.map(r => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} onPay={handlePay} payingId={payingId} />)}
// </div>
// )}
// </div>
// </div>
// );
// }
'use client';
import { useState, useEffect, useCallback } from 'react';
@ -5,11 +400,12 @@ import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import {
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
MapPin, DollarSign, Home, ArrowLeft,
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star, Flag,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { getRentProperty } from '../utils/api';
import { getRentProperties, getUserReservations, payDeposit } from '../utils/api';
import { addPropertyRating } from '../utils/ratings';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
@ -37,27 +433,261 @@ function StatusBadge({ code }) {
);
}
async function enrich(reservation) {
if (!reservation.propertyId) return reservation;
const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? '';
const propImages = (p, r) => {
if (p?.images && Array.isArray(p.images)) return p.images;
if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images;
return [];
};
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
const getAuthToken = () => {
if (typeof window === 'undefined') return '';
return (
AuthService.getToken?.() ||
AuthService.getAccessToken?.() ||
localStorage.getItem('token') ||
localStorage.getItem('accessToken') ||
localStorage.getItem('authToken') ||
''
);
};
const readStoredUser = () => {
if (typeof window === 'undefined') return null;
const keys = ['user', 'currentUser', 'authUser', 'profile'];
for (const key of keys) {
const raw = localStorage.getItem(key);
if (!raw) continue;
try {
const prop = await getRentProperty(reservation.propertyId);
reservation._prop = prop?.propertyInformation ?? prop ?? null;
} catch { /* skip */ }
return reservation;
return JSON.parse(raw);
} catch {
return raw;
}
}
return null;
};
const extractNumericUserId = (value) => {
if (!value) return null;
if (typeof value === 'number') return Number.isInteger(value) ? value : null;
if (typeof value === 'string') {
const n = Number(value);
return Number.isInteger(n) ? n : null;
}
if (typeof value === 'object') {
const candidates = [
value.id,
value.userId,
value.userID,
value.user?.id,
value.user?.userId,
value.profile?.id,
value.profile?.userId,
value.data?.id,
];
for (const candidate of candidates) {
const id = extractNumericUserId(candidate);
if (id !== null) return id;
}
}
return null;
};
async function reportReservation(reservationId, message) {
const user = AuthService.getUser?.() ?? readStoredUser();
const reporter = extractNumericUserId(user);
const rid = Number(reservationId);
if (!Number.isInteger(rid)) {
throw new Error('رقم الحجز غير صالح');
}
if (!Number.isInteger(reporter)) {
throw new Error('تعذر تحديد المستخدم الحالي');
}
const token = getAuthToken();
const res = await fetch(`${API_BASE}/ReservationReports/ReportReservation`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
reservationId: rid,
message: message ?? null,
reporter,
}),
});
if (!res.ok) {
let errorMessage = 'فشل إرسال البلاغ';
try {
const data = await res.json();
errorMessage = data?.message || data?.title || errorMessage;
} catch (_) {
try {
const text = await res.text();
if (text) errorMessage = text;
} catch (_) {}
}
throw new Error(errorMessage);
}
try {
return await res.json();
} catch {
return null;
}
}
const propAddr = (p) => p?.address ?? '';
const propImages = (p) => Array.isArray(p?.images) ? p.images : [];
const propBeds = (p) => p?.numberOfBedRooms ?? 0;
const propBaths = (p) => p?.numberOfBathRooms ?? 0;
function parseTimeSpan(str) {
if (!str) return 0;
const clean = str.replace(/-/g, '');
const dotIdx = clean.indexOf('.');
let days = 0, timePart = clean;
if (dotIdx !== -1) {
days = parseInt(clean.substring(0, dotIdx), 10) || 0;
timePart = clean.substring(dotIdx + 1);
}
const parts = timePart.split(':');
if (parts.length < 2) return days * 86400000;
const hh = parseInt(parts[0], 10) || 0;
const mm = parseInt(parts[1], 10) || 0;
const ss = parts.length > 2 ? (parseInt(parts[2], 10) || 0) : 0;
return ((days * 86400) + (hh * 3600) + (mm * 60) + ss) * 1000;
}
function ReservationCard({ r, onViewDetails }) {
function formatWindowDuration(str) {
if (!str) return '';
const clean = str.replace(/-/g, '');
const dotIdx = clean.indexOf('.');
let totalHours = 0, timePart = clean;
if (dotIdx !== -1) {
const days = parseInt(clean.substring(0, dotIdx), 10) || 0;
totalHours += days * 24;
timePart = clean.substring(dotIdx + 1);
}
const parts = timePart.split(':');
if (parts.length >= 2) {
totalHours += parseInt(parts[0], 10) || 0;
}
if (totalHours > 0) return `${String(totalHours).padStart(2, '0')}:00:00`;
return timePart.substring(0, 8);
}
function CountdownTimer({ deadline }) {
const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
useEffect(() => {
if (!deadline) return;
const tick = () => setRemaining(Math.max(0, deadline - Date.now()));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [deadline]);
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
const h = Math.floor(remaining / 3600000);
const m = Math.floor((remaining % 3600000) / 60000);
const s = Math.floor((remaining % 60000) / 1000);
const pad = (n) => String(n).padStart(2, '0');
return <span className="text-amber-600 text-sm font-mono font-bold" dir="ltr">{pad(h)}:{pad(m)}:{pad(s)}</span>;
}
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
const [message, setMessage] = useState('');
useEffect(() => {
if (isOpen) setMessage('');
}, [isOpen, reservation?.id]);
if (!isOpen || !reservation) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.95, y: 16 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.95, y: 16 }}
className="w-full max-w-lg rounded-2xl bg-white shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 text-white">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">الإبلاغ عن الحجز</h2>
<p className="text-red-100 text-sm mt-1">رقم الحجز: #{reservation.id}</p>
</div>
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
<XCircle className="h-6 w-6" />
</button>
</div>
</div>
<div className="p-6">
<p className="text-gray-700 mb-4 leading-7">
اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
</p>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="اكتب تفاصيل البلاغ هنا..."
rows={5}
className="w-full resize-none rounded-xl border border-gray-300 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-red-500"
/>
<div className="mt-5 flex gap-3">
<button
onClick={() => onSubmit(message)}
disabled={submitting}
className={`flex-1 rounded-xl py-2.5 text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
submitting ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-red-600 text-white hover:bg-red-700'
}`}
>
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Flag className="h-4 w-4" />}
{submitting ? 'جاري الإرسال...' : 'إرسال البلاغ'}
</button>
<button
onClick={onClose}
disabled={submitting}
className="rounded-xl bg-gray-200 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-300 transition-colors disabled:cursor-not-allowed"
>
إلغاء
</button>
</div>
</div>
</motion.div>
</motion.div>
);
}
function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportingId }) {
const p = r._prop;
const imgs = propImages(p);
const imgs = propImages(p, r);
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
const addr = propAddr(p);
const beds = propBeds(p);
const baths = propBaths(p);
const addr = propAddr(p, r);
const beds = propBeds(p, r);
const baths = propBaths(p, r);
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
const canRate = STATUS_MAP[r.status] === 'depositPaid' || STATUS_MAP[r.status] === 'completed';
const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
const deadline = hasTimeWindow
? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
: null;
const isExpired = deadline ? Date.now() > deadline : false;
const isPaying = payingId === r.id;
const isReporting = reportingId === r.id;
const [showRating, setShowRating] = useState(false);
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
const [ratingComment, setRatingComment] = useState('');
const [submittingRating, setSubmittingRating] = useState(false);
return (
<motion.div initial={{ opacity:0,y:20 }} animate={{ opacity:1,y:0 }}
@ -85,20 +715,89 @@ function ReservationCard({ r, onViewDetails }) {
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
</div>
</div>
{isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-3 rounded-xl mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> متبقي للدفع:</span>
<CountdownTimer deadline={deadline} />
</div>
<div className="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
</div>}
<div className="flex gap-3 pt-3 border-t border-gray-100">
<button onClick={() => onViewDetails(r)}
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
<Eye className="w-4 h-4"/> التفاصيل
</button>
{isOwnerConfirmed && !isExpired && <button onClick={() => onPay(r)} disabled={isPaying}
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
</button>}
</div>
<button onClick={() => onReport(r)} disabled={isReporting}
className={`w-full mt-3 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isReporting ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-50 text-red-700 hover:bg-red-100'}`}>
{isReporting ? <Loader2 className="w-4 h-4 animate-spin"/> : <Flag className="w-4 h-4"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
</button>
{canRate && !showRating && <button onClick={() => setShowRating(true)}
className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
<Star className="w-4 h-4"/> قيّم هذا العقار
</button>}
{canRate && showRating && <div className="mt-3 bg-amber-50 p-3 rounded-xl">
<div className="space-y-2 mb-3">
{[
{ key: 'clean', label: 'النظافة' },
{ key: 'services', label: 'الخدمات' },
{ key: 'ownerBehavior', label: 'تعامل المالك' },
{ key: 'experience', label: 'التجربة العامة' },
].map(cat => <div key={cat.key} className="flex items-center justify-between">
<span className="text-sm text-gray-700">{cat.label}</span>
<div className="flex gap-0.5">
{[1,2,3,4,5].map(n => (
<button key={n} onClick={() => setRatings(p => ({...p, [cat.key]: n}))}
className={`p-0.5 rounded-full transition-colors ${n <= ratings[cat.key] ? 'text-amber-500' : 'text-gray-300'}`}>
<Star className={`w-4 h-4 ${n <= ratings[cat.key] ? 'fill-amber-500' : ''}`} />
</button>
))}
</div>
</div>)}
</div>
<textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
placeholder="أكتب تعليقك (اختياري)"
className="w-full p-2 text-sm border border-amber-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 mb-2" rows={2} />
<div className="flex gap-2">
<button onClick={async () => {
if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error('قيّم جميع الفئات');
setSubmittingRating(true);
try {
await addPropertyRating({ reservationId: r.id, cleanRating: ratings.clean, servicesRating: ratings.services, ownerBehaviorRating: ratings.ownerBehavior, experienceRating: ratings.experience, comment: ratingComment || null });
toast.success('تم إرسال التقييم');
setShowRating(false);
setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
setRatingComment('');
} catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
finally { setSubmittingRating(false); }
}} disabled={submittingRating}
className="flex-1 bg-amber-500 text-white py-1.5 rounded-lg text-sm font-medium hover:bg-amber-600 transition-colors disabled:bg-gray-300">
{submittingRating ? 'جاري الإرسال...' : 'إرسال التقييم'}
</button>
<button onClick={() => { setShowRating(false); setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 }); setRatingComment(''); }}
className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">إلغاء</button>
</div>
</div>}
</div>
</motion.div>
);
}
function DetailsModal({ r, isOpen, onClose }) {
function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
if (!isOpen || !r) return null;
const p = r._prop;
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
const deadline = hasTimeWindow
? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
: null;
const isExpired = deadline ? Date.now() > deadline : false;
const isPaying = payingId === r.id;
const isReporting = reportingId === r.id;
return (
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
@ -115,10 +814,10 @@ function DetailsModal({ r, isOpen, onClose }) {
<div className="p-6 space-y-6">
{p && <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Home className="w-5 h-5 text-amber-500"/> معلومات العقار</h3>
<p><span className="text-gray-500">العنوان:</span> {propAddr(p)||''}</p>
{(propBeds(p)||propBaths(p)) && <div className="flex gap-3 mt-2">
{propBeds(p)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p)} غرف</span>}
{propBaths(p)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p)} حمامات</span>}
<p><span className="text-gray-500">العنوان:</span> {propAddr(p, r)||''}</p>
{(propBeds(p, r)||propBaths(p, r)) && <div className="flex gap-3 mt-2">
{propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} غرف</span>}
{propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} حمامات</span>}
</div>}
</div>}
<div className="bg-gray-50 p-4 rounded-xl">
@ -134,6 +833,21 @@ function DetailsModal({ r, isOpen, onClose }) {
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
<div className="flex justify-between font-bold"><span className="text-gray-900">الإجمالي</span><span className="text-amber-600 text-lg">{r.totalPrice?.toLocaleString()??''}</span></div>
</div>
{isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-4 rounded-xl">
<div className="flex items-center justify-between mb-2">
<span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
<CountdownTimer deadline={deadline} />
</div>
<div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
{!isExpired && <button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
</button>}
</div>}
<button onClick={() => { onReport(r); onClose(); }} disabled={isReporting}
className={`w-full py-2 rounded-xl font-medium transition-colors flex items-center justify-center gap-2 ${isReporting ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-50 text-red-700 hover:bg-red-100'}`}>
{isReporting ? <Loader2 className="w-5 h-5 animate-spin"/> : <Flag className="w-5 h-5"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
</button>
</div>
</motion.div>
</motion.div>
@ -148,19 +862,31 @@ export default function UserReservationsPage() {
const [selected, setSelected] = useState(null);
const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [payingId, setPayingId] = useState(null);
const [reportDialog, setReportDialog] = useState({ open: false, reservation: null });
const [reportingId, setReportingId] = useState(null);
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
const loadReservations = useCallback(async () => {
try {
const res = await fetch(`${API_BASE}/Reservations/GetUserResevations`, {
headers: { Authorization: `Bearer ${AuthService.getToken()}` },
const [data, rentProps] = await Promise.all([
getUserReservations(),
getRentProperties().catch(() => []),
]);
const list = Array.isArray(data) ? data : [];
const propsList = Array.isArray(rentProps) ? rentProps : [];
const propMap = {};
propsList.forEach(rp => {
const info = rp?.propertyInformation ?? {};
if (rp?.allowedPaymentPeriod) info.allowedPaymentPeriod = rp.allowedPaymentPeriod;
propMap[rp.propertyInformationId] = info;
propMap[rp.propertyInformation?.id] = info;
});
const enriched = list.map(r => {
if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId];
return r;
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
let list = json.data || json || [];
if (!Array.isArray(list)) list = [];
const enriched = await Promise.all(list.map(enrich));
setReservations(enriched);
setFiltered(enriched);
} catch (err) {
@ -175,19 +901,78 @@ export default function UserReservationsPage() {
useEffect(() => {
let r = reservations;
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
if (searchTerm) { const q = searchTerm.toLowerCase(); r = r.filter(x => propAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)); }
if (searchTerm) {
const q = searchTerm.toLowerCase();
r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q));
}
setFiltered(r);
}, [reservations, filterStatus, searchTerm]);
const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
const handlePay = async (r) => {
setPayingId(r.id);
try {
await payDeposit({
reservationId: r.id,
paymentTypeId: 1,
transactionType: 1,
comment: null,
});
toast.success('تم دفع السلفة بنجاح!');
loadReservations();
} catch (err) {
toast.error(err?.message || 'فشل عملية الدفع');
} finally {
setPayingId(null);
}
};
const openReportDialog = (r) => {
setReportDialog({ open: true, reservation: r });
};
const closeReportDialog = () => {
setReportDialog({ open: false, reservation: null });
};
const handleSubmitReport = async (message) => {
if (!reportDialog.reservation) return;
setReportingId(reportDialog.reservation.id);
try {
await reportReservation(reportDialog.reservation.id, message.trim() || null);
toast.success('تم إرسال البلاغ بنجاح');
closeReportDialog();
} catch (err) {
toast.error(err?.message || 'فشل إرسال البلاغ');
} finally {
setReportingId(null);
}
};
if (loading) 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>;
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} />
<DetailsModal
r={selected}
isOpen={!!selected}
onClose={() => setSelected(null)}
onPay={handlePay}
onReport={openReportDialog}
payingId={payingId}
reportingId={reportingId}
/>
<ReportDialog
isOpen={reportDialog.open}
reservation={reportDialog.reservation}
onClose={closeReportDialog}
onSubmit={handleSubmitReport}
submitting={!!reportingId}
/>
<div className="container mx-auto px-4">
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
<button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"><ArrowLeft className="w-5 h-5"/> الرجوع</button>
@ -217,7 +1002,17 @@ export default function UserReservationsPage() {
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filtered.map(r => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} />)}
{filtered.map(r => (
<ReservationCard
key={r.id}
r={r}
onViewDetails={setSelected}
onPay={handlePay}
onReport={openReportDialog}
payingId={payingId}
reportingId={reportingId}
/>
))}
</div>
)}
</div>

View File

@ -127,19 +127,11 @@ const AuthService = Object.freeze({
},
/**
* User has Admin role
* @returns {boolean}
*/
isAdmin() {
return this.getRoles().includes('Admin');
},
/**
* Authenticated user without Owner or Admin role (i.e. customer)
* Authenticated user without Owner role (i.e. customer)
* @returns {boolean}
*/
isCustomer() {
return this.isAuthenticated() && !this.isOwner() && !this.isAdmin();
return this.isAuthenticated() && !this.isOwner();
},
/**
@ -160,3 +152,4 @@ const AuthService = Object.freeze({
});
export default AuthService;

662
app/settings/page.js Normal file
View File

@ -0,0 +1,662 @@
// '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>
// );
// }
'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 [showReportDialog, setShowReportDialog] = useState(false);
const [reportSubject, setReportSubject] = useState('');
const [reportBody, setReportBody] = useState('');
const [isSendingReport, setIsSendingReport] = 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) {
console.error('Delete account error:', err);
toast.error(err.message || 'فشل حذف الحساب');
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
setDeletePassword('');
}
};
const handleSendGeneralReport = async () => {
if (!reportSubject.trim() || !reportBody.trim()) {
toast.error('الرجاء تعبئة عنوان البلاغ ونصه');
return;
}
if (reportSubject.trim().length > 300) {
toast.error('عنوان البلاغ يجب ألا يتجاوز 300 حرف');
return;
}
const token =
AuthService.getToken?.() ||
(typeof window !== 'undefined'
? localStorage.getItem('token') ||
localStorage.getItem('accessToken') ||
localStorage.getItem('authToken')
: null);
if (!token) {
console.error('No token found. Checked AuthService.getToken and localStorage keys: token, accessToken, authToken');
toast.error('لم يتم العثور على التوكن');
return;
}
setIsSendingReport(true);
try {
const res = await fetch('http://45.93.137.91/api/Reports/SendGeneralReport', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
subject: reportSubject.trim(),
body: reportBody.trim(),
}),
});
const responseText = await res.text();
if (!res.ok) {
console.error('Send report failed:', {
status: res.status,
statusText: res.statusText,
responseText,
});
throw new Error(responseText || `فشل إرسال البلاغ (HTTP ${res.status})`);
}
console.log('Send report success:', responseText);
toast.success(responseText || 'تم إرسال البلاغ بنجاح');
setShowReportDialog(false);
setReportSubject('');
setReportBody('');
} catch (err) {
console.error('Send report error:', err);
toast.error(err.message || 'حدث خطأ أثناء إرسال البلاغ');
} finally {
setIsSendingReport(false);
}
};
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: 'كيف نحمي بياناتك' },
{ icon: AlertTriangle, label: 'إرسال بلاغ عام', desc: 'إرسال مشكلة أو ملاحظة إلى الإدارة', action: () => setShowReportDialog(true) },
]
},
];
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;
const content = (
<>
<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" />
</>
);
if (item.action) {
return (
<button
key={item.label}
onClick={item.action}
className="w-full flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group text-right"
>
{content}
</button>
);
}
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"
>
{content}
</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>
)}
{showReportDialog && (
<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-amber-100 flex items-center justify-center">
<MessageCircle className="w-5 h-5 text-amber-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">إرسال بلاغ عام</h3>
<p className="text-sm text-gray-500">سيتم إرسال البلاغ إلى الإدارة</p>
</div>
<button
onClick={() => {
setShowReportDialog(false);
setReportSubject('');
setReportBody('');
}}
className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-400" />
</button>
</div>
<input
type="text"
value={reportSubject}
onChange={(e) => setReportSubject(e.target.value)}
placeholder="عنوان البلاغ"
maxLength={300}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
/>
<textarea
value={reportBody}
onChange={(e) => setReportBody(e.target.value)}
placeholder="اكتب تفاصيل البلاغ هنا..."
rows={5}
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
/>
<div className="flex gap-3">
<button
onClick={() => {
setShowReportDialog(false);
setReportSubject('');
setReportBody('');
}}
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={handleSendGeneralReport}
disabled={isSendingReport}
className="flex-1 px-4 py-3 rounded-xl bg-amber-600 text-white hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
>
{isSendingReport ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageCircle className="w-4 h-4" />}
{isSendingReport ? 'جاري الإرسال...' : 'إرسال البلاغ'}
</button>
</div>
</motion.div>
</div>
)}
</div>
);
}

165
app/support/page.js Normal file
View 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
View 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>
);
}

View File

@ -452,9 +452,44 @@
import AuthService from '../services/AuthService';
import AuthService from '../services/AuthService';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || 'http://45.93.137.91/api';
function isFormData(value) {
return typeof FormData !== 'undefined' && value instanceof FormData;
}
class ApiBlockedError extends Error {
constructor(message = 'Your account is blocked') {
super(message);
this.name = 'ApiBlockedError';
this.status = 451;
}
}
export function isApiBlockedError(error) {
return error instanceof ApiBlockedError || error?.status === 451;
}
function redirectToBlockedPage() {
if (typeof window !== 'undefined' && window.location.pathname !== '/blocked') {
window.location.replace('/blocked');
}
}
function assertNotBlocked(response) {
if (response.status === 451) {
redirectToBlockedPage();
throw new ApiBlockedError();
}
}
function buildApiUrl(base, endpoint) {
return `${base.replace(/\/$/, '')}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
}
/**
* Generic API fetch — attaches auth token, unwraps { data } envelope
@ -463,23 +498,39 @@ async function apiFetch(endpoint, options = {}) {
const token = AuthService.getToken();
const headers = {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
...(options.headers || {}),
};
console.log('[API] Request:', options.method || 'GET', `${API_BASE}${endpoint}`);
const hasBody = options.body != null;
const bodyIsFormData = isFormData(options.body);
const res = await fetch(`${API_BASE}${endpoint}`, {
if (hasBody && !bodyIsFormData && !headers['Content-Type'] && !headers['content-type']) {
headers['Content-Type'] = 'application/json';
}
const url = `${API_BASE}${endpoint}`;
console.log('API Request:', url);
console.log('API Method:', options.method || 'GET');
console.log('API Body:', hasBody ? options.body : null);
const res = await fetch(url, {
...options,
headers,
body:
hasBody && !bodyIsFormData && typeof options.body !== 'string'
? JSON.stringify(options.body)
: options.body,
});
console.log('[API] Response:', res.status, endpoint);
console.log('API Response Status:', res.status);
console.log('API Response OK:', res.ok);
assertNotBlocked(res);
if (!res.ok && res.status !== 206) {
const text = await res.text().catch(() => '');
console.error('[API] Error:', res.status, text);
console.error('API Error Response:', text || res.statusText);
throw new Error(`API ${res.status}: ${text || res.statusText}`);
}
@ -501,24 +552,28 @@ async function apiFetch(endpoint, options = {}) {
* Auth fetch — returns full { status, data, ok } for status-code handling
*/
async function authFetch(endpoint, body, token = null) {
console.log('[Auth] Request:', `${API_BASE}${endpoint}`);
const headers = {};
const bodyIsFormData = isFormData(body);
if (!bodyIsFormData) {
headers['Content-Type'] = 'application/json';
}
const headers = { 'Content-Type': 'application/json' };
if (token) {
headers['Authorization'] = `Bearer ${token}`;
console.log('[Auth] Sending with Bearer token');
}
const res = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers,
body: JSON.stringify(body),
body: bodyIsFormData ? body : JSON.stringify(body),
});
console.log('[Auth] Response status:', res.status, endpoint);
assertNotBlocked(res);
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
if (data && typeof data === 'object' && 'data' in data) {
@ -528,7 +583,35 @@ async function authFetch(endpoint, body, token = null) {
data = text;
}
const message = (typeof data === 'object' && data?.message) ? data.message : null;
const message = typeof data === 'object' && data?.message ? data.message : null;
return { status: res.status, data, ok: res.ok || res.status === 206, message };
}
async function reportFetch(endpoint, body) {
const res = await fetch(buildApiUrl(REPORT_API_BASE, endpoint), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
assertNotBlocked(res);
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;
}
const message = typeof data === 'object' && data?.message ? data.message : null;
return { status: res.status, data, ok: res.ok || res.status === 206, message };
}
@ -560,7 +643,7 @@ export async function getSaleProperties() {
export async function getSaleProperty(id) {
const items = await apiFetch('/SaleProperties/GetSaleProperties');
if (!Array.isArray(items)) return items;
return items.find(p => p.id == id) || items[0];
return items.find((p) => p.id == id) || items[0];
}
// ─── Properties (generic) ───
@ -582,17 +665,9 @@ export async function getTopRecommendations(count = 10) {
// ─── Reservations ───
export async function getAvailableDateRanges(propertyId, fromDate = null, toDate = null) {
console.log('[API] Fetching available dates for property:', {
propertyId,
fromDate,
toDate,
});
const qs = new URLSearchParams();
if (fromDate) qs.set('fromDate', fromDate);
if (toDate) qs.set('toDate', toDate);
const query = qs.toString();
return apiFetch(
@ -617,17 +692,13 @@ export async function checkAvailability(propertyId, fromDate = null, toDate = nu
}
export async function bookReservation(propertyInfoId, startDate, endDate) {
const payload = {
return apiFetch('/Reservations/BookReservation/book', {
method: 'POST',
body: {
propertyInfoId,
startDate,
endDate,
};
console.log('[API] Booking reservation FINAL:', payload);
return apiFetch('/Reservations/BookReservation/book', {
method: 'POST',
body: JSON.stringify(payload),
},
});
}
@ -640,27 +711,66 @@ export async function getTerms() {
// ─── Profile ───
export async function getCustomerByUserId(userId) {
console.log('[API] Fetching customer by user ID:', userId);
return apiFetch(`/Customer/GetByUserId/${userId}`);
}
export async function getOwnerByUserId(userId) {
console.log('[API] Fetching owner by user ID:', userId);
return apiFetch(`/Owner/GetByUserId/${userId}`);
}
// ─── Properties ───
export async function getMyRentListings() {
console.log('[API] Fetching my rent listings');
return apiFetch(`/RentProperties/GetMyRentListings`);
return apiFetch('/RentProperties/GetMyRentListings');
}
export async function addRentProperty(data) {
console.log('[API] Adding rent property:', data.PropertyInformation?.Address);
return apiFetch('/RentProperties/AddRentProperty', {
method: 'POST',
body: JSON.stringify(data),
body: data,
});
}
export async function editRentProperty(id, data) {
return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
method: 'PUT',
body: data,
});
}
export async function editSaleProperty(id, data) {
return apiFetch(`/SaleProperties/EditSaleProperty/${id}`, {
method: 'PUT',
body: data,
});
}
export async function addSaleProperty(data) {
return apiFetch('/SaleProperties/AddSaleProperty', {
method: 'POST',
body: data,
});
}
export async function getMySaleListings() {
return apiFetch('/SaleProperties/GetMySaleListings');
}
export async function getSalePropertyById(id) {
return apiFetch(`/SaleProperties/${id}`);
}
export async function updateRentPropertyStatus(id, status) {
return apiFetch(`/RentProperties/UpdateStatus/${id}`, {
method: 'PUT',
body: { status },
});
}
export async function updateSalePropertyStatus(id, status) {
return apiFetch(`/SaleProperties/UpdateStatus/${id}`, {
method: 'PUT',
body: { status },
});
}
@ -673,9 +783,9 @@ export async function getCurrencies() {
// ─── Files ───
export async function uploadPicture(file) {
console.log('[API] Uploading picture:', file.name);
const formData = new FormData();
formData.append('image', file);
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}/Files/UploadPicture`, {
@ -686,8 +796,9 @@ export async function uploadPicture(file) {
body: formData,
});
assertNotBlocked(res);
const text = await res.text();
console.log('[API] Upload response:', res.status, text?.substring(0, 100));
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${text}`);
@ -702,14 +813,17 @@ export async function uploadPicture(file) {
// ─── Auth: Registration ───
async function multipartAuthFetch(endpoint, formData) {
console.log('[Auth] Multipart request:', `${API_BASE}${endpoint}`);
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
body: formData,
});
console.log('[Auth] Response status:', res.status, endpoint);
assertNotBlocked(res);
const text = await res.text();
let data = null;
@ -726,30 +840,34 @@ async function multipartAuthFetch(endpoint, formData) {
return { status: res.status, data, ok: res.ok || res.status === 206, message: data?.message };
}
export async function addOwner(data, frontImage = null, backImage = null) {
console.log('[Auth] Registering owner (multipart):', data.email);
export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) {
const formData = new FormData();
formData.append('FirstName', data.firstName || data.FirstName || '');
formData.append('LastName', data.lastName || data.LastName || '');
formData.append('Email', data.email || '');
formData.append('PhoneNumber', data.phoneNumber || '');
formData.append('WhatsAppNumber', data.whatsAppNumber || '');
formData.append('Phone', data.phone || '');
formData.append('NationalNumber', data.nationalNumber || '');
formData.append('Password', data.password || '');
formData.append('Type', String(data.ownerType ?? data.Type ?? 0));
formData.append('Language', '0');
formData.append('Email', data.email || data.Email || '');
const phoneValue = data.phone || data.phoneNumber || data.Phone || data.PhoneNumber || '';
const whatsappValue =
data.whatsAppNumber || data.whatsapp || data.WhatsAppNumber || data.WhatsApp || '';
formData.append('PhoneNumber', phoneValue);
formData.append('Phone', phoneValue);
formData.append('WhatsAppNumber', whatsappValue);
formData.append('NationalNumber', data.nationalNumber || data.NationalNumber || '');
formData.append('Password', data.password || data.Password || '');
formData.append('Type', String(data.type ?? data.ownerType ?? data.Type ?? 0));
formData.append('Language', String(data.language ?? data.Language ?? 1));
if (frontImage) formData.append('FrontIdCarImagePath', frontImage);
if (backImage) formData.append('RearIdCarImagePath', backImage);
if (licenseImage) formData.append('LicenseImagePath', licenseImage);
return multipartAuthFetch('/Owner/Add', formData);
}
export async function addCustomer(data, frontImage = null, backImage = null) {
console.log('[Auth] Registering customer (multipart):', data.email);
const formData = new FormData();
formData.append('FirstName', data.firstName || data.FirstName || '');
formData.append('LastName', data.lastName || data.LastName || '');
@ -771,7 +889,6 @@ export async function addCustomer(data, frontImage = null, backImage = null) {
// ─── Auth: Login ───
export async function loginWithEmail(credential, password) {
console.log('[Auth] Login with email:', credential);
return authFetch('/Auth/LogInWithEmail', {
credential,
password,
@ -781,7 +898,6 @@ export async function loginWithEmail(credential, password) {
}
export async function loginWithPhone(credential, password) {
console.log('[Auth] Login with phone:', credential);
return authFetch('/Auth/LogInWithPhoneNumber', {
credential,
password,
@ -793,23 +909,19 @@ export async function loginWithPhone(credential, password) {
// ─── Auth: OTP ───
export async function sendEmailOTP() {
console.log('[Auth] Sending email OTP...');
return apiFetch('/Auth/SendEmailOTP', { method: 'POST' });
}
export async function sendPhoneOTP() {
console.log('[Auth] Sending phone OTP...');
return apiFetch('/Auth/SendPhoneNumberOTP', { method: 'POST' });
}
export async function verifyEmail(code) {
console.log('[Auth] Verifying email with code:', code);
const token = AuthService.getToken();
return authFetch(`/Auth/VerifyEmail?code=${encodeURIComponent(code)}`, {}, token);
}
export async function verifyPhone(code) {
console.log('[Auth] Verifying phone with code:', code);
const token = AuthService.getToken();
return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {}, token);
}
@ -847,77 +959,203 @@ export async function getUserNotifications() {
export async function confirmDepositPayment(bookingId) {
return apiFetch('/Reservations/ConfirmDepositPayment', {
method: 'POST',
body: JSON.stringify({ bookingId }),
body: { bookingId },
});
}
export async function adminConfirmDeposit(reservationId, adminId, comment = null) {
const token = AuthService.getToken();
const endpoint = `${API_BASE}/Reservations/AdminConfirmDeposit/admin-confirm-deposit`;
const normalizedComment =
typeof comment === 'string' && comment.trim()
? comment.trim()
: null;
const payload = {
reservationId,
adminId,
comment: normalizedComment,
};
console.log('[API] AdminConfirmDeposit request', {
method: 'PUT',
endpoint,
payload,
adminIdSource: 'jwt-user-id',
hasToken: Boolean(token),
tokenPreview: token ? `${token.slice(0, 18)}...${token.slice(-8)}` : null,
});
const res = await fetch(endpoint, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
body: JSON.stringify(payload),
});
const text = await res.text();
let data = null;
console.log('[API] AdminConfirmDeposit raw response', {
status: res.status,
ok: res.ok,
endpoint,
rawText: text,
});
try {
data = text ? JSON.parse(text) : null;
if (data && typeof data === 'object' && 'data' in data) {
data = data.data;
}
} catch {
data = text;
}
const message = typeof data === 'object' && data?.message ? data.message : null;
console.log('[API] AdminConfirmDeposit parsed response', {
status: res.status,
ok: res.ok,
message,
data,
});
return { status: res.status, data, ok: res.ok, message };
}
export async function updateBookingStatus(bookingId, status) {
return apiFetch('/Reservations/UpdateStatus', {
method: 'PUT',
body: JSON.stringify({ bookingId, status }),
body: { bookingId, status },
});
}
// ─── Owner / Reservations ───
export async function getOwnerReservationRequests() {
return apiFetch('/Reservations/GetOwnerResevationRequests');
}
export async function getOwnerReservationsByStatuses(filterStatuses) {
return apiFetch('/Reservations/GetAllReservationsByStateForOwner', {
method: 'POST',
body: { 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: 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) {
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}/RealEstateAgent/Add`, {
method: 'POST',
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
body: formData,
});
assertNotBlocked(res);
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 || (typeof data === 'string' ? data : null),
};
}
// ─── 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: { 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 sendGeneralReport(subject, reportBody) {
return reportFetch('/Reports/SendGeneralReport', {
subject,
body: reportBody,
});
}
export async function submitReport(subject, body) {
return apiFetch('/Reports/SendGeneralReport', {
method: 'POST',
body: { subject, body },
});
}
export async function submitReservationReport(data) {
return apiFetch('/ReservationReports', {
method: 'POST',
body: data,
});
}
export async function updateReservationReport(id, data) {
return apiFetch(`/ReservationReports/${id}`, {
method: 'PUT',
body: data,
});
}
export async function submitSaleReport(data) {
return apiFetch('/SaleReports', {
method: 'POST',
body: data,
});
}
export async function updateSaleReport(id, data) {
return apiFetch(`/SaleReports/${id}`, {
method: 'PUT',
body: data,
});
}
// ─── Terms (Add) ───
export async function addTerm(name, description) {
return apiFetch('/Terms', {
method: 'POST',
body: { name, description },
});
}