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