Edit admin page
Edit home image Added properties page
This commit is contained in:
@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user