Edit admin page
Edit home image Added properties page
This commit is contained in:
@ -1,847 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import '../i18n/config';
|
||||
import {
|
||||
Users,
|
||||
Home,
|
||||
Calendar,
|
||||
User,
|
||||
Clock,
|
||||
Users,
|
||||
DollarSign,
|
||||
PlusCircle,
|
||||
Edit,
|
||||
Trash2,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Eye,
|
||||
MapPin,
|
||||
Bed,
|
||||
Bath,
|
||||
Square,
|
||||
Star,
|
||||
Phone,
|
||||
Mail,
|
||||
CalendarDays
|
||||
TrendingUp,
|
||||
Bell
|
||||
} 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 '../i18n/config';
|
||||
|
||||
export default function AdminPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('properties');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [showAddProperty, setShowAddProperty] = useState(false);
|
||||
const [notifications, setNotifications] = useState(3);
|
||||
|
||||
// احصل على اللغة الحالية من i18n
|
||||
const currentLanguage = i18n.language || 'en';
|
||||
|
||||
const [stats, setStats] = useState({
|
||||
totalUsers: 0,
|
||||
totalProperties: 0,
|
||||
activeBookings: 0,
|
||||
availableProperties: 0
|
||||
});
|
||||
|
||||
const [properties, setProperties] = useState([
|
||||
{
|
||||
id: 1,
|
||||
name: "luxuryVillaDamascus",
|
||||
type: "villa",
|
||||
price: 500000,
|
||||
location: "Damascus, Al-Mazzeh",
|
||||
bedrooms: 5,
|
||||
bathrooms: 4,
|
||||
area: 450,
|
||||
status: "available",
|
||||
images: [],
|
||||
features: ["swimmingPool", "privateGarden", "parking", "superLuxFinish"]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "modernApartmentAleppo",
|
||||
type: "apartment",
|
||||
price: 250000,
|
||||
location: "Aleppo, Al-Shahba",
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
area: 180,
|
||||
status: "booked",
|
||||
images: [],
|
||||
features: ["equippedKitchen", "centralHeating", "balcony", "securitySystem"]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "familyHouseHoms",
|
||||
type: "house",
|
||||
price: 350000,
|
||||
location: "Homs, Baba Amr",
|
||||
bedrooms: 4,
|
||||
bathrooms: 3,
|
||||
area: 300,
|
||||
status: "available",
|
||||
images: [],
|
||||
features: ["largeGarden", "receptionHall", "maidRoom", "garage"]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "seasideApartmentLatakia",
|
||||
type: "apartment",
|
||||
price: 300000,
|
||||
location: "Latakia, Blue Beach",
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
area: 200,
|
||||
status: "available",
|
||||
images: [],
|
||||
features: ["seaView", "centralHeating", "centralAC", "parking"]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "villaDaraa",
|
||||
type: "villa",
|
||||
price: 400000,
|
||||
location: "Daraa, Doctors District",
|
||||
bedrooms: 4,
|
||||
bathrooms: 3,
|
||||
area: 350,
|
||||
status: "booked",
|
||||
images: [],
|
||||
features: ["fruitGarden", "highWall", "advancedSecurity", "storage"]
|
||||
}
|
||||
]);
|
||||
|
||||
const [users, setUsers] = useState([
|
||||
{
|
||||
id: 1,
|
||||
name: "Ahmed Mohamed",
|
||||
email: "ahmed@example.com",
|
||||
phone: "+963 123 456 789",
|
||||
joinDate: "2024-01-15",
|
||||
activeBookings: 1,
|
||||
totalBookings: 3,
|
||||
currentBooking: {
|
||||
propertyId: 2,
|
||||
propertyName: "modernApartmentAleppo",
|
||||
startDate: "2024-02-01",
|
||||
endDate: "2024-08-01",
|
||||
duration: "6 months",
|
||||
totalAmount: 1500000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Sara Ahmed",
|
||||
email: "sara@example.com",
|
||||
phone: "+963 987 654 321",
|
||||
joinDate: "2024-02-10",
|
||||
activeBookings: 0,
|
||||
totalBookings: 2,
|
||||
currentBooking: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Mohammed Al-Halabi",
|
||||
email: "mohammed@example.com",
|
||||
phone: "+963 555 123 456",
|
||||
joinDate: "2024-01-25",
|
||||
activeBookings: 1,
|
||||
totalBookings: 1,
|
||||
currentBooking: {
|
||||
propertyId: 5,
|
||||
propertyName: "villaDaraa",
|
||||
startDate: "2024-02-15",
|
||||
endDate: "2024-05-15",
|
||||
duration: "3 months",
|
||||
totalAmount: 1200000
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const [bookingRequests, setBookingRequests] = useState([
|
||||
{
|
||||
id: "B001",
|
||||
userId: 2,
|
||||
userName: "Sara Ahmed",
|
||||
propertyId: 1,
|
||||
propertyName: "luxuryVillaDamascus",
|
||||
startDate: "2024-03-01",
|
||||
endDate: "2024-06-01",
|
||||
duration: "3 months",
|
||||
totalAmount: 1500000,
|
||||
status: "pending",
|
||||
requestDate: "2024-02-20"
|
||||
},
|
||||
{
|
||||
id: "B002",
|
||||
userId: 1,
|
||||
userName: "Ahmed Mohamed",
|
||||
propertyId: 4,
|
||||
propertyName: "seasideApartmentLatakia",
|
||||
startDate: "2024-03-15",
|
||||
endDate: "2024-05-15",
|
||||
duration: "2 months",
|
||||
totalAmount: 600000,
|
||||
status: "pending",
|
||||
requestDate: "2024-02-18"
|
||||
}
|
||||
]);
|
||||
|
||||
const formatNumber = (num) => {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
};
|
||||
|
||||
const fadeInUp = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5 }
|
||||
}
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cardHover = {
|
||||
rest: {
|
||||
scale: 1,
|
||||
y: 0,
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.05)"
|
||||
},
|
||||
hover: {
|
||||
scale: 1.02,
|
||||
y: -5,
|
||||
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.1)",
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 300
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buttonHover = {
|
||||
rest: { scale: 1 },
|
||||
hover: { scale: 1.05 },
|
||||
tap: { scale: 0.98 }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const totalBookings = users.reduce((sum, user) => sum + user.activeBookings, 0);
|
||||
const availableProps = properties.filter(p => p.status === "available").length;
|
||||
|
||||
setStats({
|
||||
totalUsers: users.length,
|
||||
totalProperties: properties.length,
|
||||
activeBookings: totalBookings,
|
||||
availableProperties: availableProps
|
||||
});
|
||||
}, [users, properties]);
|
||||
|
||||
const handleBookingAction = (bookingId, action) => {
|
||||
setBookingRequests(prev =>
|
||||
prev.map(booking =>
|
||||
booking.id === bookingId
|
||||
? { ...booking, status: action === 'accept' ? 'approved' : 'rejected' }
|
||||
: booking
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handlePropertyAction = (propertyId, action) => {
|
||||
if (action === 'delete') {
|
||||
setProperties(prev => prev.filter(p => p.id !== propertyId));
|
||||
}
|
||||
};
|
||||
|
||||
const showUserDetails = (user) => {
|
||||
setSelectedUser(user);
|
||||
};
|
||||
|
||||
const getLocation = (location) => {
|
||||
const [city, district] = location.split(",").map(s => s.trim());
|
||||
|
||||
// تحويل القيم إلى مفاتيح ترجمة
|
||||
const cityKey = city.toLowerCase();
|
||||
const districtKey = district.replace(/\s+/g, '');
|
||||
|
||||
return `${t(cityKey)}, ${t(districtKey)}`;
|
||||
};
|
||||
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 (
|
||||
<div className={`min-h-screen bg-gray-50 p-4 md:p-6 font-sans ${currentLanguage === 'ar' ? 'text-right' : 'text-left'}`}>
|
||||
{/* Header بدون زر اختيار اللغة */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2 tracking-tight">
|
||||
{t("adminDashboard")}
|
||||
</h1>
|
||||
<p className="text-gray-600 text-base mb-1">{t("manageProperties")}</p>
|
||||
<p className="text-gray-500 text-sm">{t("pricesInSYP")}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* إحصائيات */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<PropertyProvider>
|
||||
<div className={`min-h-screen bg-gray-50 p-4 md:p-6 ${i18n.language === 'ar' ? 'text-right' : 'text-left'}`}>
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
whileHover="hover"
|
||||
variants={cardHover}
|
||||
className="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-xl shadow p-5"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<div className={`flex items-center justify-between mb-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="p-3 bg-white/20 rounded-lg">
|
||||
<Users className="w-6 h-6" />
|
||||
<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>
|
||||
<div className={currentLanguage === 'ar' ? 'text-left' : 'text-right'}>
|
||||
<div className="text-2xl font-bold mb-1">{stats.totalUsers}</div>
|
||||
<div className="text-blue-100 text-sm">{t("totalUsers")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-blue-100 mt-3 pt-3 border-t border-blue-400/30">
|
||||
{users.filter(u => u.activeBookings > 0).length} {t("usersWithActiveBookings")}
|
||||
|
||||
<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>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
whileHover="hover"
|
||||
variants={cardHover}
|
||||
className="bg-gradient-to-br from-emerald-600 to-emerald-700 text-white rounded-xl shadow p-5"
|
||||
>
|
||||
<div className={`flex items-center justify-between mb-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="p-3 bg-white/20 rounded-lg">
|
||||
<Home className="w-6 h-6" />
|
||||
</div>
|
||||
<div className={currentLanguage === 'ar' ? 'text-left' : 'text-right'}>
|
||||
<div className="text-2xl font-bold mb-1">{stats.totalProperties}</div>
|
||||
<div className="text-emerald-100 text-sm">{t("totalProperties")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-emerald-100 mt-3 pt-3 border-t border-emerald-400/30">
|
||||
{stats.availableProperties} {t("propertiesAvailable")}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
whileHover="hover"
|
||||
variants={cardHover}
|
||||
className="bg-gradient-to-br from-purple-600 to-purple-700 text-white rounded-xl shadow p-5"
|
||||
>
|
||||
<div className={`flex items-center justify-between mb-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="p-3 bg-white/20 rounded-lg">
|
||||
<Calendar className="w-6 h-6" />
|
||||
</div>
|
||||
<div className={currentLanguage === 'ar' ? 'text-left' : 'text-right'}>
|
||||
<div className="text-2xl font-bold mb-1">{stats.activeBookings}</div>
|
||||
<div className="text-purple-100 text-sm">{t("activeBookings")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-purple-100 mt-3 pt-3 border-t border-purple-400/30">
|
||||
{bookingRequests.filter(b => b.status === 'pending').length} {t("bookingRequestsPending")}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
whileHover="hover"
|
||||
variants={cardHover}
|
||||
className="bg-gradient-to-br from-amber-600 to-amber-700 text-white rounded-xl shadow p-5"
|
||||
>
|
||||
<div className={`flex items-center justify-between mb-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="p-3 bg-white/20 rounded-lg">
|
||||
<Home className="w-6 h-6" />
|
||||
</div>
|
||||
<div className={currentLanguage === 'ar' ? 'text-left' : 'text-right'}>
|
||||
<div className="text-2xl font-bold mb-1">{stats.availableProperties}</div>
|
||||
<div className="text-amber-100 text-sm">{t("availableProperties")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-amber-100 mt-3 pt-3 border-t border-amber-400/30">
|
||||
{properties.filter(p => p.status === 'available').length} {t("propertiesReadyForRent")}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="mb-6"
|
||||
>
|
||||
<div className={`flex flex-wrap gap-2 border-b border-gray-200 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<button
|
||||
onClick={() => setActiveTab('properties')}
|
||||
className={`px-4 py-3 font-medium text-sm rounded-t-lg transition-all ${activeTab === 'properties' ? '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">
|
||||
<Home className="w-4 h-4" />
|
||||
<span>{t("properties")}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('bookings')}
|
||||
className={`px-4 py-3 font-medium text-sm rounded-t-lg transition-all ${activeTab === 'bookings' ? '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">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{t("bookingRequests")}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`px-4 py-3 font-medium text-sm rounded-t-lg transition-all ${activeTab === 'users' ? '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">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{t("users")}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
||||
|
||||
{activeTab === 'properties' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className={`flex flex-col md:flex-row md:items-center justify-between mb-6 ${currentLanguage === 'ar' ? 'md:flex-row-reverse' : ''}`}>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-1">{t("propertiesManagement")}</h2>
|
||||
<p className="text-gray-600 text-sm">{t("addEditDeleteProperties")}</p>
|
||||
</div>
|
||||
<motion.button
|
||||
variants={buttonHover}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
className="mt-3 md:mt-0 bg-blue-700 hover:bg-blue-800 text-white px-5 py-3 rounded-lg flex items-center gap-2 text-sm"
|
||||
>
|
||||
<PlusCircle className="w-4 h-4" />
|
||||
{t("addNewProperty")}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className={`mb-6 flex flex-col md:flex-row gap-3 ${currentLanguage === 'ar' ? 'md:flex-row-reverse' : ''}`}>
|
||||
<div className="relative flex-1">
|
||||
<Search className={`absolute ${currentLanguage === 'ar' ? 'right-3' : 'left-3'} top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("searchProperties")}
|
||||
className={`w-full ${currentLanguage === 'ar' ? 'pr-3 pl-10' : 'pl-3 pr-10'} py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm placeholder:text-gray-400`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={`flex gap-2 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<motion.button
|
||||
variants={buttonHover}
|
||||
whileHover="hover"
|
||||
className="px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-2 text-sm"
|
||||
>
|
||||
<Filter className="w-4 h-4" />
|
||||
{t("filter")}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
variants={buttonHover}
|
||||
whileHover="hover"
|
||||
className="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg flex items-center gap-2 text-sm"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{t("export")}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{properties.map((property) => (
|
||||
<motion.div
|
||||
key={property.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
whileHover={{ y: -4 }}
|
||||
className="border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className={`flex justify-between items-start mb-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-gray-900 mb-1">{t(property.name)}</h3>
|
||||
<div className="flex items-center gap-2 text-gray-600 text-xs">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span>{getLocation(property.location)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${property.status === 'available' ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{property.status === 'available' ? t("available") : t("booked")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||
<div className="text-center bg-gray-50 p-3 rounded-lg">
|
||||
<div className="flex items-center justify-center gap-1 text-gray-700 mb-1 text-xs">
|
||||
<Bed className="w-3 h-3" />
|
||||
<span>{t("bedrooms")}</span>
|
||||
</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{property.bedrooms}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center bg-gray-50 p-3 rounded-lg">
|
||||
<div className="flex items-center justify-center gap-1 text-gray-700 mb-1 text-xs">
|
||||
<Bath className="w-3 h-3" />
|
||||
<span>{t("bathrooms")}</span>
|
||||
</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{property.bathrooms}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center bg-gray-50 p-3 rounded-lg">
|
||||
<div className="flex items-center justify-center gap-1 text-gray-700 mb-1 text-xs">
|
||||
<Square className="w-3 h-3" />
|
||||
<span>{t("area")}</span>
|
||||
</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{property.area} m²</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center bg-gray-50 p-3 rounded-lg">
|
||||
<div className="flex items-center justify-center gap-1 text-gray-700 mb-1 text-xs">
|
||||
<DollarSign className="w-3 h-3" />
|
||||
<span>{t("price")}</span>
|
||||
</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{formatNumber(property.price)} SYP/{t("month")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-5">
|
||||
<h4 className="font-bold text-gray-900 text-sm mb-2">{t("features")}:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{property.features.map((feature, idx) => (
|
||||
<span key={idx} className="px-2.5 py-1 bg-blue-50 text-blue-800 rounded-full text-xs font-medium">
|
||||
{t(feature)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex justify-end gap-2 pt-4 border-t border-gray-100 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="px-3 py-2 bg-blue-100 text-blue-800 hover:bg-blue-200 rounded-lg flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
{t("viewDetails")}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="px-3 py-2 bg-amber-100 text-amber-800 hover:bg-amber-200 rounded-lg flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Edit className="w-3 h-3" />
|
||||
{t("edit")}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => handlePropertyAction(property.id, 'delete')}
|
||||
className="px-3 py-2 bg-red-100 text-red-800 hover:bg-red-200 rounded-lg flex items-center gap-2 text-xs"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
{t("delete")}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'bookings' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-1">{t("bookingRequests")}</h2>
|
||||
<p className="text-gray-600 text-sm">{t("manageBookingRequests")}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{bookingRequests.map((request) => (
|
||||
<motion.div
|
||||
key={request.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="border border-gray-200 rounded-lg p-5 hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className={`flex items-center justify-between mb-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<h3 className="text-base font-bold text-gray-900">{t("bookingRequest")} #{request.id}</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${request.status === 'pending' ? 'bg-yellow-100 text-yellow-800' : request.status === 'approved' ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{request.status === 'pending' ? t("pending") : request.status === 'approved' ? t("approved") : t("rejected")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<div className="text-xs text-gray-600 mb-1">{t("user")}</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{request.userName}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<div className="text-xs text-gray-600 mb-1">{t("property")}</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{t(request.propertyName)}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<div className="text-xs text-gray-600 mb-1">{t("duration")}</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{request.duration}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<div className="text-xs text-gray-600 mb-1">{t("totalAmount")}</div>
|
||||
<div className="font-bold text-gray-900 text-sm">{formatNumber(request.totalAmount)} SYP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-600 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarDays className="w-3 h-3" />
|
||||
<span>{t("from")} <span className="font-medium">{request.startDate}</span> {t("to")} <span className="font-medium">{request.endDate}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{t("requestDate")}: <span className="font-medium">{request.requestDate}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.status === 'pending' && (
|
||||
<div className={`flex gap-2 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<motion.button
|
||||
variants={buttonHover}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onClick={() => handleBookingAction(request.id, 'accept')}
|
||||
className="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg flex items-center gap-2 text-sm"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{t("accept")}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
variants={buttonHover}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onClick={() => handleBookingAction(request.id, 'reject')}
|
||||
className="px-4 py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2 text-sm"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
{t("reject")}
|
||||
</motion.button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'users' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-1">{t("users")}</h2>
|
||||
<p className="text-gray-600 text-sm">{t("viewUserDetails")}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{users.map((user) => (
|
||||
<motion.div
|
||||
key={user.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="border border-gray-200 rounded-lg overflow-hidden hover:shadow-sm transition-all"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-5">
|
||||
<div className={`flex items-center gap-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-blue-700" />
|
||||
</div>
|
||||
<div className={currentLanguage === 'ar' ? 'text-right' : 'text-left'}>
|
||||
<h3 className="text-base font-bold text-gray-900 mb-1">{user.name}</h3>
|
||||
<div className="flex flex-wrap gap-3 text-xs">
|
||||
<div className="flex items-center gap-1 text-gray-700">
|
||||
<Mail className="w-3 h-3" />
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-gray-700">
|
||||
<Phone className="w-3 h-3" />
|
||||
<span>{user.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex gap-4 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="text-center bg-blue-50 p-3 rounded-lg">
|
||||
<div className="text-lg font-bold text-blue-700">{user.activeBookings}</div>
|
||||
<div className="text-gray-700 text-xs">{t("activeBookings")}</div>
|
||||
</div>
|
||||
<div className="text-center bg-emerald-50 p-3 rounded-lg">
|
||||
<div className="text-lg font-bold text-emerald-700">{user.totalBookings}</div>
|
||||
<div className="text-gray-700 text-xs">{t("totalBookings")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.currentBooking ? (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-5">
|
||||
<h4 className="font-bold text-blue-900 text-sm mb-3 flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{t("currentActiveBooking")}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div className="bg-white p-3 rounded">
|
||||
<div className="text-xs text-blue-700 mb-1">{t("property")}</div>
|
||||
<div className="font-bold text-blue-900 text-sm">{t(user.currentBooking.propertyName)}</div>
|
||||
</div>
|
||||
<div className="bg-white p-3 rounded">
|
||||
<div className="text-xs text-blue-700 mb-1">{t("duration")}</div>
|
||||
<div className="font-bold text-blue-900 text-sm">{user.currentBooking.duration}</div>
|
||||
</div>
|
||||
<div className="bg-white p-3 rounded">
|
||||
<div className="text-xs text-blue-700 mb-1">{t("totalAmount")}</div>
|
||||
<div className="font-bold text-blue-900 text-sm">{formatNumber(user.currentBooking.totalAmount)} SYP</div>
|
||||
</div>
|
||||
<div className="bg-white p-3 rounded">
|
||||
<div className="text-xs text-blue-700 mb-1">{t("bookingPeriod")}</div>
|
||||
<div className="font-bold text-blue-900 text-sm">
|
||||
{user.currentBooking.startDate} {t("to")} {user.currentBooking.endDate}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-100 border border-gray-300 rounded-lg p-4 mb-5 text-center">
|
||||
<div className="text-gray-600 text-sm">{t("noActiveBookings")}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<motion.button
|
||||
variants={buttonHover}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onClick={() => showUserDetails(user)}
|
||||
className="px-4 py-2.5 bg-blue-700 hover:bg-blue-800 text-white rounded-lg flex items-center gap-2 text-sm"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{t("viewFullDetails")}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedUser && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className={`flex justify-between items-center mb-6 ${currentLanguage === 'ar' ? 'flex-row-reverse' : ''}`}>
|
||||
<h3 className="text-lg font-bold text-gray-900">{t("userDetails")}: {selectedUser.name}</h3>
|
||||
<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
|
||||
onClick={() => setSelectedUser(null)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg"
|
||||
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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<XCircle className="w-5 h-5 text-gray-600" />
|
||||
<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>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 className="font-bold text-sm text-gray-800 mb-3">{t("personalInformation")}</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t("fullName")}:</span>
|
||||
<span className="font-medium">{selectedUser.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t("email")}:</span>
|
||||
<span className="font-medium">{selectedUser.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t("phoneNumber")}:</span>
|
||||
<span className="font-medium">{selectedUser.phone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">{t("joinDate")}:</span>
|
||||
<span className="font-medium">{selectedUser.joinDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'bookings' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<BookingRequests />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<h4 className="font-bold text-sm text-blue-800 mb-3">{t("bookingStatistics")}</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-blue-600">{t("activeBookings")}:</span>
|
||||
<span className="font-medium text-blue-800">{selectedUser.activeBookings}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-blue-600">{t("totalBookings")}:</span>
|
||||
<span className="font-medium text-blue-800">{selectedUser.totalBookings}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showAddProperty && (
|
||||
<AddPropertyForm
|
||||
onClose={() => setShowAddProperty(false)}
|
||||
onSuccess={() => {
|
||||
setShowAddProperty(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PropertyProvider>
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
|
||||
export function NavLink({ href, children }) {
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname === href;
|
||||
const isActive = pathname === href || pathname.startsWith(href + '/');
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
@ -24,7 +24,7 @@ export function NavLink({ href, children }) {
|
||||
|
||||
export function MobileNavLink({ href, children, onClick }) {
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname === href;
|
||||
const isActive = pathname === href || pathname.startsWith(href + '/');
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
|
||||
346
app/components/admin/AddPropertyForm.js
Normal file
346
app/components/admin/AddPropertyForm.js
Normal file
@ -0,0 +1,346 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useProperties } from '@/app/contexts/PropertyContext';
|
||||
import { COMMISSION_TYPE, CITIES } from '@/app/utils/constants';
|
||||
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: COMMISSION_TYPE.FROM_OWNER,
|
||||
|
||||
securityDeposit: 0,
|
||||
|
||||
images: [],
|
||||
features: [],
|
||||
|
||||
status: 'available'
|
||||
});
|
||||
|
||||
const [selectedFeatures, setSelectedFeatures] = useState([]);
|
||||
|
||||
const featuresList = [
|
||||
'swimmingPool', 'privateGarden', 'parking', 'superLuxFinish',
|
||||
'equippedKitchen', 'centralHeating', 'balcony', 'securitySystem',
|
||||
'largeGarden', 'receptionHall', 'maidRoom', 'garage',
|
||||
'seaView', 'centralAC', 'fruitGarden', 'storage'
|
||||
];
|
||||
|
||||
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 COMMISSION_TYPE.FROM_TENANT:
|
||||
return dailyPrice + commission;
|
||||
case COMMISSION_TYPE.FROM_OWNER:
|
||||
return dailyPrice;
|
||||
case COMMISSION_TYPE.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>
|
||||
{Object.values(CITIES).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={COMMISSION_TYPE.FROM_OWNER}
|
||||
checked={formData.commissionType === COMMISSION_TYPE.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={COMMISSION_TYPE.FROM_TENANT}
|
||||
checked={formData.commissionType === COMMISSION_TYPE.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={COMMISSION_TYPE.FROM_BOTH}
|
||||
checked={formData.commissionType === COMMISSION_TYPE.FROM_BOTH}
|
||||
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
|
||||
/>
|
||||
<span>من الاثنين</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 bg-white p-3 rounded-lg">
|
||||
<h4 className="font-medium mb-2">تفاصيل السعر بعد العمولة:</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">السعر الأصلي:</span>
|
||||
<span className="block font-bold">{formData.dailyPrice} ل.س</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">العمولة:</span>
|
||||
<span className="block font-bold">
|
||||
{(formData.dailyPrice * formData.commissionRate / 100)} ل.س
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">السعر النهائي:</span>
|
||||
<span className="block font-bold text-green-600">
|
||||
{calculateCommissionPrice(formData)} ل.س
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">نوع العقار</label>
|
||||
<select
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData({...formData, type: e.target.value})}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
>
|
||||
<option value="apartment">شقة</option>
|
||||
<option value="house">بيت</option>
|
||||
<option value="villa">فيلا</option>
|
||||
<option value="studio">استوديو</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">مبلغ الضمان (ل.س)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.securityDeposit}
|
||||
onChange={(e) => setFormData({...formData, securityDeposit: Number(e.target.value)})}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">المميزات</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{featuresList.map(feature => (
|
||||
<label key={feature} className="flex items-center gap-2 p-2 border rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFeatures.includes(feature)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedFeatures([...selectedFeatures, feature]);
|
||||
} else {
|
||||
setSelectedFeatures(selectedFeatures.filter(f => f !== feature));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm">{feature}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
إضافة العقار
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
إلغاء
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
948
app/components/admin/BookingRequests.js
Normal file
948
app/components/admin/BookingRequests.js
Normal file
@ -0,0 +1,948 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
User,
|
||||
Home,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
AlertCircle,
|
||||
Key,
|
||||
DoorOpen,
|
||||
Shield,
|
||||
Phone,
|
||||
Mail,
|
||||
MessageCircle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
FileText,
|
||||
Download,
|
||||
Printer,
|
||||
History
|
||||
} from 'lucide-react';
|
||||
|
||||
const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' }) => {
|
||||
const [reason, setReason] = useState(defaultReason);
|
||||
const [otherReason, setOtherReason] = useState('');
|
||||
|
||||
const commonReasons = [
|
||||
'أعمال صيانة في العقار',
|
||||
'العقار غير متاح في هذه التواريخ',
|
||||
'مشكلة في وثائق المستأجر',
|
||||
'المالك غير متاح للتسليم',
|
||||
'تأخر في دفع الضمان',
|
||||
'سبب آخر'
|
||||
];
|
||||
|
||||
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">{title}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">يرجى تحديد سبب الرفض</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{commonReasons.map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => {
|
||||
if (r === 'سبب آخر') {
|
||||
} else {
|
||||
onConfirm(r);
|
||||
}
|
||||
}}
|
||||
className="w-full text-right p-3 border rounded-xl hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
{r}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<textarea
|
||||
placeholder="اكتب سبباً آخر..."
|
||||
value={otherReason}
|
||||
onChange={(e) => setOtherReason(e.target.value)}
|
||||
className="w-full p-3 border rounded-xl resize-none focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<div className="flex gap-3 pt-3">
|
||||
<button
|
||||
onClick={() => onConfirm(otherReason)}
|
||||
className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-colors"
|
||||
disabled={!otherReason.trim()}
|
||||
>
|
||||
تأكيد الرفض
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
|
||||
if (!isOpen || !request) return null;
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return amount?.toLocaleString() + ' ل.س';
|
||||
};
|
||||
|
||||
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-white border-b p-4 flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-blue-600" />
|
||||
تفاصيل الطلب #{request.id}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||
>
|
||||
<XCircle className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-4 rounded-xl">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2 text-blue-800">
|
||||
<User className="w-4 h-4" />
|
||||
معلومات المستأجر
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">الاسم الكامل</label>
|
||||
<div className="font-medium">{request.user}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">نوع الهوية</label>
|
||||
<div className="font-medium">
|
||||
{request.userType === 'syrian' ? '🇸🇾 هوية سورية' : '🛂 جواز سفر'}
|
||||
<span className="text-xs text-gray-500 mr-2">{request.identityNumber}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">البريد الإلكتروني</label>
|
||||
<div className="font-medium flex items-center gap-1">
|
||||
<Mail className="w-3 h-3 text-gray-400" />
|
||||
{request.userEmail}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">رقم الهاتف</label>
|
||||
<div className="font-medium flex items-center gap-1">
|
||||
<Phone className="w-3 h-3 text-gray-400" />
|
||||
{request.userPhone}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-green-50 to-emerald-50 p-4 rounded-xl">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2 text-green-800">
|
||||
<Home className="w-4 h-4" />
|
||||
معلومات العقار
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">العقار</label>
|
||||
<div className="font-medium">{request.property}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">السعر اليومي</label>
|
||||
<div className="font-medium text-green-600">{formatCurrency(request.dailyPrice)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 p-4 rounded-xl">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2 text-amber-800">
|
||||
<Calendar className="w-4 h-4" />
|
||||
تفاصيل الحجز
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">تاريخ البداية</label>
|
||||
<div className="font-medium">{request.startDate}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">تاريخ النهاية</label>
|
||||
<div className="font-medium">{request.endDate}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">عدد الأيام</label>
|
||||
<div className="font-medium">{request.days} يوم</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">المبلغ الإجمالي</label>
|
||||
<div className="font-medium text-green-600">{formatCurrency(request.totalAmount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-purple-50 to-pink-50 p-4 rounded-xl">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2 text-purple-800">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
المعلومات المالية
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">سلفة الضمان</label>
|
||||
<div className="font-medium text-blue-600">{formatCurrency(request.securityDeposit)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">نسبة العمولة</label>
|
||||
<div className="font-medium">{request.commissionRate}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">نوع العمولة</label>
|
||||
<div className="font-medium text-amber-600">{request.commissionType}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500">قيمة العمولة</label>
|
||||
<div className="font-medium text-amber-600">{formatCurrency(request.commissionAmount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-bold mb-3 flex items-center gap-2">
|
||||
<History className="w-4 h-4" />
|
||||
سجل الإجراءات
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="text-gray-600">تم إنشاء الطلب: {request.requestDate}</span>
|
||||
</div>
|
||||
{request.ownerApproved && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="text-gray-600">موافقة المالك</span>
|
||||
</div>
|
||||
)}
|
||||
{request.adminApproved && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="text-gray-600">موافقة الإدارة</span>
|
||||
</div>
|
||||
)}
|
||||
{request.ownerDelivered && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
|
||||
<span className="text-gray-600">تم تسليم المفتاح من المالك</span>
|
||||
</div>
|
||||
)}
|
||||
{request.notes && (
|
||||
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
||||
<p className="text-sm text-gray-600">{request.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<Printer className="w-5 h-5" />
|
||||
طباعة التفاصيل
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 bg-green-600 text-white py-3 rounded-xl font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
تصدير PDF
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestCard = ({ request, onAction, onViewDetails }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return amount?.toLocaleString() + ' ل.س';
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch(status) {
|
||||
case 'pending': return 'border-yellow-400 bg-yellow-50';
|
||||
case 'owner_approved': return 'border-blue-400 bg-blue-50';
|
||||
case 'admin_approved': return 'border-green-400 bg-green-50';
|
||||
case 'active': return 'border-purple-400 bg-purple-50';
|
||||
case 'completed': return 'border-gray-400 bg-gray-50';
|
||||
case 'rejected': return 'border-red-400 bg-red-50';
|
||||
default: return 'border-gray-200 bg-white';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const styles = {
|
||||
pending: 'bg-yellow-500 text-white',
|
||||
owner_approved: 'bg-blue-500 text-white',
|
||||
admin_approved: 'bg-green-500 text-white',
|
||||
active: 'bg-purple-500 text-white',
|
||||
completed: 'bg-gray-500 text-white',
|
||||
rejected: 'bg-red-500 text-white'
|
||||
};
|
||||
|
||||
const labels = {
|
||||
pending: ' بانتظار الموافقة',
|
||||
owner_approved: ' موافقة المالك',
|
||||
admin_approved: ' موافقة الإدارة',
|
||||
active: ' إيجار نشط',
|
||||
completed: ' منتهي',
|
||||
rejected: ' مرفوض'
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium shadow-sm ${styles[status]}`}>
|
||||
{labels[status]}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`bg-white border-2 rounded-2xl overflow-hidden transition-all hover:shadow-xl ${getStatusColor(request.status)}`}
|
||||
>
|
||||
<div className="p-4 cursor-pointer" onClick={() => setExpanded(!expanded)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg font-bold text-gray-900">طلب #{request.id}</span>
|
||||
{getStatusBadge(request.status)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500">{request.requestDate}</span>
|
||||
{expanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
<span className="truncate">{request.user}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Home className="w-4 h-4 text-gray-400" />
|
||||
<span className="truncate">{request.property}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="w-4 h-4 text-gray-400" />
|
||||
<span>{request.days} أيام</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<DollarSign className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-bold text-green-600">{formatCurrency(request.totalAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t bg-white p-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
|
||||
<div className="bg-blue-50 p-2 rounded-lg">
|
||||
<div className="text-xs text-gray-500">سلفة ضمان</div>
|
||||
<div className="font-bold text-blue-600">{formatCurrency(request.securityDeposit)}</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 p-2 rounded-lg">
|
||||
<div className="text-xs text-gray-500">العمولة</div>
|
||||
<div className="font-bold text-amber-600">{request.commissionRate}% ({request.commissionType})</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 p-2 rounded-lg">
|
||||
<div className="text-xs text-gray-500">مدة الإيجار</div>
|
||||
<div className="font-bold text-purple-600">{request.startDate} إلى {request.endDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(request.ownerApproved || request.adminApproved) && (
|
||||
<div className="bg-green-50 p-3 rounded-lg mb-4">
|
||||
<h4 className="font-bold text-sm mb-2 flex items-center gap-2 text-green-800">
|
||||
<Phone className="w-4 h-4" />
|
||||
معلومات الاتصال
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<Mail className="w-3 h-3 text-gray-500" />
|
||||
{request.userEmail}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Phone className="w-3 h-3 text-gray-500" />
|
||||
{request.userPhone}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{request.status === 'pending' && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => onAction('owner_approve', request.id)}
|
||||
className="flex-1 bg-green-600 text-white py-3 rounded-xl font-medium hover:bg-green-700 transition-all transform hover:scale-105 flex items-center justify-center gap-2"
|
||||
>
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
موافقة المالك
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('owner_reject', request.id)}
|
||||
className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-all transform hover:scale-105 flex items-center justify-center gap-2"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
رفض
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('view_details', request)}
|
||||
className="px-4 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<FileText className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request.status === 'owner_approved' && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => onAction('admin_approve', request.id)}
|
||||
className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-all transform hover:scale-105 flex items-center justify-center gap-2"
|
||||
>
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
موافقة الإدارة
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('admin_reject', request.id)}
|
||||
className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-all transform hover:scale-105 flex items-center justify-center gap-2"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
رفض إداري
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('view_details', request)}
|
||||
className="px-4 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<FileText className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request.status === 'admin_approved' && (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => onAction('deliver_key', { id: request.id, type: 'owner' })}
|
||||
disabled={request.ownerDelivered}
|
||||
className={`py-3 rounded-xl font-medium flex items-center justify-center gap-2 transition-all ${
|
||||
request.ownerDelivered
|
||||
? 'bg-green-100 text-green-800 cursor-default'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700 transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<Key className="w-5 h-5" />
|
||||
{request.ownerDelivered ? '✓ تم تسليم المفتاح' : 'تسليم المفتاح'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('receive_property', { id: request.id, type: 'tenant' })}
|
||||
disabled={request.tenantReceived}
|
||||
className={`py-3 rounded-xl font-medium flex items-center justify-center gap-2 transition-all ${
|
||||
request.tenantReceived
|
||||
? 'bg-green-100 text-green-800 cursor-default'
|
||||
: 'bg-green-600 text-white hover:bg-green-700 transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<DoorOpen className="w-5 h-5" />
|
||||
{request.tenantReceived ? '✓ تم الاستلام' : 'استلام العقار'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request.status === 'active' && (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => onAction('tenant_leave', { id: request.id, type: 'tenant' })}
|
||||
disabled={request.tenantLeft}
|
||||
className={`py-3 rounded-xl font-medium flex items-center justify-center gap-2 transition-all ${
|
||||
request.tenantLeft
|
||||
? 'bg-green-100 text-green-800 cursor-default'
|
||||
: 'bg-amber-600 text-white hover:bg-amber-700 transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<DoorOpen className="w-5 h-5" />
|
||||
{request.tenantLeft ? '✓ تم المغادرة' : 'مغادرة العقار'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('owner_receive', { id: request.id, type: 'owner' })}
|
||||
disabled={request.ownerReceived}
|
||||
className={`py-3 rounded-xl font-medium flex items-center justify-center gap-2 transition-all ${
|
||||
request.ownerReceived
|
||||
? 'bg-green-100 text-green-800 cursor-default'
|
||||
: 'bg-purple-600 text-white hover:bg-purple-700 transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<Key className="w-5 h-5" />
|
||||
{request.ownerReceived ? '✓ تم الاستلام' : 'استلام العقار'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{request.actualStartDate && (
|
||||
<div className="bg-gray-100 p-3 rounded-lg">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>بدأ الإيجار: {request.actualStartDate}</span>
|
||||
<span>المدة: {request.days} يوم</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-300 rounded-full h-2">
|
||||
<div
|
||||
className="bg-purple-600 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: '45%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function BookingRequests() {
|
||||
const [requests, setRequests] = useState([
|
||||
{
|
||||
id: 'REQ001',
|
||||
user: 'أحمد محمد',
|
||||
userEmail: 'ahmed@example.com',
|
||||
userPhone: '0938123456',
|
||||
userType: 'syrian',
|
||||
identityNumber: '123456789',
|
||||
property: 'فيلا فاخرة في دمشق',
|
||||
propertyId: 1,
|
||||
startDate: '2024-03-01',
|
||||
endDate: '2024-03-10',
|
||||
days: 10,
|
||||
totalAmount: 5000000,
|
||||
dailyPrice: 500000,
|
||||
commissionRate: 5,
|
||||
commissionType: 'من المالك',
|
||||
commissionAmount: 250000,
|
||||
securityDeposit: 500000,
|
||||
status: 'pending',
|
||||
requestDate: '2024-02-25',
|
||||
ownerApproved: false,
|
||||
adminApproved: false,
|
||||
ownerDelivered: false,
|
||||
tenantReceived: false,
|
||||
tenantLeft: false,
|
||||
ownerReceived: false,
|
||||
securityDepositReturned: null,
|
||||
contractSigned: false,
|
||||
notes: '',
|
||||
actualStartDate: null,
|
||||
actualEndDate: null
|
||||
},
|
||||
{
|
||||
id: 'REQ002',
|
||||
user: 'سارة أحمد',
|
||||
userEmail: 'sara@example.com',
|
||||
userPhone: '0945123789',
|
||||
userType: 'passport',
|
||||
identityNumber: 'AB123456',
|
||||
property: 'شقة حديثة في حلب',
|
||||
propertyId: 2,
|
||||
startDate: '2024-03-05',
|
||||
endDate: '2024-03-15',
|
||||
days: 10,
|
||||
totalAmount: 2500000,
|
||||
dailyPrice: 250000,
|
||||
commissionRate: 7,
|
||||
commissionType: 'من المستأجر',
|
||||
commissionAmount: 175000,
|
||||
securityDeposit: 250000,
|
||||
status: 'owner_approved',
|
||||
requestDate: '2024-02-24',
|
||||
ownerApproved: true,
|
||||
adminApproved: false,
|
||||
ownerDelivered: false,
|
||||
tenantReceived: false,
|
||||
tenantLeft: false,
|
||||
ownerReceived: false,
|
||||
securityDepositReturned: null,
|
||||
contractSigned: false,
|
||||
notes: '',
|
||||
actualStartDate: null,
|
||||
actualEndDate: null
|
||||
},
|
||||
{
|
||||
id: 'REQ003',
|
||||
user: 'محمد الحلبي',
|
||||
userEmail: 'mohammed@example.com',
|
||||
userPhone: '0956123456',
|
||||
userType: 'syrian',
|
||||
identityNumber: '987654321',
|
||||
property: 'شقة بجانب البحر في اللاذقية',
|
||||
propertyId: 3,
|
||||
startDate: '2024-02-20',
|
||||
endDate: '2024-03-20',
|
||||
days: 30,
|
||||
totalAmount: 9000000,
|
||||
dailyPrice: 300000,
|
||||
commissionRate: 5,
|
||||
commissionType: 'من الاثنين',
|
||||
commissionAmount: 450000,
|
||||
securityDeposit: 500000,
|
||||
status: 'active',
|
||||
requestDate: '2024-02-15',
|
||||
ownerApproved: true,
|
||||
adminApproved: true,
|
||||
ownerDelivered: true,
|
||||
tenantReceived: true,
|
||||
tenantLeft: false,
|
||||
ownerReceived: false,
|
||||
securityDepositReturned: null,
|
||||
contractSigned: true,
|
||||
notes: 'عقد موقع إلكترونياً',
|
||||
actualStartDate: '2024-02-20',
|
||||
actualEndDate: null
|
||||
}
|
||||
]);
|
||||
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [reasonDialog, setReasonDialog] = useState({ isOpen: false, requestId: null, type: null });
|
||||
const [detailsDialog, setDetailsDialog] = useState({ isOpen: false, request: null });
|
||||
|
||||
const handleAction = (action, data) => {
|
||||
switch(action) {
|
||||
case 'owner_approve':
|
||||
handleOwnerApprove(data);
|
||||
break;
|
||||
case 'owner_reject':
|
||||
setReasonDialog({ isOpen: true, requestId: data, type: 'owner' });
|
||||
break;
|
||||
case 'admin_approve':
|
||||
handleAdminApprove(data);
|
||||
break;
|
||||
case 'admin_reject':
|
||||
setReasonDialog({ isOpen: true, requestId: data, type: 'admin' });
|
||||
break;
|
||||
case 'deliver_key':
|
||||
handleKeyDelivery(data.id, data.type);
|
||||
break;
|
||||
case 'receive_property':
|
||||
handleKeyDelivery(data.id, data.type);
|
||||
break;
|
||||
case 'tenant_leave':
|
||||
handleEndRental(data.id, data.type);
|
||||
break;
|
||||
case 'owner_receive':
|
||||
handleEndRental(data.id, data.type);
|
||||
break;
|
||||
case 'view_details':
|
||||
setDetailsDialog({ isOpen: true, request: data });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectWithReason = (reason) => {
|
||||
const { requestId, type } = reasonDialog;
|
||||
|
||||
setRequests(prev =>
|
||||
prev.map(req =>
|
||||
req.id === requestId
|
||||
? {
|
||||
...req,
|
||||
status: 'rejected',
|
||||
[type === 'owner' ? 'ownerApproved' : 'adminApproved']: false,
|
||||
rejectionReason: reason,
|
||||
rejectionType: type,
|
||||
notes: `${type === 'owner' ? 'رفض من المالك' : 'رفض إداري'}: ${reason}`
|
||||
}
|
||||
: req
|
||||
)
|
||||
);
|
||||
|
||||
setReasonDialog({ isOpen: false, requestId: null, type: null });
|
||||
};
|
||||
|
||||
const handleOwnerApprove = (requestId) => {
|
||||
setRequests(prev =>
|
||||
prev.map(req =>
|
||||
req.id === requestId
|
||||
? {
|
||||
...req,
|
||||
ownerApproved: true,
|
||||
status: 'owner_approved',
|
||||
notes: 'تمت الموافقة من قبل المالك'
|
||||
}
|
||||
: req
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdminApprove = (requestId) => {
|
||||
setRequests(prev =>
|
||||
prev.map(req =>
|
||||
req.id === requestId
|
||||
? {
|
||||
...req,
|
||||
adminApproved: true,
|
||||
status: 'admin_approved',
|
||||
notes: 'تمت الموافقة من قبل الإدارة'
|
||||
}
|
||||
: req
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyDelivery = (requestId, userType) => {
|
||||
setRequests(prev =>
|
||||
prev.map(req => {
|
||||
if (req.id === requestId) {
|
||||
const updates = {};
|
||||
|
||||
if (userType === 'owner') {
|
||||
updates.ownerDelivered = true;
|
||||
updates.notes = 'تم تسليم المفتاح من قبل المالك';
|
||||
}
|
||||
if (userType === 'tenant') {
|
||||
updates.tenantReceived = true;
|
||||
updates.notes = 'تم استلام العقار من قبل المستأجر';
|
||||
}
|
||||
|
||||
if ((userType === 'owner' && req.tenantReceived) ||
|
||||
(userType === 'tenant' && req.ownerDelivered)) {
|
||||
updates.status = 'active';
|
||||
updates.contractSigned = true;
|
||||
updates.actualStartDate = new Date().toISOString().split('T')[0];
|
||||
updates.notes = 'بدأت فترة الإيجار الفعلية';
|
||||
}
|
||||
|
||||
return { ...req, ...updates };
|
||||
}
|
||||
return req;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleEndRental = (requestId, userType) => {
|
||||
setRequests(prev =>
|
||||
prev.map(req => {
|
||||
if (req.id === requestId) {
|
||||
const updates = {};
|
||||
|
||||
if (userType === 'tenant') {
|
||||
updates.tenantLeft = true;
|
||||
updates.notes = 'غادر المستأجر العقار';
|
||||
}
|
||||
if (userType === 'owner') {
|
||||
updates.ownerReceived = true;
|
||||
updates.notes = 'استلم المالك العقار';
|
||||
}
|
||||
|
||||
if ((userType === 'tenant' && req.ownerReceived) ||
|
||||
(userType === 'owner' && req.tenantLeft)) {
|
||||
|
||||
const actualEndDate = new Date();
|
||||
const actualStartDate = new Date(req.actualStartDate || req.startDate);
|
||||
const actualDays = Math.ceil((actualEndDate - actualStartDate) / (1000 * 60 * 60 * 24));
|
||||
const actualAmount = req.dailyPrice * actualDays;
|
||||
|
||||
const damageDeduction = 0;
|
||||
const refundAmount = req.securityDeposit - damageDeduction;
|
||||
|
||||
updates.status = 'completed';
|
||||
updates.actualEndDate = actualEndDate.toISOString().split('T')[0];
|
||||
updates.actualDays = actualDays;
|
||||
updates.actualAmount = actualAmount;
|
||||
updates.securityDepositReturned = refundAmount;
|
||||
updates.damageDeduction = damageDeduction;
|
||||
updates.notes = `انتهى الإيجار بعد ${actualDays} يوم - المبلغ الفعلي: ${actualAmount.toLocaleString()} ل.س - مسترد الضمان: ${refundAmount.toLocaleString()} ل.س`;
|
||||
}
|
||||
|
||||
return { ...req, ...updates };
|
||||
}
|
||||
return req;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const filteredRequests = requests.filter(req =>
|
||||
filter === 'all' ? true : req.status === filter
|
||||
);
|
||||
|
||||
const stats = {
|
||||
total: requests.length,
|
||||
pending: requests.filter(r => r.status === 'pending').length,
|
||||
active: requests.filter(r => r.status === 'active').length,
|
||||
completed: requests.filter(r => r.status === 'completed').length
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid 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-2xl p-4 shadow-lg"
|
||||
>
|
||||
<div className="text-3xl font-bold mb-1">{stats.total}</div>
|
||||
<div className="text-sm opacity-90">إجمالي الطلبات</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-yellow-600 to-yellow-700 text-white rounded-2xl p-4 shadow-lg"
|
||||
>
|
||||
<div className="text-3xl font-bold mb-1">{stats.pending}</div>
|
||||
<div className="text-sm opacity-90">قيد الانتظار</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-purple-600 to-purple-700 text-white rounded-2xl p-4 shadow-lg"
|
||||
>
|
||||
<div className="text-3xl font-bold mb-1">{stats.active}</div>
|
||||
<div className="text-sm opacity-90">إيجارات نشطة</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-green-600 to-green-700 text-white rounded-2xl p-4 shadow-lg"
|
||||
>
|
||||
<div className="text-3xl font-bold mb-1">{stats.completed}</div>
|
||||
<div className="text-sm opacity-90">منتهية</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/80 backdrop-blur-sm rounded-2xl p-4 shadow-lg border border-white/20">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-bold text-gray-700">تصفية حسب الحالة</h3>
|
||||
<span className="text-sm text-gray-500">{filteredRequests.length} طلب</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ id: 'all', label: 'الكل', color: 'gray' },
|
||||
{ id: 'pending', label: 'قيد الانتظار', color: 'yellow' },
|
||||
{ id: 'owner_approved', label: 'موافقة المالك', color: 'blue' },
|
||||
{ id: 'admin_approved', label: 'موافقة الإدارة', color: 'green' },
|
||||
{ id: 'active', label: 'إيجارات نشطة', color: 'purple' },
|
||||
{ id: 'completed', label: 'منتهية', color: 'gray' }
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setFilter(tab.id)}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all transform hover:scale-105 ${
|
||||
filter === tab.id
|
||||
? `bg-${tab.color}-600 text-white shadow-lg`
|
||||
: `bg-${tab.color}-100 text-${tab.color}-800 hover:bg-${tab.color}-200`
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filteredRequests.map((request) => (
|
||||
<RequestCard
|
||||
key={request.id}
|
||||
request={request}
|
||||
onAction={handleAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredRequests.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"
|
||||
>
|
||||
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Clock className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد طلبات حجز</h3>
|
||||
<p className="text-gray-500">لا توجد طلبات حجز في هذه الفئة</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<ReasonDialog
|
||||
isOpen={reasonDialog.isOpen}
|
||||
onClose={() => setReasonDialog({ isOpen: false, requestId: null, type: null })}
|
||||
onConfirm={handleRejectWithReason}
|
||||
title={reasonDialog.type === 'owner' ? 'رفض من المالك' : 'رفض إداري'}
|
||||
/>
|
||||
|
||||
<RequestDetailsDialog
|
||||
request={detailsDialog.request}
|
||||
isOpen={detailsDialog.isOpen}
|
||||
onClose={() => setDetailsDialog({ isOpen: false, request: null })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
139
app/components/admin/DashboardStats.js
Normal file
139
app/components/admin/DashboardStats.js
Normal file
@ -0,0 +1,139 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
352
app/components/admin/LedgerBook.js
Normal file
352
app/components/admin/LedgerBook.js
Normal file
@ -0,0 +1,352 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
DollarSign,
|
||||
Calendar,
|
||||
User,
|
||||
Home,
|
||||
Download,
|
||||
Filter,
|
||||
Search,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Wallet,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
import { formatCurrency } from '@/app/utils/calculations';
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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 = () => {
|
||||
const csvContent = [
|
||||
['التاريخ', 'الوصف', 'من', 'إلى', 'المبلغ', 'العمولة', 'الحالة'],
|
||||
...filteredTransactions.map(t => [
|
||||
t.date,
|
||||
t.description,
|
||||
t.fromUser,
|
||||
t.toUser,
|
||||
t.amount,
|
||||
t.commission,
|
||||
t.status
|
||||
])
|
||||
].map(row => row.join(',')).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `ledger_${new Date().toISOString()}.csv`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<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-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="بحث في المعاملات..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border rounded-lg 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-3 py-2 border rounded-lg"
|
||||
/>
|
||||
<span className="text-gray-500 self-center">إلى</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateRange.end}
|
||||
onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
|
||||
className="px-3 py-2 border rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={exportToExcel}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2 hover:bg-green-700"
|
||||
>
|
||||
<Download 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">
|
||||
{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 ${
|
||||
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">
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
157
app/components/admin/PropertiesTable.js
Normal file
157
app/components/admin/PropertiesTable.js
Normal file
@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Edit,
|
||||
Trash2,
|
||||
Eye,
|
||||
MapPin,
|
||||
Bed,
|
||||
Bath,
|
||||
Square,
|
||||
DollarSign,
|
||||
Percent,
|
||||
MoreVertical
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function PropertiesTable() {
|
||||
const [properties, setProperties] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: 'luxuryVillaDamascus',
|
||||
type: 'villa',
|
||||
location: 'دمشق, المزة',
|
||||
price: 500000,
|
||||
commission: 5,
|
||||
commissionType: 'من المالك',
|
||||
bedrooms: 5,
|
||||
bathrooms: 4,
|
||||
area: 450,
|
||||
status: 'available',
|
||||
bookings: 3
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'modernApartmentAleppo',
|
||||
type: 'apartment',
|
||||
location: 'حلب, الشهباء',
|
||||
price: 250000,
|
||||
commission: 7,
|
||||
commissionType: 'من المستأجر',
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
area: 180,
|
||||
status: 'booked',
|
||||
bookings: 1
|
||||
}
|
||||
]);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<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}</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">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button className="p-1 hover:bg-blue-100 rounded text-blue-600">
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-1 hover:bg-amber-100 rounded text-amber-600">
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-1 hover:bg-red-100 rounded text-red-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-1 hover:bg-gray-100 rounded">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
190
app/components/admin/UsersList.js
Normal file
190
app/components/admin/UsersList.js
Normal file
@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
Calendar,
|
||||
Home,
|
||||
DollarSign,
|
||||
Search,
|
||||
Filter,
|
||||
Eye
|
||||
} from 'lucide-react';
|
||||
|
||||
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 filteredUsers = users.filter(user =>
|
||||
user.name.includes(searchTerm) ||
|
||||
user.email.includes(searchTerm) ||
|
||||
user.phone.includes(searchTerm)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="بحث عن مستخدم..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pr-10 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button className="px-4 py-2 border rounded-lg flex items-center gap-2 hover:bg-gray-50">
|
||||
<Filter className="w-4 h-4" />
|
||||
تصفية
|
||||
</button>
|
||||
</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.1 }}
|
||||
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<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-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold">{user.name}</h3>
|
||||
<div className="flex flex-wrap gap-3 mt-1 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-1">
|
||||
<Mail className="w-3 h-3" />
|
||||
{user.email}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Phone className="w-3 h-3" />
|
||||
{user.phone}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-blue-600">{user.totalBookings}</div>
|
||||
<div className="text-xs text-gray-500">إجمالي الحجوزات</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-green-600">{user.activeBookings}</div>
|
||||
<div className="text-xs text-gray-500">حجوزات نشطة</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg 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-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm flex items-center gap-1 hover:bg-blue-700"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
عرض التفاصيل
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedUser && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-white rounded-xl w-full max-w-2xl p-6"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold">تفاصيل المستخدم</h2>
|
||||
<button
|
||||
onClick={() => setSelectedUser(null)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">الاسم</label>
|
||||
<div className="font-medium">{selectedUser.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">البريد الإلكتروني</label>
|
||||
<div className="font-medium">{selectedUser.email}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">رقم الهاتف</label>
|
||||
<div className="font-medium">{selectedUser.phone}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">نوع الهوية</label>
|
||||
<div className="font-medium">
|
||||
{selectedUser.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">رقم الهوية</label>
|
||||
<div className="font-medium">{selectedUser.identityNumber}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">تاريخ التسجيل</label>
|
||||
<div className="font-medium">{selectedUser.joinDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-bold mb-3">سجل الحجوزات</h3>
|
||||
<p className="text-gray-500 text-center py-4">
|
||||
لا توجد حجوزات سابقة
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
app/components/property/BookingCalendar.js
Normal file
162
app/components/property/BookingCalendar.js
Normal file
@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
export default function BookingCalendar({ property, onDateSelect }) {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [selectedRange, setSelectedRange] = useState({ start: null, end: null });
|
||||
const [bookedDates, setBookedDates] = useState(property.bookings || []);
|
||||
|
||||
const daysInMonth = new Date(
|
||||
currentMonth.getFullYear(),
|
||||
currentMonth.getMonth() + 1,
|
||||
0
|
||||
).getDate();
|
||||
|
||||
const firstDayOfMonth = new Date(
|
||||
currentMonth.getFullYear(),
|
||||
currentMonth.getMonth(),
|
||||
1
|
||||
).getDay();
|
||||
|
||||
const monthNames = [
|
||||
'يناير', 'فبراير', 'مارس', 'إبريل', 'مايو', 'يونيو',
|
||||
'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'
|
||||
];
|
||||
|
||||
const isDateBooked = (date) => {
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
return bookedDates.some(booking => {
|
||||
const start = new Date(booking.startDate);
|
||||
const end = new Date(booking.endDate);
|
||||
const current = new Date(date);
|
||||
return current >= start && current <= end;
|
||||
});
|
||||
};
|
||||
|
||||
const isInSelectedRange = (date) => {
|
||||
if (!selectedRange.start || !selectedRange.end) return false;
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
return dateStr >= selectedRange.start && dateStr <= selectedRange.end;
|
||||
};
|
||||
|
||||
const handleDateClick = (date) => {
|
||||
if (isDateBooked(date)) return;
|
||||
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
if (!selectedRange.start || (selectedRange.start && selectedRange.end)) {
|
||||
setSelectedRange({ start: dateStr, end: null });
|
||||
} else {
|
||||
if (dateStr > selectedRange.start) {
|
||||
setSelectedRange({ ...selectedRange, end: dateStr });
|
||||
onDateSelect?.({ start: selectedRange.start, end: dateStr });
|
||||
} else {
|
||||
setSelectedRange({ start: dateStr, end: null });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeMonth = (direction) => {
|
||||
setCurrentMonth(new Date(
|
||||
currentMonth.getFullYear(),
|
||||
currentMonth.getMonth() + direction,
|
||||
1
|
||||
));
|
||||
};
|
||||
|
||||
const renderDays = () => {
|
||||
const days = [];
|
||||
const totalDays = daysInMonth + firstDayOfMonth;
|
||||
|
||||
for (let i = 0; i < totalDays; i++) {
|
||||
if (i < firstDayOfMonth) {
|
||||
days.push(<div key={`empty-${i}`} className="p-2" />);
|
||||
} else {
|
||||
const dayNumber = i - firstDayOfMonth + 1;
|
||||
const date = new Date(
|
||||
currentMonth.getFullYear(),
|
||||
currentMonth.getMonth(),
|
||||
dayNumber
|
||||
);
|
||||
|
||||
const isBooked = isDateBooked(date);
|
||||
const isSelected = isInSelectedRange(date);
|
||||
const isToday = date.toDateString() === new Date().toDateString();
|
||||
|
||||
days.push(
|
||||
<motion.button
|
||||
key={dayNumber}
|
||||
whileHover={!isBooked ? { scale: 1.1 } : {}}
|
||||
onClick={() => handleDateClick(date)}
|
||||
disabled={isBooked}
|
||||
className={`
|
||||
p-2 rounded-lg text-center transition-all
|
||||
${isBooked ? 'bg-gray-200 text-gray-400 cursor-not-allowed line-through' : 'hover:bg-amber-100 cursor-pointer'}
|
||||
${isSelected ? 'bg-amber-500 text-white hover:bg-amber-600' : ''}
|
||||
${isToday && !isSelected && !isBooked ? 'border-2 border-amber-500' : ''}
|
||||
`}
|
||||
>
|
||||
{dayNumber}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl p-6 shadow-lg">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<button
|
||||
onClick={() => changeMonth(-1)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||
<CalendarIcon className="w-5 h-5 text-amber-500" />
|
||||
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
||||
</h3>
|
||||
|
||||
<button
|
||||
onClick={() => changeMonth(1)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-1 mb-2 text-center text-sm font-semibold text-gray-600">
|
||||
<div>جمعة</div>
|
||||
<div>سبت</div>
|
||||
<div>أحد</div>
|
||||
<div>إثنين</div>
|
||||
<div>ثلاثاء</div>
|
||||
<div>أربعاء</div>
|
||||
<div>خميس</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{renderDays()}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mt-6 pt-4 border-t text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-amber-500 rounded" />
|
||||
<span>محدد</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-gray-200 rounded line-through" />
|
||||
<span>محجوز</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-amber-500 rounded" />
|
||||
<span>اليوم</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
app/contexts/PropertyContext.js
Normal file
141
app/contexts/PropertyContext.js
Normal file
@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useCallback } from 'react';
|
||||
|
||||
const PropertyContext = createContext();
|
||||
|
||||
export const useProperties = () => {
|
||||
const context = useContext(PropertyContext);
|
||||
if (!context) {
|
||||
throw new Error('useProperties must be used within PropertyProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const PropertyProvider = ({ children }) => {
|
||||
const [properties, setProperties] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const addProperty = useCallback(async (propertyData) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const newProperty = {
|
||||
id: Date.now().toString(),
|
||||
...propertyData,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
bookings: [],
|
||||
status: propertyData.status || 'available'
|
||||
};
|
||||
|
||||
setProperties(prev => [...prev, newProperty]);
|
||||
return newProperty;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateProperty = useCallback(async (id, updates) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
setProperties(prev =>
|
||||
prev.map(p => p.id === id
|
||||
? { ...p, ...updates, updatedAt: new Date().toISOString() }
|
||||
: p
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteProperty = useCallback(async (id) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setProperties(prev => prev.filter(p => p.id !== id));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getProperty = useCallback((id) => {
|
||||
return properties.find(p => p.id === id);
|
||||
}, [properties]);
|
||||
|
||||
const checkAvailability = useCallback((propertyId, startDate, endDate) => {
|
||||
const property = properties.find(p => p.id === propertyId);
|
||||
if (!property) return false;
|
||||
|
||||
const checkStart = new Date(startDate);
|
||||
const checkEnd = new Date(endDate);
|
||||
|
||||
return !property.bookings?.some(booking => {
|
||||
if (booking.status === 'cancelled' || booking.status === 'rejected') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookingStart = new Date(booking.startDate);
|
||||
const bookingEnd = new Date(booking.endDate);
|
||||
|
||||
return (
|
||||
(checkStart >= bookingStart && checkStart <= bookingEnd) ||
|
||||
(checkEnd >= bookingStart && checkEnd <= bookingEnd) ||
|
||||
(checkStart <= bookingStart && checkEnd >= bookingEnd)
|
||||
);
|
||||
});
|
||||
}, [properties]);
|
||||
|
||||
const getPropertiesByOwner = useCallback((ownerId) => {
|
||||
return properties.filter(p => p.ownerId === ownerId);
|
||||
}, [properties]);
|
||||
|
||||
const getAvailableProperties = useCallback(() => {
|
||||
return properties.filter(p => p.status === 'available');
|
||||
}, [properties]);
|
||||
|
||||
const updatePropertyStatus = useCallback(async (id, status) => {
|
||||
return updateProperty(id, { status });
|
||||
}, [updateProperty]);
|
||||
|
||||
const addBookingToProperty = useCallback(async (propertyId, bookingData) => {
|
||||
const property = getProperty(propertyId);
|
||||
if (!property) throw new Error('Property not found');
|
||||
|
||||
const updatedBookings = [...(property.bookings || []), bookingData];
|
||||
return updateProperty(propertyId, { bookings: updatedBookings });
|
||||
}, [getProperty, updateProperty]);
|
||||
|
||||
return (
|
||||
<PropertyContext.Provider value={{
|
||||
properties,
|
||||
loading,
|
||||
error,
|
||||
addProperty,
|
||||
updateProperty,
|
||||
deleteProperty,
|
||||
getProperty,
|
||||
checkAvailability,
|
||||
getPropertiesByOwner,
|
||||
getAvailableProperties,
|
||||
updatePropertyStatus,
|
||||
addBookingToProperty
|
||||
}}>
|
||||
{children}
|
||||
</PropertyContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -6,7 +6,7 @@ const resources = {
|
||||
en: {
|
||||
translation: {
|
||||
"home": "Home",
|
||||
"ourProducts": "Our Products",
|
||||
"ourProducts": "Our Properties",
|
||||
"admin": "Admin",
|
||||
"logoAlt": "SweetHome Logo",
|
||||
"brandNamePart1": "Sweet",
|
||||
@ -184,7 +184,7 @@ const resources = {
|
||||
translation: {
|
||||
|
||||
"home": "الرئيسية",
|
||||
"ourProducts": "منتجاتنا",
|
||||
"ourProducts": "عقاراتنا",
|
||||
"admin": "الإدارة",
|
||||
// "logoAlt": "شعار سويت هوم",
|
||||
"brandNamePart1": "سويت",
|
||||
|
||||
@ -104,7 +104,7 @@ export default function RootLayout({ children }) {
|
||||
<NavLink href="/">
|
||||
{t("home")}
|
||||
</NavLink>
|
||||
<NavLink href="/products">
|
||||
<NavLink href="/properties">
|
||||
{t("ourProducts")}
|
||||
</NavLink>
|
||||
<NavLink href="/admin">
|
||||
@ -182,7 +182,7 @@ export default function RootLayout({ children }) {
|
||||
<MobileNavLink href="/" onClick={closeMobileMenu}>
|
||||
{t("home")}
|
||||
</MobileNavLink>
|
||||
<MobileNavLink href="/products" onClick={closeMobileMenu}>
|
||||
<MobileNavLink href="/properties" onClick={closeMobileMenu}>
|
||||
{t("ourProducts")}
|
||||
</MobileNavLink>
|
||||
<MobileNavLink href="/admin" onClick={closeMobileMenu}>
|
||||
@ -229,7 +229,7 @@ export default function RootLayout({ children }) {
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/products" className="text-gray-400 hover:text-white transition-colors block py-1">
|
||||
<Link href="/properties" className="text-gray-400 hover:text-white transition-colors block py-1">
|
||||
{t("ourProducts")}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@ -61,7 +61,7 @@ export default function HomePage() {
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: 'url(/hero.jpg)',
|
||||
backgroundImage: 'url(/Home.jpg)',
|
||||
}}
|
||||
initial={{ scale: 1.1 }}
|
||||
animate={{ scale: 1 }}
|
||||
@ -176,7 +176,7 @@ export default function HomePage() {
|
||||
<option value="house">{t("house")}</option>
|
||||
<option value="villa">{t("villa")}</option>
|
||||
<option value="studio">{t("studio")}</option>
|
||||
<option value="penthouse">{t("penthouse")}</option>
|
||||
{/* <option value="penthouse">{t("penthouse")}</option> */}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
713
app/properties/page.js
Normal file
713
app/properties/page.js
Normal file
@ -0,0 +1,713 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Search,
|
||||
MapPin,
|
||||
Bed,
|
||||
Bath,
|
||||
Square,
|
||||
DollarSign,
|
||||
Filter,
|
||||
Grid3x3,
|
||||
List,
|
||||
Heart,
|
||||
Share2,
|
||||
ChevronDown,
|
||||
Star,
|
||||
Camera,
|
||||
Home,
|
||||
Building2,
|
||||
Trees,
|
||||
Waves,
|
||||
Warehouse,
|
||||
Sparkles,
|
||||
Shield,
|
||||
Calendar,
|
||||
Phone,
|
||||
Mail,
|
||||
MessageCircle
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const PropertyCard = ({ property, viewMode = 'grid' }) => {
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [currentImage, setCurrentImage] = useState(0);
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return amount?.toLocaleString() + ' ل.س';
|
||||
};
|
||||
|
||||
const getPropertyTypeIcon = (type) => {
|
||||
switch(type) {
|
||||
case 'villa': return <Home className="w-4 h-4" />;
|
||||
case 'apartment': return <Building2 className="w-4 h-4" />;
|
||||
case 'house': return <Home className="w-4 h-4" />;
|
||||
case 'studio': return <Building2 className="w-4 h-4" />;
|
||||
default: return <Home className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPropertyTypeLabel = (type) => {
|
||||
switch(type) {
|
||||
case 'villa': return 'فيلا';
|
||||
case 'apartment': return 'شقة';
|
||||
case 'house': return 'بيت';
|
||||
case 'studio': return 'استوديو';
|
||||
default: return type;
|
||||
}
|
||||
};
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden border border-gray-100"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row">
|
||||
<div className="md:w-1/3 relative h-64 md:h-auto bg-gray-100">
|
||||
<Image
|
||||
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
||||
alt={property.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
{property.images.length > 1 && (
|
||||
<div className="absolute bottom-2 left-2 right-2 flex justify-center gap-1">
|
||||
{property.images.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentImage(idx)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||||
idx === currentImage ? 'bg-gray-800 w-3' : 'bg-white/70'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-2 right-2 flex gap-2">
|
||||
<button
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
className="w-8 h-8 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-sm"
|
||||
>
|
||||
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
|
||||
</button>
|
||||
</div>
|
||||
{property.isNew && (
|
||||
<div className="absolute top-2 left-2 bg-gray-800 text-white px-2 py-1 rounded-lg text-xs font-medium">
|
||||
جديد
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:w-2/3 p-6">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium flex items-center gap-1">
|
||||
{getPropertyTypeIcon(property.type)}
|
||||
{getPropertyTypeLabel(property.type)}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
|
||||
property.status === 'available'
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{property.status === 'available' ? 'متاح' : 'محجوز'}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-1">{property.title}</h3>
|
||||
<div className="flex items-center gap-1 text-gray-500 text-sm mb-3">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{property.location.city}، {property.location.district}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-2xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
||||
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mb-4">
|
||||
<div className="flex items-center gap-1 text-gray-600">
|
||||
<Bed className="w-4 h-4" />
|
||||
<span>{property.bedrooms} غرف</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-gray-600">
|
||||
<Bath className="w-4 h-4" />
|
||||
<span>{property.bathrooms} حمامات</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-gray-600">
|
||||
<Square className="w-4 h-4" />
|
||||
<span>{property.area} م²</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
|
||||
{property.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{property.features.slice(0, 4).map((feature, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
{property.features.length > 4 && (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
|
||||
+{property.features.length - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href={`/property/${property.id}`}
|
||||
className="flex-1 bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
|
||||
>
|
||||
عرض التفاصيل
|
||||
</Link>
|
||||
<button className="px-4 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2">
|
||||
<Phone className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 overflow-hidden border border-gray-100"
|
||||
>
|
||||
<div className="relative h-56 bg-gray-100">
|
||||
<Image
|
||||
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
||||
alt={property.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
{property.images.length > 1 && (
|
||||
<div className="absolute bottom-2 left-2 right-2 flex justify-center gap-1">
|
||||
{property.images.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentImage(idx)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||||
idx === currentImage ? 'bg-gray-800 w-3' : 'bg-white/70'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-2 right-2 flex gap-2">
|
||||
<button
|
||||
onClick={() => setIsFavorite(!isFavorite)}
|
||||
className="w-8 h-8 bg-white/90 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-sm"
|
||||
>
|
||||
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
|
||||
</button>
|
||||
</div>
|
||||
{property.isNew && (
|
||||
<div className="absolute top-2 left-2 bg-gray-800 text-white px-2 py-1 rounded-lg text-xs font-medium">
|
||||
جديد
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium flex items-center gap-1">
|
||||
{getPropertyTypeIcon(property.type)}
|
||||
{getPropertyTypeLabel(property.type)}
|
||||
</span>
|
||||
{property.status === 'available' && (
|
||||
<span className="px-2 py-1 bg-gray-800 text-white rounded-lg text-xs font-medium">
|
||||
متاح
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-bold text-gray-900 mb-1 line-clamp-1">{property.title}</h3>
|
||||
<div className="flex items-center gap-1 text-gray-500 text-xs mb-2">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span className="line-clamp-1">{property.location.city}، {property.location.district}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
||||
<div className="text-xs text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-3 text-gray-600 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<Bed className="w-4 h-4" />
|
||||
<span>{property.bedrooms}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Bath className="w-4 h-4" />
|
||||
<span>{property.bathrooms}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Square className="w-4 h-4" />
|
||||
<span>{property.area}م²</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="w-4 h-4 fill-gray-400 text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700">{property.rating || 4.5}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{property.features.slice(0, 3).map((feature, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
{property.features.length > 3 && (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-lg text-xs">
|
||||
+{property.features.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/property/${property.id}`}
|
||||
className="block w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors text-center"
|
||||
>
|
||||
عرض التفاصيل
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterBar = ({ filters, onFilterChange }) => {
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
const propertyTypes = [
|
||||
{ id: 'all', label: 'الكل' },
|
||||
{ id: 'apartment', label: 'شقة', icon: Building2 },
|
||||
{ id: 'villa', label: 'فيلا', icon: Home },
|
||||
{ id: 'house', label: 'بيت', icon: Home },
|
||||
{ id: 'studio', label: 'استوديو', icon: Building2 }
|
||||
];
|
||||
|
||||
const priceRanges = [
|
||||
{ id: 'all', label: 'جميع الأسعار' },
|
||||
{ id: '0-500000', label: 'أقل من 500,000' },
|
||||
{ id: '500000-1000000', label: '500,000 - 1,000,000' },
|
||||
{ id: '1000000-2000000', label: '1,000,000 - 2,000,000' },
|
||||
{ id: '2000000-5000000', label: '2,000,000 - 5,000,000' },
|
||||
{ id: '5000000+', label: 'أكثر من 5,000,000' }
|
||||
];
|
||||
|
||||
const cities = [
|
||||
{ id: 'all', label: 'جميع المدن' },
|
||||
{ id: 'damascus', label: 'دمشق' },
|
||||
{ id: 'aleppo', label: 'حلب' },
|
||||
{ id: 'homs', label: 'حمص' },
|
||||
{ id: 'latakia', label: 'اللاذقية' },
|
||||
{ id: 'daraa', label: 'درعا' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
|
||||
<div className="flex flex-col md:flex-row gap-3 mb-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="ابحث عن عقار..."
|
||||
className="w-full pr-12 px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 transition-all"
|
||||
value={filters.search}
|
||||
onChange={(e) => onFilterChange({ ...filters, search: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="px-6 py-3 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2 text-gray-700"
|
||||
>
|
||||
<Filter className="w-5 h-5" />
|
||||
فلاتر متقدمة
|
||||
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{showFilters && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">نوع العقار</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{propertyTypes.map((type) => {
|
||||
const Icon = type.icon;
|
||||
return (
|
||||
<button
|
||||
key={type.id}
|
||||
onClick={() => onFilterChange({ ...filters, propertyType: type.id })}
|
||||
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all flex items-center gap-1 ${
|
||||
filters.propertyType === type.id
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{Icon && <Icon className="w-4 h-4" />}
|
||||
{type.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">المدينة</label>
|
||||
<select
|
||||
value={filters.city}
|
||||
onChange={(e) => onFilterChange({ ...filters, city: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
>
|
||||
{cities.map((city) => (
|
||||
<option key={city.id} value={city.id}>{city.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">نطاق السعر</label>
|
||||
<select
|
||||
value={filters.priceRange}
|
||||
onChange={(e) => onFilterChange({ ...filters, priceRange: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
>
|
||||
{priceRanges.map((range) => (
|
||||
<option key={range.id} value={range.id}>{range.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">غرف النوم</label>
|
||||
<select
|
||||
value={filters.bedrooms}
|
||||
onChange={(e) => onFilterChange({ ...filters, bedrooms: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
>
|
||||
<option value="all">جميع الأعداد</option>
|
||||
<option value="1">1+</option>
|
||||
<option value="2">2+</option>
|
||||
<option value="3">3+</option>
|
||||
<option value="4">4+</option>
|
||||
<option value="5">5+</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">المساحة (م²)</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="من"
|
||||
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
value={filters.minArea}
|
||||
onChange={(e) => onFilterChange({ ...filters, minArea: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="إلى"
|
||||
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
value={filters.maxArea}
|
||||
onChange={(e) => onFilterChange({ ...filters, maxArea: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">المميزات</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['مسبح', 'حديقة', 'موقف سيارات', 'أمن', 'مصعد', 'تكييف'].map((feature) => (
|
||||
<button
|
||||
key={feature}
|
||||
onClick={() => {
|
||||
const newFeatures = filters.features.includes(feature)
|
||||
? filters.features.filter(f => f !== feature)
|
||||
: [...filters.features, feature];
|
||||
onFilterChange({ ...filters, features: newFeatures });
|
||||
}}
|
||||
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all ${
|
||||
filters.features.includes(feature)
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{feature}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-4 pt-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => onFilterChange({
|
||||
search: '',
|
||||
propertyType: 'all',
|
||||
city: 'all',
|
||||
priceRange: 'all',
|
||||
bedrooms: 'all',
|
||||
minArea: '',
|
||||
maxArea: '',
|
||||
features: []
|
||||
})}
|
||||
className="px-6 py-2 bg-gray-100 rounded-xl font-medium hover:bg-gray-200 transition-colors text-gray-700"
|
||||
>
|
||||
إعادة تعيين
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowFilters(false)}
|
||||
className="px-6 py-2 bg-gray-800 text-white rounded-xl font-medium hover:bg-gray-900 transition-colors"
|
||||
>
|
||||
تطبيق الفلاتر
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PropertiesPage() {
|
||||
const [viewMode, setViewMode] = useState('grid');
|
||||
const [sortBy, setSortBy] = useState('newest');
|
||||
const [filters, setFilters] = useState({
|
||||
search: '',
|
||||
propertyType: 'all',
|
||||
city: 'all',
|
||||
priceRange: 'all',
|
||||
bedrooms: 'all',
|
||||
minArea: '',
|
||||
maxArea: '',
|
||||
features: []
|
||||
});
|
||||
|
||||
const [properties] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: 'فيلا فاخرة في المزة',
|
||||
description: 'فيلا فاخرة مع حديقة خاصة ومسبح في أفضل أحياء دمشق.',
|
||||
type: 'villa',
|
||||
price: 500000,
|
||||
priceUnit: 'daily',
|
||||
location: { city: 'دمشق', district: 'المزة' },
|
||||
bedrooms: 5,
|
||||
bathrooms: 4,
|
||||
area: 450,
|
||||
features: ['مسبح', 'حديقة خاصة', 'موقف سيارات', 'أمن'],
|
||||
images: ['/villa1.jpg'],
|
||||
status: 'available',
|
||||
rating: 4.8,
|
||||
isNew: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'شقة حديثة في الشهباء',
|
||||
description: 'شقة عصرية في حي الشهباء الراقي بحلب.',
|
||||
type: 'apartment',
|
||||
price: 250000,
|
||||
priceUnit: 'daily',
|
||||
location: { city: 'حلب', district: 'الشهباء' },
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
area: 180,
|
||||
features: ['مطبخ مجهز', 'بلكونة', 'موقف سيارات', 'مصعد'],
|
||||
images: ['/apartment1.jpg'],
|
||||
status: 'available',
|
||||
rating: 4.5,
|
||||
isNew: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'بيت عائلي في بابا عمرو',
|
||||
description: 'بيت واسع مناسب للعائلات في حمص.',
|
||||
type: 'house',
|
||||
price: 350000,
|
||||
priceUnit: 'daily',
|
||||
location: { city: 'حمص', district: 'بابا عمرو' },
|
||||
bedrooms: 4,
|
||||
bathrooms: 3,
|
||||
area: 300,
|
||||
features: ['حديقة كبيرة', 'موقف سيارات', 'مدفأة'],
|
||||
images: ['/house1.jpg'],
|
||||
status: 'booked',
|
||||
rating: 4.3,
|
||||
isNew: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'شقة بجانب البحر',
|
||||
description: 'شقة رائعة مع إطلالة بحرية في اللاذقية.',
|
||||
type: 'apartment',
|
||||
price: 300000,
|
||||
priceUnit: 'daily',
|
||||
location: { city: 'اللاذقية', district: 'الشاطئ الأزرق' },
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
area: 200,
|
||||
features: ['إطلالة بحرية', 'شرفة', 'تكييف'],
|
||||
images: ['/seaside1.jpg'],
|
||||
status: 'available',
|
||||
rating: 4.9,
|
||||
isNew: true
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'فيلا في درعا',
|
||||
description: 'فيلا فاخرة في حي الأطباء بدرعا.',
|
||||
type: 'villa',
|
||||
price: 400000,
|
||||
priceUnit: 'daily',
|
||||
location: { city: 'درعا', district: 'حي الأطباء' },
|
||||
bedrooms: 4,
|
||||
bathrooms: 3,
|
||||
area: 350,
|
||||
features: ['حديقة مثمرة', 'أنظمة أمن', 'مسبح'],
|
||||
images: ['/villa4.jpg'],
|
||||
status: 'available',
|
||||
rating: 4.6,
|
||||
isNew: false
|
||||
}
|
||||
]);
|
||||
|
||||
const filteredProperties = properties
|
||||
.filter(property => {
|
||||
if (filters.search && !property.title.includes(filters.search) && !property.description.includes(filters.search)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.propertyType !== 'all' && property.type !== filters.propertyType) {
|
||||
return false;
|
||||
}
|
||||
if (filters.city !== 'all' && property.location.city !== filters.city) {
|
||||
return false;
|
||||
}
|
||||
if (filters.priceRange !== 'all') {
|
||||
const [min, max] = filters.priceRange.split('-');
|
||||
if (max) {
|
||||
if (property.price < parseInt(min) || property.price > parseInt(max)) return false;
|
||||
} else if (filters.priceRange.endsWith('+')) {
|
||||
const min = parseInt(filters.priceRange.replace('+', ''));
|
||||
if (property.price < min) return false;
|
||||
}
|
||||
}
|
||||
if (filters.bedrooms !== 'all' && property.bedrooms < parseInt(filters.bedrooms)) {
|
||||
return false;
|
||||
}
|
||||
if (filters.minArea && property.area < parseInt(filters.minArea)) return false;
|
||||
if (filters.maxArea && property.area > parseInt(filters.maxArea)) return false;
|
||||
if (filters.features.length > 0) {
|
||||
if (!filters.features.every(f => property.features.includes(f))) return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
switch(sortBy) {
|
||||
case 'price_asc': return a.price - b.price;
|
||||
case 'price_desc': return b.price - a.price;
|
||||
case 'rating': return b.rating - a.rating;
|
||||
default: return b.isNew ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-8"
|
||||
>
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">عقارات للإيجار</h1>
|
||||
<p className="text-gray-500">أفضل العقارات في سوريا</p>
|
||||
</motion.div>
|
||||
|
||||
<FilterBar filters={filters} onFilterChange={setFilters} />
|
||||
|
||||
<div className="flex justify-between items-center my-6">
|
||||
<div className="text-gray-600">
|
||||
<span className="font-bold text-gray-900">{filteredProperties.length}</span> عقار متاح
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300 text-gray-700"
|
||||
>
|
||||
<option value="newest">الأحدث</option>
|
||||
<option value="price_asc">السعر: من الأقل</option>
|
||||
<option value="price_desc">السعر: من الأعلى</option>
|
||||
<option value="rating">التقييم</option>
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded-xl transition-colors ${
|
||||
viewMode === 'grid' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Grid3x3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded-xl transition-colors ${
|
||||
viewMode === 'list' ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<List className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={viewMode === 'grid'
|
||||
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'
|
||||
: 'space-y-4'
|
||||
}>
|
||||
{filteredProperties.map((property) => (
|
||||
<PropertyCard key={property.id} property={property} viewMode={viewMode} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredProperties.length === 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-center py-16"
|
||||
>
|
||||
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Home className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد عقارات</h3>
|
||||
<p className="text-gray-500">جرب تغيير معايير البحث</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
681
app/property/[id]/page.js
Normal file
681
app/property/[id]/page.js
Normal file
@ -0,0 +1,681 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import {
|
||||
MapPin,
|
||||
Bed,
|
||||
Bath,
|
||||
Square,
|
||||
DollarSign,
|
||||
Heart,
|
||||
Share2,
|
||||
Phone,
|
||||
Mail,
|
||||
MessageCircle,
|
||||
Calendar,
|
||||
Shield,
|
||||
Star,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Check,
|
||||
X,
|
||||
Wifi,
|
||||
Car,
|
||||
Coffee,
|
||||
Wind,
|
||||
Thermometer,
|
||||
Lock,
|
||||
Camera,
|
||||
Home,
|
||||
Building2,
|
||||
Users,
|
||||
Ruler,
|
||||
CalendarDays,
|
||||
Clock,
|
||||
Award,
|
||||
FileText,
|
||||
Printer,
|
||||
Download,
|
||||
ArrowLeft
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function PropertyDetailsPage() {
|
||||
const params = useParams();
|
||||
const [currentImage, setCurrentImage] = useState(0);
|
||||
const [showContact, setShowContact] = useState(false);
|
||||
const [bookingDates, setBookingDates] = useState({ start: '', end: '' });
|
||||
const [selectedDuration, setSelectedDuration] = useState(1);
|
||||
const [property, setProperty] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const propertiesData = {
|
||||
1: {
|
||||
id: 1,
|
||||
title: 'فيلا فاخرة في المزة',
|
||||
description: `تتميز هذه الفيلا الفاخرة بتصميمها العصري وموقعها المميز في أفضل أحياء دمشق. تم بناء الفيلا بأعلى المواصفات باستخدام أفضل المواد، مع مساحات واسعة وحديقة خاصة.
|
||||
|
||||
المميزات الرئيسية:
|
||||
• موقع راقي وقريب من جميع الخدمات
|
||||
• تصميم داخلي عصري مع أثاث فاخر
|
||||
• إطلالة رائعة على المدينة
|
||||
• خصوصية تامة وأمن على مدار الساعة
|
||||
|
||||
المساحات الداخلية:
|
||||
• الطابق الأرضي: صالة استقبال كبيرة (80 م²)، مجلس رجال (40 م²)، مجلس نساء (35 م²)، مطبخ (25 م²)، غرفة طعام (30 م²)
|
||||
• الطابق الأول: 5 غرف نوم ماستر مع حمامات خاصة (كل غرفة 35-45 م²)
|
||||
• الطابق الثاني: غرفة معيشة عائلية (50 م²)، غرفة ترفيه (40 م²)، سطح مع إطلالة (100 م²)
|
||||
|
||||
الخدمات القريبة:
|
||||
• مدارس وجامعات على بعد 5 دقائق
|
||||
• مستشفيات ومراكز طبية
|
||||
• مولات تجارية ومطاعم
|
||||
• حدائق عامة ومسارات مشي`,
|
||||
type: 'villa',
|
||||
price: 500000,
|
||||
priceUnit: 'daily',
|
||||
location: {
|
||||
city: 'دمشق',
|
||||
district: 'المزة',
|
||||
address: 'شارع المزة - فيلات غربية',
|
||||
lat: 33.5,
|
||||
lng: 36.3
|
||||
},
|
||||
bedrooms: 5,
|
||||
bathrooms: 4,
|
||||
area: 450,
|
||||
features: [
|
||||
{ name: 'مسبح', available: true, description: 'مسبح خاص بمساحة 40 م²' },
|
||||
{ name: 'حديقة خاصة', available: true, description: 'حديقة بمساحة 200 م² مع نوافير' },
|
||||
{ name: 'موقف سيارات', available: true, description: 'موقف يتسع لـ 4 سيارات' },
|
||||
{ name: 'أمن 24/7', available: true, description: 'كاميرات مراقبة وحراسة' },
|
||||
{ name: 'تدفئة مركزية', available: true, description: 'تدفئة مركزية لجميع الغرف' },
|
||||
{ name: 'تكييف مركزي', available: true, description: 'تكييف مركزي في جميع الغرف' },
|
||||
{ name: 'مطبخ مجهز', available: true, description: 'مطبخ أمريكي مجهز بالكامل' },
|
||||
{ name: 'غرفة خادمة', available: true, description: 'غرفة خادمة مع حمام خاص' },
|
||||
{ name: 'مصعد', available: false, description: 'قابل للتركيب' },
|
||||
{ name: 'واي فاي', available: true, description: 'ألياف بصرية' }
|
||||
],
|
||||
images: [
|
||||
'/villa1.jpg',
|
||||
'/villa2.jpg',
|
||||
'/villa3.jpg',
|
||||
'/villa4.jpg',
|
||||
'/villa5.jpg',
|
||||
'/villa6.jpg'
|
||||
],
|
||||
status: 'available',
|
||||
rating: 4.8,
|
||||
reviews: 24,
|
||||
reviewList: [
|
||||
{ user: 'أحمد محمد', rating: 5, comment: 'فيلا رائعة ونظيفة، موقع ممتاز', date: '2024-01-15' },
|
||||
{ user: 'سارة أحمد', rating: 5, comment: 'إقامة مريحة، خدمات ممتازة', date: '2024-01-10' },
|
||||
{ user: 'خالد عمر', rating: 4, comment: 'مكان جميل ولكن السعر مرتفع قليلاً', date: '2023-12-20' }
|
||||
],
|
||||
owner: {
|
||||
name: 'محمد الخالد',
|
||||
phone: '0933111222',
|
||||
email: 'mohamed@example.com',
|
||||
rating: 4.9,
|
||||
properties: 5,
|
||||
memberSince: '2023',
|
||||
responseRate: '98%',
|
||||
responseTime: 'خلال ساعة'
|
||||
},
|
||||
nearby: [
|
||||
{ type: 'مدرسة', distance: '500م' },
|
||||
{ type: 'مستشفى', distance: '1كم' },
|
||||
{ type: 'مول تجاري', distance: '2كم' },
|
||||
{ type: 'مطعم', distance: '300م' },
|
||||
{ type: 'جامعة', distance: '1.5كم' },
|
||||
{ type: 'حديقة', distance: '800م' }
|
||||
],
|
||||
specifications: {
|
||||
constructionYear: 2022,
|
||||
floor: 'أرضي + 2',
|
||||
parking: 4,
|
||||
gardenArea: 200,
|
||||
poolArea: 40,
|
||||
furnished: true,
|
||||
airConditioning: 'مركزي',
|
||||
heating: 'مركزي',
|
||||
electricity: '220V',
|
||||
water: 'شبكة عامة'
|
||||
},
|
||||
rules: [
|
||||
'لا يسمح بالحيوانات الأليفة',
|
||||
'لا يسمح بالتدخين داخل الغرف',
|
||||
'حفلات مسموحة بعد التنسيق',
|
||||
'وقت المغادرة: 12:00 ظهراً'
|
||||
]
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
title: 'شقة حديثة في الشهباء',
|
||||
description: 'شقة عصرية في حي الشهباء الراقي بحلب. إطلالة رائعة وتشطيب فاخر.',
|
||||
type: 'apartment',
|
||||
price: 250000,
|
||||
priceUnit: 'daily',
|
||||
location: {
|
||||
city: 'حلب',
|
||||
district: 'الشهباء',
|
||||
address: 'شارع النيل - بناء الرحاب',
|
||||
lat: 36.2,
|
||||
lng: 37.1
|
||||
},
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
area: 180,
|
||||
features: [
|
||||
{ name: 'مطبخ مجهز', available: true, description: 'مطبخ أمريكي' },
|
||||
{ name: 'بلكونة', available: true, description: 'بلكونة بمساحة 10 م²' },
|
||||
{ name: 'موقف سيارات', available: true, description: 'موقف خاص' },
|
||||
{ name: 'مصعد', available: true, description: 'مصعد حديث' }
|
||||
],
|
||||
images: ['/apartment1.jpg', '/apartment2.jpg'],
|
||||
status: 'available',
|
||||
rating: 4.5,
|
||||
reviews: 12,
|
||||
owner: {
|
||||
name: 'أحمد حلبي',
|
||||
phone: '0944222333',
|
||||
email: 'ahmad@example.com',
|
||||
rating: 4.7,
|
||||
properties: 3,
|
||||
memberSince: '2023'
|
||||
},
|
||||
nearby: [
|
||||
{ type: 'مدرسة', distance: '300م' },
|
||||
{ type: 'مستشفى', distance: '1.2كم' },
|
||||
{ type: 'مول', distance: '500م' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setProperty(propertiesData[params.id] || propertiesData[1]);
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
}, [params.id]);
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return amount?.toLocaleString() + ' ل.س';
|
||||
};
|
||||
|
||||
const calculateTotalPrice = () => {
|
||||
if (!property) return 0;
|
||||
const days = bookingDates.start && bookingDates.end
|
||||
? Math.ceil((new Date(bookingDates.end) - new Date(bookingDates.start)) / (1000 * 60 * 60 * 24))
|
||||
: selectedDuration;
|
||||
return property.price * (days > 0 ? days : 1);
|
||||
};
|
||||
|
||||
const handleBooking = () => {
|
||||
alert('تم إرسال طلب الحجز بنجاح. سيتم التواصل معك قريباً.');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-gray-200 border-t-gray-800 rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">جاري تحميل تفاصيل العقار...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!property) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Home className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">العقار غير موجود</h2>
|
||||
<p className="text-gray-600 mb-4">لم نتمكن من العثور على العقار المطلوب</p>
|
||||
<Link href="/properties" className="bg-gray-800 text-white px-6 py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors">
|
||||
العودة إلى العقارات
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-white border-b sticky top-16 z-40 shadow-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link href="/properties" className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>العودة إلى العقارات</span>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||
<Heart className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||
<Share2 className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="relative h-[500px] rounded-2xl overflow-hidden group bg-gray-100">
|
||||
<Image
|
||||
src={property.images[currentImage] || '/property-placeholder.jpg'}
|
||||
alt={property.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
{property.images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setCurrentImage(prev => Math.max(0, prev - 1))}
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-white"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentImage(prev => Math.min(property.images.length - 1, prev + 1))}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-white"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2">
|
||||
{property.images.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentImage(idx)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
idx === currentImage ? 'bg-gray-800 w-4' : 'bg-white/70 hover:bg-white'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 bg-black/50 text-white px-3 py-1 rounded-full text-sm backdrop-blur-sm">
|
||||
<Camera className="w-4 h-4 inline ml-1" />
|
||||
{currentImage + 1} / {property.images.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{property.images.slice(1, 5).map((img, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => setCurrentImage(idx + 1)}
|
||||
className="relative h-[240px] rounded-2xl overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-gray-100"
|
||||
>
|
||||
<Image src={img} alt={`${property.title} ${idx + 2}`} fill className="object-cover" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{property.title}</h1>
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<MapPin className="w-5 h-5" />
|
||||
<span>{property.location.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-3xl font-bold text-gray-900">{formatCurrency(property.price)}</div>
|
||||
<div className="text-sm text-gray-500">/{property.priceUnit === 'daily' ? 'يوم' : 'شهر'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="w-5 h-5 fill-gray-800 text-gray-800" />
|
||||
<span className="font-bold text-gray-900">{property.rating}</span>
|
||||
<span className="text-gray-500">({property.reviews} تقييم)</span>
|
||||
</div>
|
||||
<div className="w-px h-4 bg-gray-200" />
|
||||
<span className={`font-medium ${
|
||||
property.status === 'available' ? 'text-gray-800' : 'text-gray-500'
|
||||
}`}>
|
||||
{property.status === 'available' ? 'متاح للإيجار' : 'محجوز حالياً'}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">المواصفات الرئيسية</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||
<Bed className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||
<div className="font-bold text-gray-900">{property.bedrooms}</div>
|
||||
<div className="text-sm text-gray-500">غرف نوم</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||
<Bath className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||
<div className="font-bold text-gray-900">{property.bathrooms}</div>
|
||||
<div className="text-sm text-gray-500">حمامات</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||
<Square className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||
<div className="font-bold text-gray-900">{property.area}</div>
|
||||
<div className="text-sm text-gray-500">م²</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
||||
<Home className="w-6 h-6 text-gray-700 mx-auto mb-2" />
|
||||
<div className="font-bold text-gray-900">
|
||||
{property.type === 'villa' ? 'فيلا' :
|
||||
property.type === 'apartment' ? 'شقة' : 'بيت'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">نوع العقار</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{property.specifications && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>بناء: {property.specifications.constructionYear}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Ruler className="w-4 h-4" />
|
||||
<span>حديقة: {property.specifications.gardenArea} م²</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Car className="w-4 h-4" />
|
||||
<span>موقف: {property.specifications.parking}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Wind className="w-4 h-4" />
|
||||
<span>{property.specifications.airConditioning}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">وصف العقار</h2>
|
||||
<p className="text-gray-600 whitespace-pre-line leading-relaxed">{property.description}</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">المميزات والخدمات</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{property.features.map((feature, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 p-3 bg-gray-50 rounded-xl">
|
||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
feature.available ? 'bg-gray-800 text-white' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{feature.available ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<X className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{feature.icon}</span>
|
||||
<span className={`font-medium ${feature.available ? 'text-gray-900' : 'text-gray-400'}`}>
|
||||
{feature.name}
|
||||
</span>
|
||||
</div>
|
||||
{feature.description && (
|
||||
<p className={`text-sm mt-1 ${feature.available ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||
{feature.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">القرب من الخدمات</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{property.nearby.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{item.icon}</span>
|
||||
<span className="text-gray-700">{item.type}</span>
|
||||
</div>
|
||||
<span className="font-medium text-gray-900">{item.distance}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{property.reviewList && property.reviewList.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">تقييمات المستأجرين</h2>
|
||||
<div className="space-y-4">
|
||||
{property.reviewList.map((review, idx) => (
|
||||
<div key={idx} className="border-b border-gray-100 last:border-0 pb-4 last:pb-0">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<span className="font-bold text-gray-900">{review.user}</span>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star key={i} className={`w-4 h-4 ${
|
||||
i < review.rating ? 'fill-gray-800 text-gray-800' : 'text-gray-300'
|
||||
}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{review.date}</span>
|
||||
</div>
|
||||
<p className="text-gray-600">{review.comment}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{property.rules && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">قوانين المنزل</h2>
|
||||
<ul className="space-y-2">
|
||||
{property.rules.map((rule, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2 text-gray-600">
|
||||
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full" />
|
||||
{rule}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="sticky top-28">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 mb-6"
|
||||
>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-900">احجز هذا العقار</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">اختر المدة (أيام)</label>
|
||||
<div className="flex gap-2">
|
||||
{[1, 3, 7, 14, 30].map(days => (
|
||||
<button
|
||||
key={days}
|
||||
onClick={() => setSelectedDuration(days)}
|
||||
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors ${
|
||||
selectedDuration === days
|
||||
? 'bg-gray-800 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{days}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">تاريخ البداية</label>
|
||||
<input
|
||||
type="date"
|
||||
value={bookingDates.start}
|
||||
onChange={(e) => setBookingDates({ ...bookingDates, start: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">تاريخ النهاية</label>
|
||||
<input
|
||||
type="date"
|
||||
value={bookingDates.end}
|
||||
onChange={(e) => setBookingDates({ ...bookingDates, end: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-gray-300 focus:border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-xl mb-6">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-gray-600">السعر لـ {selectedDuration} أيام</span>
|
||||
<span className="font-bold text-gray-900">{formatCurrency(property.price * selectedDuration)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-gray-600">سلفة ضمان</span>
|
||||
<span className="font-bold text-gray-900">{formatCurrency(500000)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between pt-2 border-t border-gray-200 font-bold">
|
||||
<span className="text-gray-900">الإجمالي</span>
|
||||
<span className="text-gray-900">{formatCurrency(property.price * selectedDuration + 500000)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleBooking}
|
||||
className="w-full bg-gray-800 text-white py-4 rounded-xl font-bold text-lg hover:bg-gray-900 transition-colors mb-4"
|
||||
>
|
||||
تأكيد الحجز
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Shield className="w-4 h-4 text-gray-600" />
|
||||
<span>الدفع آمن ومضمون. سلفة الضمان قابلة للاسترداد.</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
|
||||
>
|
||||
<h3 className="font-bold mb-4 text-gray-900">معلومات المالك</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-xl font-bold text-gray-700">
|
||||
{property.owner.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-gray-900">{property.owner.name}</div>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-500">
|
||||
<Star className="w-3 h-3 fill-gray-600 text-gray-600" />
|
||||
<span>{property.owner.rating}</span>
|
||||
<span>· {property.owner.properties} عقارات</span>
|
||||
</div>
|
||||
{property.owner.responseRate && (
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500 mt-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>استجابة: {property.owner.responseRate}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showContact ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-xl">
|
||||
<Phone className="w-4 h-4 text-gray-600" />
|
||||
<span className="font-medium text-gray-900">{property.owner.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-xl">
|
||||
<Mail className="w-4 h-4 text-gray-600" />
|
||||
<span className="font-medium text-gray-900">{property.owner.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowContact(true)}
|
||||
className="w-full bg-gray-800 text-white py-3 rounded-xl font-medium hover:bg-gray-900 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<Phone className="w-5 h-5" />
|
||||
عرض معلومات الاتصال
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button className="w-full mt-3 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
مراسلة المالك
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
app/utils/PropertyContext.js
Normal file
103
app/utils/PropertyContext.js
Normal file
@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useCallback } from 'react';
|
||||
|
||||
const PropertyContext = createContext();
|
||||
|
||||
export const useProperties = () => {
|
||||
const context = useContext(PropertyContext);
|
||||
if (!context) {
|
||||
throw new Error('useProperties must be used within PropertyProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const PropertyProvider = ({ children }) => {
|
||||
const [properties, setProperties] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const addProperty = useCallback(async (propertyData) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const newProperty = {
|
||||
id: Date.now().toString(),
|
||||
...propertyData,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'available',
|
||||
bookings: [],
|
||||
commission: {
|
||||
rate: propertyData.commissionRate || 5,
|
||||
type: propertyData.commissionType || 'from_owner',
|
||||
isActive: true
|
||||
}
|
||||
};
|
||||
|
||||
setProperties(prev => [...prev, newProperty]);
|
||||
return newProperty;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateProperty = useCallback(async (id, updates) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setProperties(prev =>
|
||||
prev.map(p => p.id === id ? { ...p, ...updates } : p)
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteProperty = useCallback(async (id) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setProperties(prev => prev.filter(p => p.id !== id));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkAvailability = useCallback((propertyId, startDate, endDate) => {
|
||||
const property = properties.find(p => p.id === propertyId);
|
||||
if (!property) return false;
|
||||
|
||||
return !property.bookings?.some(booking => {
|
||||
const bookingStart = new Date(booking.startDate);
|
||||
const bookingEnd = new Date(booking.endDate);
|
||||
const checkStart = new Date(startDate);
|
||||
const checkEnd = new Date(endDate);
|
||||
|
||||
return (
|
||||
(checkStart >= bookingStart && checkStart <= bookingEnd) ||
|
||||
(checkEnd >= bookingStart && checkEnd <= bookingEnd) ||
|
||||
(checkStart <= bookingStart && checkEnd >= bookingEnd)
|
||||
);
|
||||
});
|
||||
}, [properties]);
|
||||
|
||||
return (
|
||||
<PropertyContext.Provider value={{
|
||||
properties,
|
||||
loading,
|
||||
error,
|
||||
addProperty,
|
||||
updateProperty,
|
||||
deleteProperty,
|
||||
checkAvailability
|
||||
}}>
|
||||
{children}
|
||||
</PropertyContext.Provider>
|
||||
);
|
||||
};
|
||||
67
app/utils/calculations.js
Normal file
67
app/utils/calculations.js
Normal file
@ -0,0 +1,67 @@
|
||||
export const calculateRentWithCommission = (
|
||||
dailyPrice,
|
||||
numberOfDays,
|
||||
commissionRate,
|
||||
commissionType
|
||||
) => {
|
||||
const baseRent = dailyPrice * numberOfDays;
|
||||
const commission = (baseRent * commissionRate) / 100;
|
||||
|
||||
switch(commissionType) {
|
||||
case 'from_tenant':
|
||||
return {
|
||||
totalForTenant: baseRent + commission,
|
||||
totalForOwner: baseRent,
|
||||
commission: commission
|
||||
};
|
||||
case 'from_owner':
|
||||
return {
|
||||
totalForTenant: baseRent,
|
||||
totalForOwner: baseRent - commission,
|
||||
commission: commission
|
||||
};
|
||||
case 'from_both':
|
||||
return {
|
||||
totalForTenant: baseRent + (commission / 2),
|
||||
totalForOwner: baseRent - (commission / 2),
|
||||
commission: commission
|
||||
};
|
||||
default:
|
||||
return {
|
||||
totalForTenant: baseRent,
|
||||
totalForOwner: baseRent,
|
||||
commission: 0
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const calculateDaysBetween = (startDate, endDate) => {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const diffTime = Math.abs(end - start);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
};
|
||||
|
||||
|
||||
export const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('ar-SY', {
|
||||
style: 'currency',
|
||||
currency: 'SYP',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount).replace('SYP', '') + ' ل.س';
|
||||
};
|
||||
|
||||
export const calculateTenantBalance = (bookings, securityDeposits) => {
|
||||
return bookings.reduce((acc, booking) => {
|
||||
const deposit = securityDeposits.find(d => d.bookingId === booking.id) || 0;
|
||||
return {
|
||||
totalRent: acc.totalRent + booking.totalAmount,
|
||||
paidAmount: acc.paidAmount + booking.paidAmount,
|
||||
securityDeposit: acc.securityDeposit + deposit.amount,
|
||||
pendingAmount: (acc.pendingAmount + (booking.totalAmount - booking.paidAmount))
|
||||
};
|
||||
}, { totalRent: 0, paidAmount: 0, securityDeposit: 0, pendingAmount: 0 });
|
||||
};
|
||||
41
app/utils/constants.js
Normal file
41
app/utils/constants.js
Normal file
@ -0,0 +1,41 @@
|
||||
export const PROPERTY_STATUS = {
|
||||
AVAILABLE: 'available',
|
||||
BOOKED: 'booked',
|
||||
MAINTENANCE: 'maintenance'
|
||||
};
|
||||
|
||||
export const BOOKING_STATUS = {
|
||||
PENDING: 'pending',
|
||||
OWNER_APPROVED: 'owner_approved',
|
||||
ADMIN_APPROVED: 'admin_approved',
|
||||
REJECTED: 'rejected',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed',
|
||||
CANCELLED: 'cancelled'
|
||||
};
|
||||
|
||||
export const COMMISSION_TYPE = {
|
||||
FROM_OWNER: 'from_owner',
|
||||
FROM_TENANT: 'from_tenant',
|
||||
FROM_BOTH: 'from_both'
|
||||
};
|
||||
|
||||
export const IDENTITY_TYPE = {
|
||||
SYRIAN: 'syrian',
|
||||
PASSPORT: 'passport'
|
||||
};
|
||||
|
||||
export const PAYMENT_METHOD = {
|
||||
CASH: 'cash',
|
||||
ELECTRONIC: 'electronic'
|
||||
};
|
||||
|
||||
export const CITIES = {
|
||||
DAMASCUS: 'damascus',
|
||||
ALEPPO: 'aleppo',
|
||||
HOMS: 'homs',
|
||||
LATTAKIA: 'latakia',
|
||||
DARAA: 'daraa'
|
||||
};
|
||||
|
||||
export const DEFAULT_COMMISSION_RATE = 5;
|
||||
BIN
public/Home.jpg
Normal file
BIN
public/Home.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 MiB |
Reference in New Issue
Block a user