This commit is contained in:
@ -25,7 +25,6 @@ import {
|
||||
Mail,
|
||||
MapPin,
|
||||
Camera,
|
||||
Shield,
|
||||
Bell,
|
||||
Home,
|
||||
ChevronDown,
|
||||
@ -34,7 +33,6 @@ import {
|
||||
TrendingUp,
|
||||
CalendarDays,
|
||||
Clock,
|
||||
Users,
|
||||
DollarSign,
|
||||
Star,
|
||||
FileText,
|
||||
@ -80,9 +78,7 @@ export default function ClientLayout({ children }) {
|
||||
name: authUser.name || authUser.email,
|
||||
email: authUser.email,
|
||||
phone: authUser.phone,
|
||||
role: AuthService.isAdmin() ? UserRole.ADMIN
|
||||
: AuthService.isOwner() ? UserRole.OWNER
|
||||
: UserRole.CUSTOMER,
|
||||
role: AuthService.isOwner() ? UserRole.OWNER : UserRole.CUSTOMER,
|
||||
});
|
||||
} else {
|
||||
setUser(null);
|
||||
@ -138,7 +134,6 @@ export default function ClientLayout({ children }) {
|
||||
const isProfilePage = pathname === "/profile";
|
||||
|
||||
const isOwner = user?.role === UserRole.OWNER;
|
||||
const isAdmin = user?.role === UserRole.ADMIN;
|
||||
const isCustomer = user?.role === UserRole.CUSTOMER;
|
||||
const isAuthenticated = !!user;
|
||||
|
||||
@ -234,14 +229,6 @@ export default function ClientLayout({ children }) {
|
||||
<NavLink href="/">الرئيسية</NavLink>
|
||||
<NavLink href="/properties">عقاراتنا</NavLink>
|
||||
|
||||
{isAdmin && (
|
||||
<NavLink href="/admin">
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
الإدارة
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<>
|
||||
@ -500,82 +487,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 && (
|
||||
<>
|
||||
<div className="border-t border-gray-100 my-2"></div>
|
||||
@ -730,15 +641,6 @@ export default function ClientLayout({ children }) {
|
||||
</div>
|
||||
<div className="border-t border-gray-200 my-2"></div>
|
||||
|
||||
{isAdmin && (
|
||||
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
الإدارة
|
||||
</span>
|
||||
</MobileNavLink>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<>
|
||||
<MobileNavLink
|
||||
@ -871,16 +773,6 @@ export default function ClientLayout({ children }) {
|
||||
{t("ourProducts")}
|
||||
</Link>
|
||||
</li>
|
||||
{isAdmin && (
|
||||
<li>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="text-gray-400 hover:text-white transition-colors block py-1"
|
||||
>
|
||||
الإدارة
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -3,12 +3,12 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
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 { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||
import AuthService from '@/app/services/AuthService';
|
||||
|
||||
export default function FloatingSidebar({ isRTL, isAdmin }) {
|
||||
export default function FloatingSidebar({ isRTL }) {
|
||||
const { favorites } = useFavorites();
|
||||
const { unreadCount } = useNotifications();
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
@ -62,139 +62,97 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
|
||||
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">
|
||||
{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
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('favorites')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/favorites"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<Heart className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
{favorites.length > 0 && (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
|
||||
>
|
||||
{favorites.length}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{renderTooltip('favorites', 'المفضلة')}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('notifications')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/notifications"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<Bell className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
{unreadCount > 0 && (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
|
||||
>
|
||||
{unreadCount}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{renderTooltip('notifications', 'الإشعارات')}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('payments')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/payments"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
</Link>
|
||||
{renderTooltip('payments', 'المدفوعات')}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('settings')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
</Link>
|
||||
{renderTooltip('settings', 'الإعدادات')}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('favorites')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/favorites"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<Heart className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
{favorites.length > 0 && (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
|
||||
>
|
||||
{favorites.length}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{renderTooltip('favorites', 'المفضلة')}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('notifications')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/notifications"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<div className="relative">
|
||||
<Bell className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
{unreadCount > 0 && (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
|
||||
>
|
||||
{unreadCount}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{renderTooltip('notifications', 'الإشعارات')}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('payments')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/payments"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
</Link>
|
||||
{renderTooltip('payments', 'المدفوعات')}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="relative group"
|
||||
variants={buttonVariants}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onMouseEnter={() => showTooltip('settings')}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
|
||||
>
|
||||
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
</Link>
|
||||
{renderTooltip('settings', 'الإعدادات')}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,357 +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>
|
||||
<option value="office">مكتب</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>
|
||||
);
|
||||
}
|
||||
@ -1,38 +1,29 @@
|
||||
/**
|
||||
* BookingStatus Enum
|
||||
* Backend values are strings
|
||||
* Used in: Reservation workflow
|
||||
*/
|
||||
|
||||
const BookingStatus = Object.freeze({
|
||||
PENDING: 'pending',
|
||||
OWNER_APPROVED: 'owner_approved',
|
||||
ADMIN_APPROVED: 'admin_approved',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed',
|
||||
REJECTED: 'rejected',
|
||||
CANCELLED: 'cancelled',
|
||||
ownerConfirmed: 'ownerConfirmed',
|
||||
depositPaid: 'depositPaid',
|
||||
depositConfirmed: 'depositConfirmed',
|
||||
completed: 'completed',
|
||||
cancelled: 'cancelled',
|
||||
});
|
||||
|
||||
// Map status → Arabic label
|
||||
const BookingStatusLabels = Object.freeze({
|
||||
[BookingStatus.PENDING]: 'بانتظار الموافقة',
|
||||
[BookingStatus.OWNER_APPROVED]: 'موافقة المالك',
|
||||
[BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة',
|
||||
[BookingStatus.ACTIVE]: 'إيجار نشط',
|
||||
[BookingStatus.COMPLETED]: 'منتهي',
|
||||
[BookingStatus.REJECTED]: 'مرفوض',
|
||||
[BookingStatus.CANCELLED]: 'ملغي',
|
||||
[BookingStatus.PENDING]: 'قيد الانتظار',
|
||||
[BookingStatus.ownerConfirmed]: 'مؤكد من المالك',
|
||||
[BookingStatus.depositPaid]: 'تم دفع السلفة',
|
||||
[BookingStatus.depositConfirmed]: 'تم تأكيد الدفع',
|
||||
[BookingStatus.completed]: 'منتهي',
|
||||
[BookingStatus.cancelled]: 'ملغي',
|
||||
});
|
||||
|
||||
// Map status → color class (Tailwind bg)
|
||||
const BookingStatusColors = Object.freeze({
|
||||
[BookingStatus.PENDING]: 'yellow',
|
||||
[BookingStatus.OWNER_APPROVED]: 'blue',
|
||||
[BookingStatus.ADMIN_APPROVED]: 'green',
|
||||
[BookingStatus.ACTIVE]: 'purple',
|
||||
[BookingStatus.COMPLETED]: 'gray',
|
||||
[BookingStatus.REJECTED]: 'red',
|
||||
[BookingStatus.CANCELLED]: 'red',
|
||||
[BookingStatus.ownerConfirmed]: 'blue',
|
||||
[BookingStatus.depositPaid]: 'orange',
|
||||
[BookingStatus.depositConfirmed]: 'green',
|
||||
[BookingStatus.completed]: 'teal',
|
||||
[BookingStatus.cancelled]: 'red',
|
||||
});
|
||||
|
||||
export { BookingStatus, BookingStatusLabels, BookingStatusColors };
|
||||
|
||||
@ -7,21 +7,18 @@ const UserRole = Object.freeze({
|
||||
GUEST: 'guest',
|
||||
CUSTOMER: 'customer',
|
||||
OWNER: 'owner',
|
||||
ADMIN: 'admin',
|
||||
});
|
||||
|
||||
const UserRoleLabels = Object.freeze({
|
||||
[UserRole.GUEST]: 'زائر',
|
||||
[UserRole.CUSTOMER]: 'مستأجر',
|
||||
[UserRole.OWNER]: 'مالك عقار',
|
||||
[UserRole.ADMIN]: 'مدير النظام',
|
||||
});
|
||||
|
||||
const UserRoleColors = Object.freeze({
|
||||
[UserRole.GUEST]: 'gray',
|
||||
[UserRole.CUSTOMER]: 'blue',
|
||||
[UserRole.OWNER]: 'amber',
|
||||
[UserRole.ADMIN]: 'red',
|
||||
});
|
||||
|
||||
export { UserRole, UserRoleLabels, UserRoleColors };
|
||||
|
||||
@ -12,14 +12,14 @@ import AuthService from '@/app/services/AuthService';
|
||||
export default function FavoritesPage() {
|
||||
const router = useRouter();
|
||||
const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites();
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (AuthService.isAdmin()) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
setIsAdmin(AuthService.isAdmin());
|
||||
// Admin check removed
|
||||
// if (AuthService.isAdmin()) {
|
||||
// router.push('/');
|
||||
// return;
|
||||
// }
|
||||
// setIsAdmin(AuthService.isAdmin());
|
||||
}, [router]);
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
|
||||
@ -126,11 +126,7 @@ export default function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const userRole = AuthService.isAdmin()
|
||||
? "admin"
|
||||
: AuthService.isOwner()
|
||||
? "owner"
|
||||
: "customer";
|
||||
const userRole = AuthService.isOwner() ? "owner" : "customer";
|
||||
console.log("[Login] User role:", userRole);
|
||||
|
||||
setIsSuccess(true);
|
||||
@ -139,11 +135,7 @@ export default function LoginPage() {
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (userRole === "admin") {
|
||||
router.push("/admin");
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
router.push("/");
|
||||
}, 1500);
|
||||
} else if (result.status === 206) {
|
||||
console.log("[Login] 206 — OTP required");
|
||||
|
||||
@ -26,10 +26,11 @@ export default function PaymentsPage() {
|
||||
const [payingId, setPayingId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (AuthService.isAdmin()) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
// Admin check removed
|
||||
// if (AuthService.isAdmin()) {
|
||||
// router.push('/');
|
||||
// return;
|
||||
// }
|
||||
loadReservations();
|
||||
}, [router]);
|
||||
|
||||
|
||||
@ -1289,7 +1289,7 @@ export default function PropertyDetailsPage() {
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-4">
|
||||
{/* Booking Card */}
|
||||
{property.isRent && !AuthService.isAdmin() && (
|
||||
{property.isRent && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
|
||||
@ -127,19 +127,11 @@ const AuthService = Object.freeze({
|
||||
},
|
||||
|
||||
/**
|
||||
* User has Admin role
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAdmin() {
|
||||
return this.getRoles().includes('Admin');
|
||||
},
|
||||
|
||||
/**
|
||||
* Authenticated user without Owner or Admin role (i.e. customer)
|
||||
* Authenticated user without Owner role (i.e. customer)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCustomer() {
|
||||
return this.isAuthenticated() && !this.isOwner() && !this.isAdmin();
|
||||
return this.isAuthenticated() && !this.isOwner();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -160,3 +152,4 @@ const AuthService = Object.freeze({
|
||||
});
|
||||
|
||||
export default AuthService;
|
||||
|
||||
|
||||
@ -884,47 +884,6 @@ export async function confirmDepositPayment(bookingId) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function adminConfirmDeposit(reservationId, adminId, comment = null) {
|
||||
const token = AuthService.getToken();
|
||||
const endpoint = `${API_BASE}/Reservations/AdminConfirmDeposit/admin-confirm-deposit`;
|
||||
|
||||
const normalizedComment =
|
||||
typeof comment === 'string' && comment.trim()
|
||||
? comment.trim()
|
||||
: null;
|
||||
|
||||
const payload = {
|
||||
reservationId,
|
||||
adminId,
|
||||
comment: normalizedComment,
|
||||
};
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let data = null;
|
||||
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
if (data && typeof data === 'object' && 'data' in data) {
|
||||
data = data.data;
|
||||
}
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
|
||||
const message = typeof data === 'object' && data?.message ? data.message : null;
|
||||
|
||||
return { status: res.status, data, ok: res.ok, message };
|
||||
}
|
||||
|
||||
export async function updateBookingStatus(bookingId, status) {
|
||||
return apiFetch('/Reservations/UpdateStatus', {
|
||||
method: 'PUT',
|
||||
|
||||
Reference in New Issue
Block a user