Compare commits
30 Commits
819d5ea802
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bf45a48504 | |||
| eaa4206b0b | |||
| c5d9ad7b70 | |||
| 46945464ae | |||
| 64d422ddd8 | |||
| 224c29bc19 | |||
| f892fc4a4d | |||
| 01ac4f8d6c | |||
| f2724a5cd2 | |||
| bef133ad5b | |||
| a9eb1cc684 | |||
| 13b563e35e | |||
| 5d593d593f | |||
| 51850b85c2 | |||
| 8cacf464d1 | |||
| 91de3d47b7 | |||
| 71b1a71904 | |||
| 34da1314d4 | |||
| ce6caf08eb | |||
| 845ba2436a | |||
| 471332b59f | |||
| 53a83494b7 | |||
| 6bc0c8ba27 | |||
| 74dc12171d | |||
| 9fdeadaa61 | |||
| d11f105dfc | |||
| 8d4ac3ddd6 | |||
| b4196c340d | |||
| ddf5367f92 | |||
| 417b6cb393 |
@ -7,7 +7,7 @@ import Image from "next/image";
|
|||||||
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
||||||
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
||||||
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
|
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
|
||||||
import FloatingSidebar from '@/app/components/FloatingSidebar';
|
import BottomNav from './components/BottomNav';
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
LogIn,
|
LogIn,
|
||||||
@ -25,7 +25,6 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
Camera,
|
Camera,
|
||||||
Shield,
|
|
||||||
Bell,
|
Bell,
|
||||||
Home,
|
Home,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@ -34,7 +33,6 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Clock,
|
Clock,
|
||||||
Users,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Star,
|
Star,
|
||||||
FileText,
|
FileText,
|
||||||
@ -80,9 +78,7 @@ export default function ClientLayout({ children }) {
|
|||||||
name: authUser.name || authUser.email,
|
name: authUser.name || authUser.email,
|
||||||
email: authUser.email,
|
email: authUser.email,
|
||||||
phone: authUser.phone,
|
phone: authUser.phone,
|
||||||
role: AuthService.isAdmin() ? UserRole.ADMIN
|
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
|
||||||
: AuthService.isOwner() ? UserRole.OWNER
|
|
||||||
: UserRole.CUSTOMER,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@ -130,6 +126,7 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
const isAuthPage = [
|
const isAuthPage = [
|
||||||
"/login",
|
"/login",
|
||||||
|
"/blocked",
|
||||||
"/register",
|
"/register",
|
||||||
"/forgot-password",
|
"/forgot-password",
|
||||||
"/auth/choose-role",
|
"/auth/choose-role",
|
||||||
@ -138,7 +135,6 @@ export default function ClientLayout({ children }) {
|
|||||||
const isProfilePage = pathname === "/profile";
|
const isProfilePage = pathname === "/profile";
|
||||||
|
|
||||||
const isOwner = user?.role === UserRole.OWNER;
|
const isOwner = user?.role === UserRole.OWNER;
|
||||||
const isAdmin = user?.role === UserRole.ADMIN;
|
|
||||||
const isCustomer = user?.role === UserRole.CUSTOMER;
|
const isCustomer = user?.role === UserRole.CUSTOMER;
|
||||||
const isAuthenticated = !!user;
|
const isAuthenticated = !!user;
|
||||||
|
|
||||||
@ -162,7 +158,7 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
return (
|
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">
|
<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 className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div
|
||||||
@ -234,14 +230,6 @@ export default function ClientLayout({ children }) {
|
|||||||
<NavLink href="/">الرئيسية</NavLink>
|
<NavLink href="/">الرئيسية</NavLink>
|
||||||
<NavLink href="/properties">عقاراتنا</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 && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
@ -500,82 +488,6 @@ export default function ClientLayout({ children }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{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"
|
|
||||||
className="flex items-center gap-3 px-4 py-3 text-gray-700 hover:bg-amber-50 rounded-lg transition-colors"
|
|
||||||
onClick={() => setShowUserMenu(false)}
|
|
||||||
>
|
|
||||||
<DollarSign className="w-5 h-5 text-amber-500" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">دفتر الحسابات</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
إدارة المعاملات المالية
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCustomer && (
|
{isCustomer && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-gray-100 my-2"></div>
|
<div className="border-t border-gray-100 my-2"></div>
|
||||||
@ -730,15 +642,6 @@ export default function ClientLayout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 my-2"></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 && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
@ -806,18 +709,21 @@ export default function ClientLayout({ children }) {
|
|||||||
</nav>
|
</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>
|
<NotificationsProvider>
|
||||||
<FavoritesProvider>
|
<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}
|
{children}
|
||||||
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
|
|
||||||
</FavoritesProvider>
|
|
||||||
</NotificationsProvider>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{!isAuthPage && !isProfilePage && (
|
{isAuthenticated && !isAuthPage && (
|
||||||
|
<BottomNav isOwner={isOwner} />
|
||||||
|
)}
|
||||||
|
</FavoritesProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
|
|
||||||
|
{pathname === "/" && (
|
||||||
<footer className="bg-gray-900 text-white py-12">
|
<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 className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div
|
<div
|
||||||
@ -868,16 +774,6 @@ export default function ClientLayout({ children }) {
|
|||||||
{t("ourProducts")}
|
{t("ourProducts")}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{isAdmin && (
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="/admin"
|
|
||||||
className="text-gray-400 hover:text-white transition-colors block py-1"
|
|
||||||
>
|
|
||||||
الإدارة
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
166
app/blocked/page.js
Normal file
166
app/blocked/page.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
app/components/BottomNav.js
Normal file
58
app/components/BottomNav.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard } 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: "/payments", label: "المدفوعات", icon: CreditCard },
|
||||||
|
{ 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,12 +3,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Heart, Bell, CreditCard, Shield, UserPlus, Settings } from 'lucide-react';
|
import { Heart, Bell, CreditCard, Settings } from 'lucide-react';
|
||||||
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
||||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
|
|
||||||
export default function FloatingSidebar({ isRTL, isAdmin }) {
|
export default function FloatingSidebar({ isRTL }) {
|
||||||
const { favorites } = useFavorites();
|
const { favorites } = useFavorites();
|
||||||
const { unreadCount } = useNotifications();
|
const { unreadCount } = useNotifications();
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null);
|
||||||
@ -62,46 +62,6 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
|
|||||||
animate="animate"
|
animate="animate"
|
||||||
>
|
>
|
||||||
<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">
|
<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">
|
||||||
{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-14 h-14 rounded-xl bg-amber-50 border border-amber-200 text-amber-600 hover:bg-amber-100 transition-colors"
|
|
||||||
>
|
|
||||||
<UserPlus className="w-7 h-7" />
|
|
||||||
</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-14 h-14 rounded-xl bg-slate-50 border border-slate-200 text-slate-700 hover:bg-slate-100 transition-colors"
|
|
||||||
>
|
|
||||||
<Shield className="w-7 h-7" />
|
|
||||||
</Link>
|
|
||||||
{renderTooltip('editPrivacy', 'تعديل سياسة الخصوصية')}
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative group"
|
className="relative group"
|
||||||
variants={buttonVariants}
|
variants={buttonVariants}
|
||||||
@ -192,8 +152,6 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
|
|||||||
</Link>
|
</Link>
|
||||||
{renderTooltip('settings', 'الإعدادات')}
|
{renderTooltip('settings', 'الإعدادات')}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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}m²
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -67,9 +67,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
|
|||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
||||||
setShowLoginDialog(true);
|
setShowLoginDialog(true);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
handleSearch();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
@ -139,6 +137,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{activeTab === 'rent' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
<label className="block text-sm font-medium text-white mb-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@ -163,6 +162,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-white mb-2">
|
<label className="block text-sm font-medium text-white mb-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@ -234,6 +234,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'rent' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
|
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
|
||||||
<select
|
<select
|
||||||
@ -255,8 +256,9 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="md:col-span-2 flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5">
|
<div className={`${activeTab === 'rent' ? 'md:col-span-2' : 'md:col-span-3'} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
|
||||||
<label className="mt-4 flex items-center gap-3 text-white text-sm">
|
<label className="mt-4 flex items-center gap-3 text-white text-sm">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@ -1,38 +1,29 @@
|
|||||||
/**
|
|
||||||
* BookingStatus Enum
|
|
||||||
* Backend values are strings
|
|
||||||
* Used in: Reservation workflow
|
|
||||||
*/
|
|
||||||
const BookingStatus = Object.freeze({
|
const BookingStatus = Object.freeze({
|
||||||
PENDING: 'pending',
|
PENDING: 'pending',
|
||||||
OWNER_APPROVED: 'owner_approved',
|
ownerConfirmed: 'ownerConfirmed',
|
||||||
ADMIN_APPROVED: 'admin_approved',
|
depositPaid: 'depositPaid',
|
||||||
ACTIVE: 'active',
|
depositConfirmed: 'depositConfirmed',
|
||||||
COMPLETED: 'completed',
|
completed: 'completed',
|
||||||
REJECTED: 'rejected',
|
cancelled: 'cancelled',
|
||||||
CANCELLED: 'cancelled',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map status → Arabic label
|
|
||||||
const BookingStatusLabels = Object.freeze({
|
const BookingStatusLabels = Object.freeze({
|
||||||
[BookingStatus.PENDING]: 'بانتظار الموافقة',
|
[BookingStatus.PENDING]: 'قيد الانتظار',
|
||||||
[BookingStatus.OWNER_APPROVED]: 'موافقة المالك',
|
[BookingStatus.ownerConfirmed]: 'مؤكد من المالك',
|
||||||
[BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة',
|
[BookingStatus.depositPaid]: 'تم دفع السلفة',
|
||||||
[BookingStatus.ACTIVE]: 'إيجار نشط',
|
[BookingStatus.depositConfirmed]: 'تم تأكيد الدفع',
|
||||||
[BookingStatus.COMPLETED]: 'منتهي',
|
[BookingStatus.completed]: 'منتهي',
|
||||||
[BookingStatus.REJECTED]: 'مرفوض',
|
[BookingStatus.cancelled]: 'ملغي',
|
||||||
[BookingStatus.CANCELLED]: 'ملغي',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map status → color class (Tailwind bg)
|
|
||||||
const BookingStatusColors = Object.freeze({
|
const BookingStatusColors = Object.freeze({
|
||||||
[BookingStatus.PENDING]: 'yellow',
|
[BookingStatus.PENDING]: 'yellow',
|
||||||
[BookingStatus.OWNER_APPROVED]: 'blue',
|
[BookingStatus.ownerConfirmed]: 'blue',
|
||||||
[BookingStatus.ADMIN_APPROVED]: 'green',
|
[BookingStatus.depositPaid]: 'orange',
|
||||||
[BookingStatus.ACTIVE]: 'purple',
|
[BookingStatus.depositConfirmed]: 'green',
|
||||||
[BookingStatus.COMPLETED]: 'gray',
|
[BookingStatus.completed]: 'teal',
|
||||||
[BookingStatus.REJECTED]: 'red',
|
[BookingStatus.cancelled]: 'red',
|
||||||
[BookingStatus.CANCELLED]: 'red',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export { BookingStatus, BookingStatusLabels, BookingStatusColors };
|
export { BookingStatus, BookingStatusLabels, BookingStatusColors };
|
||||||
|
|||||||
@ -1,23 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* BuildingType Enum
|
* BuildingType Enum
|
||||||
* Backend values are numeric (0-8)
|
* Backend values are numeric
|
||||||
* Used in: PropertyInformation.buildingType
|
* Used in: PropertyInformation.buildingType
|
||||||
*/
|
*/
|
||||||
const BuildingType = Object.freeze({
|
const BuildingType = Object.freeze({
|
||||||
APARTMENT: 0,
|
APARTMENT: 0,
|
||||||
VILLA: 1,
|
VILLA: 1,
|
||||||
SWEET: 2,
|
HOUSE: 2,
|
||||||
ROOM: 3,
|
SWEET: 3,
|
||||||
STUDIO: 4,
|
ROOM: 4,
|
||||||
OFFICE: 5,
|
STUDIO: 5,
|
||||||
FARMS: 6,
|
OFFICE: 6,
|
||||||
SHOP: 7,
|
FARMS: 7,
|
||||||
WAREHOUSE: 8,
|
SHOP: 8,
|
||||||
|
WAREHOUSE: 9,
|
||||||
});
|
});
|
||||||
|
|
||||||
const BuildingTypeLabels = Object.freeze({
|
const BuildingTypeLabels = Object.freeze({
|
||||||
[BuildingType.APARTMENT]: 'شقة',
|
[BuildingType.APARTMENT]: 'شقة',
|
||||||
[BuildingType.VILLA]: 'فيلا',
|
[BuildingType.VILLA]: 'فيلا',
|
||||||
|
[BuildingType.HOUSE]: 'بيت',
|
||||||
[BuildingType.SWEET]: 'سويت',
|
[BuildingType.SWEET]: 'سويت',
|
||||||
[BuildingType.ROOM]: 'غرفة',
|
[BuildingType.ROOM]: 'غرفة',
|
||||||
[BuildingType.STUDIO]: 'استوديو',
|
[BuildingType.STUDIO]: 'استوديو',
|
||||||
@ -30,6 +32,7 @@ const BuildingTypeLabels = Object.freeze({
|
|||||||
const BuildingTypeKeys = Object.freeze({
|
const BuildingTypeKeys = Object.freeze({
|
||||||
[BuildingType.APARTMENT]: 'apartment',
|
[BuildingType.APARTMENT]: 'apartment',
|
||||||
[BuildingType.VILLA]: 'villa',
|
[BuildingType.VILLA]: 'villa',
|
||||||
|
[BuildingType.HOUSE]: 'house',
|
||||||
[BuildingType.SWEET]: 'sweet',
|
[BuildingType.SWEET]: 'sweet',
|
||||||
[BuildingType.ROOM]: 'room',
|
[BuildingType.ROOM]: 'room',
|
||||||
[BuildingType.STUDIO]: 'studio',
|
[BuildingType.STUDIO]: 'studio',
|
||||||
@ -42,7 +45,9 @@ const BuildingTypeKeys = Object.freeze({
|
|||||||
const BuildingTypeByKey = Object.freeze({
|
const BuildingTypeByKey = Object.freeze({
|
||||||
apartment: BuildingType.APARTMENT,
|
apartment: BuildingType.APARTMENT,
|
||||||
villa: BuildingType.VILLA,
|
villa: BuildingType.VILLA,
|
||||||
|
house: BuildingType.HOUSE,
|
||||||
sweet: BuildingType.SWEET,
|
sweet: BuildingType.SWEET,
|
||||||
|
suite: BuildingType.SWEET,
|
||||||
room: BuildingType.ROOM,
|
room: BuildingType.ROOM,
|
||||||
studio: BuildingType.STUDIO,
|
studio: BuildingType.STUDIO,
|
||||||
office: BuildingType.OFFICE,
|
office: BuildingType.OFFICE,
|
||||||
|
|||||||
@ -7,21 +7,18 @@ const UserRole = Object.freeze({
|
|||||||
GUEST: 'guest',
|
GUEST: 'guest',
|
||||||
CUSTOMER: 'customer',
|
CUSTOMER: 'customer',
|
||||||
OWNER: 'owner',
|
OWNER: 'owner',
|
||||||
ADMIN: 'admin',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserRoleLabels = Object.freeze({
|
const UserRoleLabels = Object.freeze({
|
||||||
[UserRole.GUEST]: 'زائر',
|
[UserRole.GUEST]: 'زائر',
|
||||||
[UserRole.CUSTOMER]: 'مستأجر',
|
[UserRole.CUSTOMER]: 'مستأجر',
|
||||||
[UserRole.OWNER]: 'مالك عقار',
|
[UserRole.OWNER]: 'مالك عقار',
|
||||||
[UserRole.ADMIN]: 'مدير النظام',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserRoleColors = Object.freeze({
|
const UserRoleColors = Object.freeze({
|
||||||
[UserRole.GUEST]: 'gray',
|
[UserRole.GUEST]: 'gray',
|
||||||
[UserRole.CUSTOMER]: 'blue',
|
[UserRole.CUSTOMER]: 'blue',
|
||||||
[UserRole.OWNER]: 'amber',
|
[UserRole.OWNER]: 'amber',
|
||||||
[UserRole.ADMIN]: 'red',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export { UserRole, UserRoleLabels, UserRoleColors };
|
export { UserRole, UserRoleLabels, UserRoleColors };
|
||||||
|
|||||||
@ -12,14 +12,14 @@ import AuthService from '@/app/services/AuthService';
|
|||||||
export default function FavoritesPage() {
|
export default function FavoritesPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
|
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (AuthService.isAdmin()) {
|
// Admin check removed
|
||||||
router.push('/');
|
// if (AuthService.isAdmin()) {
|
||||||
return;
|
// router.push('/');
|
||||||
}
|
// return;
|
||||||
setIsAdmin(AuthService.isAdmin());
|
// }
|
||||||
|
// setIsAdmin(AuthService.isAdmin());
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const formatCurrency = (amount) => {
|
const formatCurrency = (amount) => {
|
||||||
|
|||||||
@ -126,11 +126,7 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRole = AuthService.isAdmin()
|
const userRole = AuthService.isOwner() ? "owner" : "customer";
|
||||||
? "admin"
|
|
||||||
: AuthService.isOwner()
|
|
||||||
? "owner"
|
|
||||||
: "customer";
|
|
||||||
console.log("[Login] User role:", userRole);
|
console.log("[Login] User role:", userRole);
|
||||||
|
|
||||||
setIsSuccess(true);
|
setIsSuccess(true);
|
||||||
@ -139,11 +135,7 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (userRole === "admin") {
|
|
||||||
router.push("/admin");
|
|
||||||
} else {
|
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else if (result.status === 206) {
|
} else if (result.status === 206) {
|
||||||
console.log("[Login] 206 — OTP required");
|
console.log("[Login] 206 — OTP required");
|
||||||
|
|||||||
@ -59,6 +59,8 @@ import {
|
|||||||
getMySaleListings,
|
getMySaleListings,
|
||||||
editRentProperty,
|
editRentProperty,
|
||||||
editSaleProperty,
|
editSaleProperty,
|
||||||
|
updateRentPropertyStatus,
|
||||||
|
updateSalePropertyStatus,
|
||||||
} from "../../utils/api";
|
} from "../../utils/api";
|
||||||
|
|
||||||
const DeleteConfirmationModal = ({
|
const DeleteConfirmationModal = ({
|
||||||
@ -117,6 +119,86 @@ const DeleteConfirmationModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DeactivateConfirmationModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
propertyTitle,
|
||||||
|
isActivating,
|
||||||
|
}) => {
|
||||||
|
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-amber-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<AlertCircle className="w-8 h-8 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900">
|
||||||
|
{isActivating ? "تأكيد التنشيط" : "تأكيد إلغاء التنشيط"}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
العقار:{" "}
|
||||||
|
<span className="font-bold text-gray-700">"{propertyTitle}"</span>
|
||||||
|
</p>
|
||||||
|
{!isActivating ? (
|
||||||
|
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-xl">
|
||||||
|
<p className="text-sm text-red-700 font-medium">
|
||||||
|
⚠️ تحذير
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
إذا قمت بإلغاء التنشيط، سيصبح هذا العقار غير متاح للحجز أو الشراء.
|
||||||
|
لا يمكن للعملاء رؤيته أو حجزه حتى تقوم بتنشيطه مرة أخرى.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-xl">
|
||||||
|
<p className="text-sm text-green-700 font-medium">
|
||||||
|
✅ تنشيط العقار
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-green-600 mt-1">
|
||||||
|
سيصبح العقار متاحاً مرة أخرى للحجز أو الشراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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 py-3 rounded-xl font-medium transition-colors ${
|
||||||
|
isActivating
|
||||||
|
? "bg-green-600 text-white hover:bg-green-700"
|
||||||
|
: "bg-red-600 text-white hover:bg-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isActivating ? "نعم، فعّل" : "نعم، ألغي التنشيط"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const serviceLabels = {
|
const serviceLabels = {
|
||||||
Electricity: "كهرباء",
|
Electricity: "كهرباء",
|
||||||
Internet: "إنترنت",
|
Internet: "إنترنت",
|
||||||
@ -1074,6 +1156,11 @@ export default function OwnerPropertiesPage() {
|
|||||||
});
|
});
|
||||||
const [viewModal, setViewModal] = useState({ isOpen: false, property: null });
|
const [viewModal, setViewModal] = useState({ isOpen: false, property: null });
|
||||||
const [editModal, setEditModal] = useState({ isOpen: false, property: null });
|
const [editModal, setEditModal] = useState({ isOpen: false, property: null });
|
||||||
|
const [deactivateModal, setDeactivateModal] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
property: null,
|
||||||
|
isActivating: false,
|
||||||
|
});
|
||||||
|
|
||||||
const filteredProperties = properties.filter((p) => p.purpose === activeTab);
|
const filteredProperties = properties.filter((p) => p.purpose === activeTab);
|
||||||
const rentCount = properties.filter((p) => p.purpose === "rent").length;
|
const rentCount = properties.filter((p) => p.purpose === "rent").length;
|
||||||
@ -1511,11 +1598,44 @@ export default function OwnerPropertiesPage() {
|
|||||||
toast.success('تم تحديث العقار بنجاح');
|
toast.success('تم تحديث العقار بنجاح');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OwnerProperties] Edit failed:', err);
|
console.error('[OwnerProperties] Edit failed:', err);
|
||||||
toast.error('فشل تحديث العقار');
|
toast.error("فشل تحديث العقار");
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleActivation = async () => {
|
||||||
|
const prop = deactivateModal.property;
|
||||||
|
if (!prop) return;
|
||||||
|
|
||||||
|
const willActivate = deactivateModal.isActivating;
|
||||||
|
const newStatus = willActivate ? "available" : "notAvailable";
|
||||||
|
const statusCode = willActivate ? 0 : 1;
|
||||||
|
|
||||||
|
setDeactivateModal({ isOpen: false, property: null, isActivating: false });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (prop.purpose === "rent") {
|
||||||
|
await updateRentPropertyStatus(prop.id, statusCode);
|
||||||
|
} else {
|
||||||
|
await updateSalePropertyStatus(prop.id, statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProperties = properties.map((p) =>
|
||||||
|
p.id === prop.id ? { ...p, status: newStatus } : p,
|
||||||
|
);
|
||||||
|
setProperties(newProperties);
|
||||||
|
localStorage.setItem("ownerProperties", JSON.stringify(newProperties));
|
||||||
|
toast.success(
|
||||||
|
willActivate
|
||||||
|
? "تم تنشيط العقار بنجاح"
|
||||||
|
: "تم إلغاء تنشيط العقار بنجاح",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[OwnerProperties] Toggle status failed:", err);
|
||||||
|
toast.error("فشل تحديث حالة العقار");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
initial: { opacity: 0, y: 20 },
|
initial: { opacity: 0, y: 20 },
|
||||||
animate: { opacity: 1, y: 0 },
|
animate: { opacity: 1, y: 0 },
|
||||||
@ -1557,6 +1677,16 @@ export default function OwnerPropertiesPage() {
|
|||||||
onSave={handleSaveEdit}
|
onSave={handleSaveEdit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeactivateConfirmationModal
|
||||||
|
isOpen={deactivateModal.isOpen}
|
||||||
|
onClose={() =>
|
||||||
|
setDeactivateModal({ isOpen: false, property: null, isActivating: false })
|
||||||
|
}
|
||||||
|
onConfirm={handleToggleActivation}
|
||||||
|
propertyTitle={deactivateModal.property?.title}
|
||||||
|
isActivating={deactivateModal.isActivating}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
@ -1714,10 +1844,16 @@ export default function OwnerPropertiesPage() {
|
|||||||
className={`px-2 py-0.5 rounded-md text-xs font-medium shadow-sm backdrop-blur-sm ${
|
className={`px-2 py-0.5 rounded-md text-xs font-medium shadow-sm backdrop-blur-sm ${
|
||||||
property.status === "available"
|
property.status === "available"
|
||||||
? "bg-white/90 text-green-700"
|
? "bg-white/90 text-green-700"
|
||||||
|
: property.status === "notAvailable"
|
||||||
|
? "bg-red-50/90 text-red-700"
|
||||||
: "bg-white/90 text-yellow-700"
|
: "bg-white/90 text-yellow-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{property.status === "available" ? "متاح" : "مؤجر"}
|
{property.status === "notAvailable"
|
||||||
|
? "غير متاح"
|
||||||
|
: property.status === "available"
|
||||||
|
? "متاح"
|
||||||
|
: "مؤجل"}
|
||||||
</span>
|
</span>
|
||||||
{property.purpose === "rent" &&
|
{property.purpose === "rent" &&
|
||||||
property.furnished !== undefined && (
|
property.furnished !== undefined && (
|
||||||
@ -1862,6 +1998,35 @@ export default function OwnerPropertiesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
{property.status === "notAvailable" ? (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setDeactivateModal({
|
||||||
|
isOpen: true,
|
||||||
|
property,
|
||||||
|
isActivating: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="p-1.5 hover:bg-green-50 rounded-lg transition-colors group"
|
||||||
|
title="تنشيط العقار"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-gray-400 group-hover:text-green-600" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setDeactivateModal({
|
||||||
|
isOpen: true,
|
||||||
|
property,
|
||||||
|
isActivating: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="p-1.5 hover:bg-red-50 rounded-lg transition-colors group"
|
||||||
|
title="إلغاء تنشيط العقار"
|
||||||
|
>
|
||||||
|
<XCircle className="w-3.5 h-3.5 text-gray-400 group-hover:text-red-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewModal({ isOpen: true, property })}
|
onClick={() => setViewModal({ isOpen: true, property })}
|
||||||
className="px-3 py-1.5 bg-gray-50 hover:bg-blue-50 border border-gray-200 hover:border-blue-200 rounded-lg transition-all group flex items-center gap-1.5"
|
className="px-3 py-1.5 bg-gray-50 hover:bg-blue-50 border border-gray-200 hover:border-blue-200 rounded-lg transition-all group flex items-center gap-1.5"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,165 @@
|
|||||||
|
// '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';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { CreditCard, Loader2, Home, Calendar, Check, X, Clock } from 'lucide-react';
|
import { CreditCard, Loader2, Home, Calendar, Check, X, Clock, LogIn, Lock } from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '@/app/services/AuthService';
|
import AuthService from '@/app/services/AuthService';
|
||||||
import { getUserReservations, payDeposit } from '@/app/utils/api';
|
import { payDeposit } from '@/app/utils/api';
|
||||||
|
|
||||||
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
|
|
||||||
@ -24,19 +177,60 @@ export default function PaymentsPage() {
|
|||||||
const [reservations, setReservations] = useState([]);
|
const [reservations, setReservations] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [payingId, setPayingId] = useState(null);
|
const [payingId, setPayingId] = useState(null);
|
||||||
|
const [isGuest, setIsGuest] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const getAuthToken = () => {
|
||||||
if (AuthService.isAdmin()) {
|
if (typeof window === 'undefined') return '';
|
||||||
router.push('/');
|
return (
|
||||||
return;
|
AuthService?.getToken?.() ||
|
||||||
}
|
AuthService?.getAccessToken?.() ||
|
||||||
loadReservations();
|
localStorage.getItem('token') ||
|
||||||
}, [router]);
|
localStorage.getItem('accessToken') ||
|
||||||
|
localStorage.getItem('authToken') ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const loadReservations = useCallback(async () => {
|
const loadReservations = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getUserReservations();
|
const token = getAuthToken();
|
||||||
setReservations(Array.isArray(data) ? data : []);
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('فشل تحميل المدفوعات');
|
toast.error('فشل تحميل المدفوعات');
|
||||||
@ -45,6 +239,16 @@ export default function PaymentsPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (AuthService.isGuest()) {
|
||||||
|
setIsGuest(true);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsGuest(false);
|
||||||
|
loadReservations();
|
||||||
|
}, [loadReservations]);
|
||||||
|
|
||||||
const handlePayDeposit = async (reservation) => {
|
const handlePayDeposit = async (reservation) => {
|
||||||
setPayingId(reservation.id);
|
setPayingId(reservation.id);
|
||||||
try {
|
try {
|
||||||
@ -58,7 +262,14 @@ export default function PaymentsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (v) => (v ?? 0).toLocaleString() + ' ل.س';
|
const formatCurrency = (v, sign = 'ل.س') => `${sign} ${Number(v ?? 0).toLocaleString()}`;
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '';
|
||||||
|
const d = new Date(date);
|
||||||
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
|
return d.toLocaleDateString('en-GB');
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -68,6 +279,33 @@ export default function PaymentsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGuest) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir="rtl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center"
|
||||||
|
>
|
||||||
|
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<Lock className="w-10 h-10 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-3">المدفوعات</h2>
|
||||||
|
<p className="text-gray-600 leading-relaxed mb-8">
|
||||||
|
دفعاتك مرتبطة بحاسبك لذلك يرجى تسجيل الدخول أولاً
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200"
|
||||||
|
>
|
||||||
|
<LogIn className="w-5 h-5" />
|
||||||
|
تسجيل الدخول
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
|
const canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,8 +318,11 @@ export default function PaymentsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{reservations.length === 0 ? (
|
{reservations.length === 0 ? (
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
|
||||||
|
>
|
||||||
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
|
||||||
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
||||||
@ -94,42 +335,73 @@ export default function PaymentsPage() {
|
|||||||
const amount = r.depositAmount || r.totalPrice || 0;
|
const amount = r.depositAmount || r.totalPrice || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
key={r.id || i}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="flex items-start gap-3">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0">
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all"
|
||||||
<Home className="w-5 h-5 text-amber-600" />
|
>
|
||||||
</div>
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="font-bold text-gray-900">
|
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
|
||||||
{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}`}>
|
<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.depositPaid ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
||||||
{cfg.label}
|
{cfg.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{canPay(r.status) && (
|
{canPay(r.status) && (
|
||||||
<div className="mt-4 pt-4 border-t border-gray-100 flex justify-end">
|
<div className="pt-2 flex justify-end">
|
||||||
<button onClick={() => handlePayDeposit(r)} disabled={payingId === r.id}
|
<button
|
||||||
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">
|
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 ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
|
||||||
{payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
{payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,104 +1,216 @@
|
|||||||
|
// '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';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { useEffect, useState } from 'react';
|
||||||
import { Shield, Lock, Eye, Database, RefreshCw, Trash2, CheckCircle } from 'lucide-react';
|
import { Languages, Loader2, Shield } from 'lucide-react';
|
||||||
|
|
||||||
const sections = [
|
const API_BASE = 'http://45.93.137.91/api';
|
||||||
{
|
|
||||||
title: 'المقدمة',
|
const ENDPOINTS = {
|
||||||
icon: Shield,
|
ar: '/Configuration/GetARPrivacyPolicy',
|
||||||
content:
|
en: '/Configuration/GetENPrivacyPolicy',
|
||||||
'نحن في 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() {
|
export default function PrivacyPage() {
|
||||||
return (
|
const [language, setLanguage] = useState('ar');
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
const [policyText, setPolicyText] = useState('');
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
const [loading, setLoading] = useState(false);
|
||||||
<motion.div
|
const [error, setError] = useState('');
|
||||||
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">
|
useEffect(() => {
|
||||||
{sections.map((section, index) => {
|
const controller = new AbortController();
|
||||||
const Icon = section.icon;
|
|
||||||
return (
|
const loadPolicy = async () => {
|
||||||
<motion.div
|
try {
|
||||||
key={index}
|
setLoading(true);
|
||||||
initial={{ opacity: 0, y: 20 }}
|
setError('');
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.1 }}
|
const response = await fetch(`${API_BASE}${ENDPOINTS[language]}`, {
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
method: 'GET',
|
||||||
>
|
cache: 'no-store',
|
||||||
<div className="flex items-start gap-4">
|
signal: controller.signal,
|
||||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center shrink-0">
|
headers: {
|
||||||
<Icon className="w-6 h-6 text-amber-600" />
|
Accept: 'text/plain',
|
||||||
</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>
|
if (!response.ok) {
|
||||||
</div>
|
throw new Error(`HTTP ${response.status}`);
|
||||||
</div>
|
}
|
||||||
</motion.div>
|
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<button
|
||||||
initial={{ opacity: 0 }}
|
type="button"
|
||||||
animate={{ opacity: 1 }}
|
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
|
||||||
transition={{ delay: 0.6 }}
|
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"
|
||||||
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" />
|
<Languages className="h-4 w-4" />
|
||||||
<div>
|
{language === 'ar' ? 'English' : 'العربية'}
|
||||||
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
|
</button>
|
||||||
<p className="text-amber-700">
|
</div>
|
||||||
تم آخر تحديث لسياسة الخصوصية في 1 مايو 2026. للمزيد من المعلومات أو الاستفسارات، يرجى التواصل مع فريق الدعم.
|
|
||||||
</p>
|
{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>
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,572 @@
|
|||||||
|
// // app/register/owner/page.js
|
||||||
|
|
||||||
|
// 'use client';
|
||||||
|
|
||||||
|
// import { useState, useRef, useMemo } from 'react';
|
||||||
|
// import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
// import { useRouter } from 'next/navigation';
|
||||||
|
// import Link from 'next/link';
|
||||||
|
// import Image from 'next/image';
|
||||||
|
// import {
|
||||||
|
// User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
|
||||||
|
// Camera, X, CheckCircle, XCircle, ArrowLeft, Building,
|
||||||
|
// Loader2, Shield, KeyRound, FileText
|
||||||
|
// } from 'lucide-react';
|
||||||
|
// import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
// import { addOwner, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
|
||||||
|
// import AuthService from '../../services/AuthService';
|
||||||
|
// import { OwnerType, OwnerTypeLabels } from '../../enums';
|
||||||
|
|
||||||
|
// export default function OwnerRegisterPage() {
|
||||||
|
// const router = useRouter();
|
||||||
|
// const [step, setStep] = useState(1);
|
||||||
|
// const [showOtpModal, setShowOtpModal] = useState(false);
|
||||||
|
// const [showPassword, setShowPassword] = useState(false);
|
||||||
|
// const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
// const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// const [formData, setFormData] = useState({
|
||||||
|
// firstName: '',
|
||||||
|
// lastName: '',
|
||||||
|
// email: '',
|
||||||
|
// phone: '',
|
||||||
|
// whatsapp: '',
|
||||||
|
// phone2: '',
|
||||||
|
// nationalNumber: '',
|
||||||
|
// password: '',
|
||||||
|
// confirmPassword: '',
|
||||||
|
// ownerType: OwnerType.PERSON,
|
||||||
|
// agreeTerms: false
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const [idImages, setIdImages] = useState({ front: null, back: null, license: null });
|
||||||
|
// const [idImagePreviews, setIdImagePreviews] = useState({ front: '', back: '', license: '' });
|
||||||
|
// const [otpCode, setOtpCode] = useState('');
|
||||||
|
// const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
// const fileInputFrontRef = useRef(null);
|
||||||
|
// const fileInputBackRef = useRef(null);
|
||||||
|
// const fileInputLicenseRef = useRef(null);
|
||||||
|
|
||||||
|
// const isCompany = Number(formData.ownerType) === OwnerType.REAL_ESTATE_AGENCY;
|
||||||
|
|
||||||
|
// const handleImageUpload = (side, file) => {
|
||||||
|
// if (!file) return;
|
||||||
|
// if (!file.type.startsWith('image/')) {
|
||||||
|
// toast.error('الرجاء اختيار صورة صالحة');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (file.size > 5 * 1024 * 1024) {
|
||||||
|
// toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const reader = new FileReader();
|
||||||
|
// reader.onloadend = () => {
|
||||||
|
// setIdImagePreviews(prev => ({ ...prev, [side]: reader.result }));
|
||||||
|
// };
|
||||||
|
// reader.readAsDataURL(file);
|
||||||
|
// setIdImages(prev => ({ ...prev, [side]: file }));
|
||||||
|
// toast.success('تم رفع الصورة بنجاح', { style: { background: '#dcfce7', color: '#166534' } });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
// const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone);
|
||||||
|
|
||||||
|
// const validateStep1 = () => {
|
||||||
|
// const newErrors = {};
|
||||||
|
// if (!formData.firstName) newErrors.firstName = 'الاسم الأول مطلوب';
|
||||||
|
// if (!formData.lastName) newErrors.lastName = 'اسم العائلة مطلوب';
|
||||||
|
// if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب';
|
||||||
|
// else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح';
|
||||||
|
// if (!formData.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;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const validateStep2 = () => {
|
||||||
|
// const newErrors = {};
|
||||||
|
// if (!idImages.front) newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
|
||||||
|
// if (!idImages.back) newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
|
||||||
|
// setErrors(newErrors);
|
||||||
|
// return Object.keys(newErrors).length === 0;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleNextStep = () => {
|
||||||
|
// if (validateStep1()) {
|
||||||
|
// setStep(2);
|
||||||
|
// window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
// } else {
|
||||||
|
// toast.error('يرجى تصحيح الأخطاء في النموذج');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleSubmit = async (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// if (!validateStep2()) {
|
||||||
|
// toast.error('يرجى إكمال جميع الصور المطلوبة');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (!formData.agreeTerms) {
|
||||||
|
// toast.error('يجب الموافقة على الشروط والأحكام');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setIsLoading(true);
|
||||||
|
// const payload = {
|
||||||
|
// firstName: formData.firstName,
|
||||||
|
// lastName: formData.lastName,
|
||||||
|
// email: formData.email,
|
||||||
|
// phoneNumber: formData.phone || '',
|
||||||
|
// whatsAppNumber: formData.whatsapp,
|
||||||
|
// phone: formData.phone2,
|
||||||
|
// nationalNumber: formData.nationalNumber,
|
||||||
|
// password: formData.password,
|
||||||
|
// ownerType: formData.ownerType,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // تمرير صورة الرخصة فقط إذا كان نوع المالك وكالة عقارية
|
||||||
|
// 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);
|
||||||
|
// toast.success(res.message || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { duration: 4000 });
|
||||||
|
|
||||||
|
// const loginRes = await loginWithEmail(formData.email, formData.password);
|
||||||
|
// if (loginRes.status === 206) {
|
||||||
|
// const otpToken = loginRes.data;
|
||||||
|
// if (otpToken) AuthService.addToken(otpToken);
|
||||||
|
// toast(loginRes.message || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
|
||||||
|
// setShowOtpModal(true);
|
||||||
|
// } else if (loginRes.status === 200) {
|
||||||
|
// const loginToken = loginRes.data;
|
||||||
|
// if (loginToken) AuthService.addToken(loginToken);
|
||||||
|
// toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!');
|
||||||
|
// router.push('/');
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// toast.error(res.message || res.data?.message || 'فشل في إنشاء الحساب');
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// toast.error(err.message || 'حدث خطأ أثناء التسجيل');
|
||||||
|
// } finally {
|
||||||
|
// setIsLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleVerifyOTP = async () => {
|
||||||
|
// if (!otpCode || otpCode.length < 4) {
|
||||||
|
// toast.error('يرجى إدخال رمز التحقق');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// setIsLoading(true);
|
||||||
|
// try {
|
||||||
|
// const res = await verifyEmail(otpCode);
|
||||||
|
// if (res.status === 200) {
|
||||||
|
// AuthService.deleteToken();
|
||||||
|
// toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!');
|
||||||
|
// setShowOtpModal(false);
|
||||||
|
// setTimeout(() => router.push('/login'), 1500);
|
||||||
|
// } else {
|
||||||
|
// toast.error(res.message || res.data?.message || 'رمز التحقق غير صحيح');
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// toast.error(err.message || 'حدث خطأ أثناء التحقق');
|
||||||
|
// } finally {
|
||||||
|
// setIsLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleResendOTP = async () => {
|
||||||
|
// setIsLoading(true);
|
||||||
|
// try {
|
||||||
|
// await sendEmailOTP();
|
||||||
|
// toast.success('تم إرسال رمز تحقق جديد');
|
||||||
|
// } catch (err) {
|
||||||
|
// toast.error('فشل في إرسال الرمز');
|
||||||
|
// } finally {
|
||||||
|
// setIsLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const fadeInUp = {
|
||||||
|
// initial: { opacity: 0, y: 20 },
|
||||||
|
// animate: { opacity: 1, y: 0 },
|
||||||
|
// transition: { duration: 0.5 }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const staggerContainer = {
|
||||||
|
// animate: { transition: { staggerChildren: 0.1 } }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const backgroundElements = useMemo(() => {
|
||||||
|
// const circles = [
|
||||||
|
// { style: { top: '20%', right: '20%', width: '256px', height: '256px' }, className: 'bg-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 = Array.from({ length: 10 }).map((_, i) => ({
|
||||||
|
// left: `${5 + i * 10}%`,
|
||||||
|
// top: `${10 + (i * 7) % 80}%`,
|
||||||
|
// size: `${80 + (i % 5) * 15}px`
|
||||||
|
// }));
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// {circles.map((circle, i) => (
|
||||||
|
// <div key={`circle-${i}`} className={`absolute rounded-full ${circle.className}`} style={circle.style} />
|
||||||
|
// ))}
|
||||||
|
// {dots.map((dot, i) => (
|
||||||
|
// <div key={`dot-${i}`} className="absolute rounded-full bg-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">{backgroundElements}</div>
|
||||||
|
|
||||||
|
// <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.5 }}
|
||||||
|
// className="relative z-10 w-full max-w-2xl">
|
||||||
|
// <div className="mb-8">
|
||||||
|
// <div className="flex items-center justify-between mb-4">
|
||||||
|
// <Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
|
||||||
|
// <motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
|
||||||
|
// <span>العودة</span>
|
||||||
|
// </Link>
|
||||||
|
// <span className="text-sm text-gray-400">خطوة {step} من 2</span>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex gap-2">
|
||||||
|
// {[1, 2].map((s) => (
|
||||||
|
// <motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? 'bg-amber-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <motion.div key={step} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.3 }}
|
||||||
|
// className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
|
||||||
|
// <div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
|
||||||
|
// <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }}
|
||||||
|
// className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||||||
|
// <motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
|
||||||
|
// <motion.div animate={{ rotate: [0, 10, -10, 0] }} transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
// className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
|
||||||
|
// <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>
|
||||||
|
// </motion.div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="p-8">
|
||||||
|
// <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">
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">الاسم الأول <span className="text-red-500">*</span></label>
|
||||||
|
// <div className="relative group">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <User className={`w-5 h-5 ${errors.firstName ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type="text" value={formData.firstName}
|
||||||
|
// onChange={(e) => { setFormData({...formData, firstName: e.target.value}); setErrors({...errors, firstName: null}); }}
|
||||||
|
// className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.firstName ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="الاسم الأول" />
|
||||||
|
// </div>
|
||||||
|
// {errors.firstName && <p className="text-red-500 text-sm mt-1">{errors.firstName}</p>}
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">اسم العائلة <span className="text-red-500">*</span></label>
|
||||||
|
// <input type="text" value={formData.lastName}
|
||||||
|
// onChange={(e) => { setFormData({...formData, lastName: e.target.value}); setErrors({...errors, lastName: null}); }}
|
||||||
|
// className={`w-full px-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.lastName ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="اسم العائلة" />
|
||||||
|
// {errors.lastName && <p className="text-red-500 text-sm mt-1">{errors.lastName}</p>}
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">البريد الإلكتروني <span className="text-red-500">*</span></label>
|
||||||
|
// <div className="relative group">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type="email" value={formData.email}
|
||||||
|
// onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }}
|
||||||
|
// className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="أدخل بريدك الإلكتروني" />
|
||||||
|
// </div>
|
||||||
|
// {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف <span className="text-gray-500">(اختياري)</span></label>
|
||||||
|
// <div className="relative group">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
|
||||||
|
// </div>
|
||||||
|
// <input type="tel" value={formData.phone}
|
||||||
|
// onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }}
|
||||||
|
// className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all"
|
||||||
|
// placeholder="أدخل رقم هاتفك (اختياري)" />
|
||||||
|
// </div>
|
||||||
|
// {errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">رقم الواتساب <span className="text-red-500">*</span></label>
|
||||||
|
// <div className="relative group">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <MessageCircle className={`w-5 h-5 ${errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type="tel" value={formData.whatsapp}
|
||||||
|
// onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }}
|
||||||
|
// className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="أدخل رقم الواتساب" />
|
||||||
|
// </div>
|
||||||
|
// {errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف (7 أرقام) <span className="text-red-500">*</span></label>
|
||||||
|
// <div className="relative group">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <Phone className={`w-5 h-5 ${errors.phone2 ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type="tel" value={formData.phone2}
|
||||||
|
// onChange={(e) => { setFormData({...formData, phone2: e.target.value}); setErrors({...errors, phone2: 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.phone2 ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="أدخل رقم الهاتف" maxLength={7} />
|
||||||
|
// </div>
|
||||||
|
// {errors.phone2 && <p className="text-red-500 text-sm mt-1">{errors.phone2}</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">
|
||||||
|
// <User className={`w-5 h-5 ${errors.nationalNumber ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type="text" value={formData.nationalNumber}
|
||||||
|
// onChange={(e) => { setFormData({...formData, nationalNumber: e.target.value}); setErrors({...errors, nationalNumber: null}); }}
|
||||||
|
// className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.nationalNumber ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="أدخل الرقم الوطني" />
|
||||||
|
// </div>
|
||||||
|
// {errors.nationalNumber && <p className="text-red-500 text-sm mt-1">{errors.nationalNumber}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">نوع المالك <span className="text-red-500">*</span></label>
|
||||||
|
// <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>
|
||||||
|
// ))}
|
||||||
|
// </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">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type={showPassword ? "text" : "password"} value={formData.password}
|
||||||
|
// onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }}
|
||||||
|
// className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="أدخل كلمة المرور" />
|
||||||
|
// <button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||||
|
// {showPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// {errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">تأكيد كلمة المرور <span className="text-red-500">*</span></label>
|
||||||
|
// <div className="relative group">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
|
||||||
|
// </div>
|
||||||
|
// <input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword}
|
||||||
|
// onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }}
|
||||||
|
// className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`}
|
||||||
|
// placeholder="أعد إدخال كلمة المرور" />
|
||||||
|
// <button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
||||||
|
// {showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400" /> : <Eye className="w-5 h-5 text-gray-400" />}
|
||||||
|
// </button>
|
||||||
|
// {formData.confirmPassword && (
|
||||||
|
// <div className="absolute inset-y-0 left-12 flex items-center">
|
||||||
|
// {formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// {errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {step === 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 onClick={() => fileInputFrontRef.current?.click()}
|
||||||
|
// className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.front ? 'border-green-500 bg-green-500/10' : errors.front ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
|
||||||
|
// <input ref={fileInputFrontRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('front', e.target.files?.[0])} className="hidden" />
|
||||||
|
// {idImagePreviews.front ? (
|
||||||
|
// <div className="relative">
|
||||||
|
// <Image src={idImagePreviews.front} alt="Front ID" width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||||
|
// <button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, front: null})); setIdImagePreviews(prev => ({...prev, front: ''})); }}
|
||||||
|
// className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
|
||||||
|
// <X className="w-4 h-4 text-white" />
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// ) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">اضغط لرفع الصورة</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • حتى 5MB</p></>)}
|
||||||
|
// </div>
|
||||||
|
// {errors.front && <p className="text-red-500 text-sm mt-1">{errors.front}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp}>
|
||||||
|
// <label className="block text-sm font-medium text-gray-300 mb-2">صورة الهوية - الوجه الخلفي <span className="text-red-500">*</span></label>
|
||||||
|
// <div onClick={() => fileInputBackRef.current?.click()}
|
||||||
|
// className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.back ? 'border-green-500 bg-green-500/10' : errors.back ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
|
||||||
|
// <input ref={fileInputBackRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('back', e.target.files?.[0])} className="hidden" />
|
||||||
|
// {idImagePreviews.back ? (
|
||||||
|
// <div className="relative">
|
||||||
|
// <Image src={idImagePreviews.back} alt="Back ID" width={200} height={120} className="mx-auto rounded-lg object-cover" />
|
||||||
|
// <button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, back: null})); setIdImagePreviews(prev => ({...prev, back: ''})); }}
|
||||||
|
// className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
|
||||||
|
// <X className="w-4 h-4 text-white" />
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// ) : (<><Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /><p className="text-gray-400">اضغط لرفع الصورة</p><p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG • حتى 5MB</p></>)}
|
||||||
|
// </div>
|
||||||
|
// {errors.back && <p className="text-red-500 text-sm mt-1">{errors.back}</p>}
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <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})}
|
||||||
|
// className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500" required />
|
||||||
|
// <label htmlFor="terms" className="text-sm text-gray-300">
|
||||||
|
// أوافق على <Link href="/terms" className="text-amber-400 hover:text-amber-300">شروط الاستخدام</Link> و <Link href="/privacy" className="text-amber-400 hover:text-amber-300">سياسة الخصوصية</Link>
|
||||||
|
// </label>
|
||||||
|
// </motion.div>
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// <motion.div variants={fadeInUp} className="flex gap-3 pt-4">
|
||||||
|
// {step === 1 ? (
|
||||||
|
// <>
|
||||||
|
// <button type="button" onClick={() => router.push('/auth/choose-role')}
|
||||||
|
// className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">إلغاء</button>
|
||||||
|
// <button type="submit"
|
||||||
|
// className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all">التالي</button>
|
||||||
|
// </>
|
||||||
|
// ) : (
|
||||||
|
// <>
|
||||||
|
// <button type="button" onClick={() => setStep(1)}
|
||||||
|
// className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">السابق</button>
|
||||||
|
// <button type="submit" disabled={isLoading || !formData.agreeTerms}
|
||||||
|
// className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
// {isLoading ? (<div className="flex items-center justify-center gap-2"><Loader2 className="w-5 h-5 animate-spin" /><span>جاري التسجيل...</span></div>) : 'إنشاء حساب'}
|
||||||
|
// </button>
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
// </motion.div>
|
||||||
|
// </motion.form>
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <AnimatePresence>
|
||||||
|
// {showOtpModal && (
|
||||||
|
// <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||||
|
// className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
|
// <motion.div initial={{ scale: 0.9, y: 20 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.9, y: 20 }}
|
||||||
|
// className="bg-gray-900 border border-white/10 rounded-2xl w-full max-w-md p-6 shadow-2xl">
|
||||||
|
// <div className="text-center mb-6">
|
||||||
|
// <div className="w-16 h-16 bg-amber-500/20 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
// <Shield className="w-8 h-8 text-amber-500" />
|
||||||
|
// </div>
|
||||||
|
// <h2 className="text-xl font-bold text-white">التحقق من البريد</h2>
|
||||||
|
// <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">
|
||||||
|
// <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
|
// <KeyRound className="w-5 h-5 text-gray-400" />
|
||||||
|
// </div>
|
||||||
|
// <input type="text" value={otpCode} maxLength={6}
|
||||||
|
// onChange={(e) => setOtpCode(e.target.value)}
|
||||||
|
// className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-white text-center tracking-[0.5em] text-xl"
|
||||||
|
// placeholder="------" />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex gap-3">
|
||||||
|
// <button onClick={handleVerifyOTP} disabled={isLoading || !otpCode}
|
||||||
|
// className="flex-1 bg-gradient-to-r from-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">
|
||||||
|
// إعادة إرسال الرمز
|
||||||
|
// </button>
|
||||||
|
// </motion.div>
|
||||||
|
// </motion.div>
|
||||||
|
// )}
|
||||||
|
// </AnimatePresence>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
// app/register/owner/page.js
|
// app/register/owner/page.js
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
@ -121,12 +690,11 @@ export default function OwnerRegisterPage() {
|
|||||||
firstName: formData.firstName,
|
firstName: formData.firstName,
|
||||||
lastName: formData.lastName,
|
lastName: formData.lastName,
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
phoneNumber: formData.phone || '',
|
|
||||||
whatsAppNumber: formData.whatsapp,
|
|
||||||
phone: formData.phone2,
|
phone: formData.phone2,
|
||||||
|
whatsAppNumber: formData.whatsapp,
|
||||||
nationalNumber: formData.nationalNumber,
|
nationalNumber: formData.nationalNumber,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
ownerType: formData.ownerType,
|
type: formData.ownerType,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -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';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
@ -5,7 +400,7 @@ import { motion } from 'framer-motion';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
|
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
|
||||||
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star,
|
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star, Flag,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '../services/AuthService';
|
import AuthService from '../services/AuthService';
|
||||||
@ -47,6 +442,106 @@ const propImages = (p, r) => {
|
|||||||
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
|
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
|
||||||
const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseTimeSpan(str) {
|
function parseTimeSpan(str) {
|
||||||
if (!str) return 0;
|
if (!str) return 0;
|
||||||
const clean = str.replace(/-/g, '');
|
const clean = str.replace(/-/g, '');
|
||||||
@ -99,7 +594,81 @@ function CountdownTimer({ deadline }) {
|
|||||||
return <span className="text-amber-600 text-sm font-mono font-bold" dir="ltr">{pad(h)}:{pad(m)}:{pad(s)}</span>;
|
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 }) {
|
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 p = r._prop;
|
||||||
const imgs = propImages(p, r);
|
const imgs = propImages(p, r);
|
||||||
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
||||||
@ -114,6 +683,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
|||||||
: null;
|
: null;
|
||||||
const isExpired = deadline ? Date.now() > deadline : false;
|
const isExpired = deadline ? Date.now() > deadline : false;
|
||||||
const isPaying = payingId === r.id;
|
const isPaying = payingId === r.id;
|
||||||
|
const isReporting = reportingId === r.id;
|
||||||
const [showRating, setShowRating] = useState(false);
|
const [showRating, setShowRating] = useState(false);
|
||||||
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
||||||
const [ratingComment, setRatingComment] = useState('');
|
const [ratingComment, setRatingComment] = useState('');
|
||||||
@ -162,6 +732,10 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
|||||||
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||||
</button>}
|
</button>}
|
||||||
</div>
|
</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)}
|
{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">
|
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"/> قيّم هذا العقار
|
<Star className="w-4 h-4"/> قيّم هذا العقار
|
||||||
@ -213,7 +787,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
|
||||||
if (!isOpen || !r) return null;
|
if (!isOpen || !r) return null;
|
||||||
const p = r._prop;
|
const p = r._prop;
|
||||||
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||||
@ -223,6 +797,7 @@ function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
|||||||
: null;
|
: null;
|
||||||
const isExpired = deadline ? Date.now() > deadline : false;
|
const isExpired = deadline ? Date.now() > deadline : false;
|
||||||
const isPaying = payingId === r.id;
|
const isPaying = payingId === r.id;
|
||||||
|
const isReporting = reportingId === r.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
|
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
|
||||||
@ -269,6 +844,10 @@ function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
|||||||
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||||
</button>}
|
</button>}
|
||||||
</div>}
|
</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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -284,6 +863,8 @@ export default function UserReservationsPage() {
|
|||||||
const [filterStatus, setFilterStatus] = useState('all');
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [payingId, setPayingId] = useState(null);
|
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]);
|
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
||||||
|
|
||||||
@ -320,7 +901,10 @@ export default function UserReservationsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let r = reservations;
|
let r = reservations;
|
||||||
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
|
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)); }
|
if (searchTerm) {
|
||||||
|
const q = searchTerm.toLowerCase();
|
||||||
|
r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q));
|
||||||
|
}
|
||||||
setFiltered(r);
|
setFiltered(r);
|
||||||
}, [reservations, filterStatus, searchTerm]);
|
}, [reservations, filterStatus, searchTerm]);
|
||||||
|
|
||||||
@ -345,12 +929,50 @@ export default function UserReservationsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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>;
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
<DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} onPay={handlePay} payingId={payingId} />
|
<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">
|
<div className="container mx-auto px-4">
|
||||||
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
|
<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>
|
<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>
|
||||||
@ -380,7 +1002,17 @@ export default function UserReservationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<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} />)}
|
{filtered.map(r => (
|
||||||
|
<ReservationCard
|
||||||
|
key={r.id}
|
||||||
|
r={r}
|
||||||
|
onViewDetails={setSelected}
|
||||||
|
onPay={handlePay}
|
||||||
|
onReport={openReportDialog}
|
||||||
|
payingId={payingId}
|
||||||
|
reportingId={reportingId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -127,19 +127,11 @@ const AuthService = Object.freeze({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User has Admin role
|
* Authenticated user without Owner role (i.e. customer)
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isAdmin() {
|
|
||||||
return this.getRoles().includes('Admin');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticated user without Owner or Admin role (i.e. customer)
|
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isCustomer() {
|
isCustomer() {
|
||||||
return this.isAuthenticated() && !this.isOwner() && !this.isAdmin();
|
return this.isAuthenticated() && !this.isOwner();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -160,3 +152,4 @@ const AuthService = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default AuthService;
|
export default AuthService;
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,260 @@
|
|||||||
|
// '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';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -31,6 +288,11 @@ export default function SettingsPage() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = 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 = () => {
|
const handleSignOut = () => {
|
||||||
AuthService.deleteToken();
|
AuthService.deleteToken();
|
||||||
toast.success('تم تسجيل الخروج بنجاح');
|
toast.success('تم تسجيل الخروج بنجاح');
|
||||||
@ -49,6 +311,7 @@ export default function SettingsPage() {
|
|||||||
toast.success('تم حذف الحساب بنجاح');
|
toast.success('تم حذف الحساب بنجاح');
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Delete account error:', err);
|
||||||
toast.error(err.message || 'فشل حذف الحساب');
|
toast.error(err.message || 'فشل حذف الحساب');
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
@ -57,6 +320,70 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = [
|
const sections = [
|
||||||
{
|
{
|
||||||
title: 'الحساب',
|
title: 'الحساب',
|
||||||
@ -79,6 +406,7 @@ export default function SettingsPage() {
|
|||||||
{ icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
{ icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
||||||
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
||||||
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
||||||
|
{ icon: AlertTriangle, label: 'إرسال بلاغ عام', desc: 'إرسال مشكلة أو ملاحظة إلى الإدارة', action: () => setShowReportDialog(true) },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -133,12 +461,9 @@ export default function SettingsPage() {
|
|||||||
<div className="divide-y divide-gray-50">
|
<div className="divide-y divide-gray-50">
|
||||||
{section.items.map((item) => {
|
{section.items.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
return (
|
|
||||||
<Link
|
const content = (
|
||||||
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">
|
<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" />
|
<Icon className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
@ -147,6 +472,28 @@ export default function SettingsPage() {
|
|||||||
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronLeft className="w-4 h-4 text-gray-400" />
|
<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>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -242,6 +589,74 @@ export default function SettingsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,62 +1,143 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { FileText, Shield, CheckCircle } from 'lucide-react';
|
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from 'lucide-react';
|
||||||
import { getTerms } from '../utils/api';
|
import { getARTerms, getENTerms } from '../utils/api';
|
||||||
|
|
||||||
const staticTerms = [
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.08 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 24 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const FALLBACK_TERMS = {
|
||||||
|
ar: [
|
||||||
{
|
{
|
||||||
title: 'مقدمة',
|
title: 'مقدمة',
|
||||||
content:
|
description:
|
||||||
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة. تحتفظ المنصة بحق تعديل هذه الشروط في أي وقت مع إشعار المستخدمين.',
|
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'استخدام المنصة',
|
title: 'استخدام المنصة',
|
||||||
content:
|
description:
|
||||||
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني أو مخالف للقوانين السارية. كما يلتزم المستخدم بعدم محاولة الوصول غير المصرح به إلى أي جزء من المنصة أو الخوادم أو الأنظمة المتصلة بها.',
|
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'حقوق ومسؤوليات المالك',
|
title: 'حقوق ومسؤوليات المالك',
|
||||||
content:
|
description:
|
||||||
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر. يلتزم المالك بتحديث معلومات العقار بشكل دوري. المنصة غير مسؤولة عن أي نزاعات تنشأ بين المالك والمستأجر. يجب على المالك الالتزام بجميع القوانين المحلية المتعلقة بتأجير العقارات.',
|
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'حقوق ومسؤوليات المستأجر',
|
title: 'حقوق ومسؤوليات المستأجر',
|
||||||
content:
|
description:
|
||||||
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات. يجب على المستأجر الالتزام بقوانين المنزل ومواعيد تسجيل الوصول والمغادرة. المنصة غير مسؤولة عن أي سلوك غير لائق من قبل المستأجرين.',
|
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'الدفع والعمولات',
|
title: 'الدفع والعمولات',
|
||||||
content:
|
description:
|
||||||
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز. جميع المدفوعات تتم عبر قنوات الدفع الآمنة في المنصة. أي رسوم إلغاء أو استرداد تخضع لسياسة الإلغاء المحددة في كل عقار.',
|
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'خصوصية البيانات',
|
title: 'خصوصية البيانات',
|
||||||
content:
|
description:
|
||||||
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا. نحن لا نشارك معلوماتك مع أطراف ثالثة دون موافقتك، إلا عندما يقتضي القانون ذلك.',
|
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.',
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
en: [
|
||||||
|
{
|
||||||
|
title: 'Introduction',
|
||||||
|
description:
|
||||||
|
'Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Platform Usage',
|
||||||
|
description:
|
||||||
|
'The platform may only be used for lawful purposes. Users must not engage in any illegal activity.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Owner Rights & Responsibilities',
|
||||||
|
description:
|
||||||
|
'Owners are responsible for the accuracy of property information including images, description, price, and availability.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tenant Rights & Responsibilities',
|
||||||
|
description:
|
||||||
|
'Tenants must use the property responsibly and not cause any damage to the property.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Payment & Commissions',
|
||||||
|
description:
|
||||||
|
'The platform charges a commission on each successful booking according to the rate specified at the time of booking.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Data Privacy',
|
||||||
|
description:
|
||||||
|
'We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export default function TermsPage() {
|
export default function TermsPage() {
|
||||||
const [terms, setTerms] = useState(staticTerms);
|
const [terms, setTerms] = useState([]);
|
||||||
|
const [language, setLanguage] = useState('ar');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchTerms() {
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const fetchTerms = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getTerms();
|
setLoading(true);
|
||||||
if (data && Array.isArray(data) && data.length > 0) {
|
setError('');
|
||||||
setTerms(data);
|
|
||||||
|
const fetcher = language === 'ar' ? getARTerms : getENTerms;
|
||||||
|
const data = await fetcher();
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
setTerms(FALLBACK_TERMS[language]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const raw = Array.isArray(data) ? data : data.terms || data.items || data.data || [];
|
||||||
|
|
||||||
|
if (!Array.isArray(raw) || raw.length === 0) {
|
||||||
|
setTerms(FALLBACK_TERMS[language]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = raw.map((item) => ({
|
||||||
|
title: item.title || item.name || '',
|
||||||
|
description: item.description || item.content || item.body || item.text || '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTerms(mapped);
|
||||||
} catch {
|
} catch {
|
||||||
// fall back to static terms
|
setTerms(FALLBACK_TERMS[language]);
|
||||||
}
|
setError('');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchTerms();
|
fetchTerms();
|
||||||
}, []);
|
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
<div
|
||||||
|
dir={language === 'ar' ? 'rtl' : 'ltr'}
|
||||||
|
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
|
||||||
|
>
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
@ -66,33 +147,95 @@ export default function TermsPage() {
|
|||||||
<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">
|
<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" />
|
<FileText className="w-10 h-10 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">شروط الاستخدام</h1>
|
|
||||||
|
<div className="flex items-center justify-center gap-4 mb-4">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900">
|
||||||
|
{language === 'ar' ? 'شروط الاستخدام' : 'Terms of Use'}
|
||||||
|
</h1>
|
||||||
|
<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>
|
||||||
|
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة
|
{language === 'ar'
|
||||||
|
? 'يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة'
|
||||||
|
: 'Please read the following terms of use carefully before using the platform'}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
|
<span>
|
||||||
|
{language === 'ar'
|
||||||
|
? 'جاري تحميل شروط الاستخدام...'
|
||||||
|
: 'Loading terms of use...'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-5 w-5 shrink-0" />
|
||||||
|
<span>{error}</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && terms.length === 0 && !error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="text-center py-16 text-gray-500"
|
||||||
|
>
|
||||||
|
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
|
||||||
|
<p className="text-xl font-medium">
|
||||||
|
{language === 'ar'
|
||||||
|
? 'لا توجد شروط استخدام متاحة حالياً'
|
||||||
|
: 'No terms of use available'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{terms.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
{terms.map((term, index) => (
|
{terms.map((term, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
variants={itemVariants}
|
||||||
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"
|
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="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">
|
<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" />
|
<Shield className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
|
{term.title && (
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
|
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
|
||||||
<p className="text-gray-600 leading-relaxed">{term.content}</p>
|
)}
|
||||||
|
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
|
||||||
|
{term.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -102,9 +245,13 @@ export default function TermsPage() {
|
|||||||
>
|
>
|
||||||
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
|
<p className="font-bold text-amber-800 mb-1">
|
||||||
|
{language === 'ar' ? 'آخر تحديث' : 'Last Updated'}
|
||||||
|
</p>
|
||||||
<p className="text-amber-700">
|
<p className="text-amber-700">
|
||||||
تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.
|
{language === 'ar'
|
||||||
|
? 'تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.'
|
||||||
|
: 'Last updated on May 1, 2026. Please review this page periodically for any changes.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
678
app/utils/api.js
678
app/utils/api.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user