Files
SweetHome/app/components/admin/BookingRequests.js
mouazkh 7e0d5eaf8d
All checks were successful
Build frontend / build (push) Successful in 40s
edited the api request
2026-04-16 22:40:59 +03:00

1713 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useCallback, useEffect, 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,
Loader2,
CreditCard
} from 'lucide-react';
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../services/AuthService';
import { adminConfirmDeposit, getRentProperty, getReservations } from '../../utils/api';
const RESERVATION_STATUS = Object.freeze({
pending: 0,
ownerConfirmed: 1,
depositPaid: 2,
depositConfirmed: 3,
completed: 4,
cancelled: 5,
});
const STATUS_KEY_BY_CODE = Object.freeze({
[RESERVATION_STATUS.pending]: 'pending',
[RESERVATION_STATUS.ownerConfirmed]: 'ownerConfirmed',
[RESERVATION_STATUS.depositPaid]: 'depositPaid',
[RESERVATION_STATUS.depositConfirmed]: 'depositConfirmed',
[RESERVATION_STATUS.completed]: 'completed',
[RESERVATION_STATUS.cancelled]: 'cancelled',
});
const STATUS_META = Object.freeze({
pending: {
label: 'قيد الانتظار',
card: 'border-yellow-400 bg-yellow-50',
badge: 'bg-yellow-500 text-white',
pdf: 'pending',
tabActive: 'bg-yellow-600 text-white shadow-lg',
tabIdle: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200',
},
ownerConfirmed: {
label: 'موافقة المالك',
card: 'border-blue-400 bg-blue-50',
badge: 'bg-blue-500 text-white',
pdf: 'ownerConfirmed',
tabActive: 'bg-blue-600 text-white shadow-lg',
tabIdle: 'bg-blue-100 text-blue-800 hover:bg-blue-200',
},
depositPaid: {
label: 'تم دفع العربون',
card: 'border-indigo-400 bg-indigo-50',
badge: 'bg-indigo-500 text-white',
pdf: 'depositPaid',
tabActive: 'bg-indigo-600 text-white shadow-lg',
tabIdle: 'bg-indigo-100 text-indigo-800 hover:bg-indigo-200',
},
depositConfirmed: {
label: 'تم تأكيد العربون',
card: 'border-green-400 bg-green-50',
badge: 'bg-green-500 text-white',
pdf: 'depositConfirmed',
tabActive: 'bg-green-600 text-white shadow-lg',
tabIdle: 'bg-green-100 text-green-800 hover:bg-green-200',
},
completed: {
label: 'منتهي',
card: 'border-gray-400 bg-gray-50',
badge: 'bg-gray-500 text-white',
pdf: 'completed',
tabActive: 'bg-gray-600 text-white shadow-lg',
tabIdle: 'bg-gray-100 text-gray-800 hover:bg-gray-200',
},
cancelled: {
label: 'ملغي',
card: 'border-red-400 bg-red-50',
badge: 'bg-red-500 text-white',
pdf: 'cancelled',
tabActive: 'bg-red-600 text-white shadow-lg',
tabIdle: 'bg-red-100 text-red-800 hover:bg-red-200',
},
unknown: {
label: 'غير معروف',
card: 'border-gray-200 bg-white',
badge: 'bg-gray-200 text-gray-700',
pdf: 'unknown',
tabActive: 'bg-gray-600 text-white shadow-lg',
tabIdle: 'bg-gray-100 text-gray-800 hover:bg-gray-200',
},
});
const FILTER_TABS = [
{
id: 'all',
label: 'الكل',
tabActive: 'bg-gray-600 text-white shadow-lg',
tabIdle: 'bg-gray-100 text-gray-800 hover:bg-gray-200',
},
{
id: 'pending',
label: STATUS_META.pending.label,
tabActive: STATUS_META.pending.tabActive,
tabIdle: STATUS_META.pending.tabIdle,
},
{
id: 'ownerConfirmed',
label: STATUS_META.ownerConfirmed.label,
tabActive: STATUS_META.ownerConfirmed.tabActive,
tabIdle: STATUS_META.ownerConfirmed.tabIdle,
},
{
id: 'depositPaid',
label: STATUS_META.depositPaid.label,
tabActive: STATUS_META.depositPaid.tabActive,
tabIdle: STATUS_META.depositPaid.tabIdle,
},
{
id: 'depositConfirmed',
label: STATUS_META.depositConfirmed.label,
tabActive: STATUS_META.depositConfirmed.tabActive,
tabIdle: STATUS_META.depositConfirmed.tabIdle,
},
{
id: 'completed',
label: STATUS_META.completed.label,
tabActive: STATUS_META.completed.tabActive,
tabIdle: STATUS_META.completed.tabIdle,
},
{
id: 'cancelled',
label: STATUS_META.cancelled.label,
tabActive: STATUS_META.cancelled.tabActive,
tabIdle: STATUS_META.cancelled.tabIdle,
},
];
function getStatusKey(status) {
return STATUS_KEY_BY_CODE[Number(status)] ?? 'unknown';
}
function getStatusMeta(status) {
return STATUS_META[getStatusKey(status)] ?? STATUS_META.unknown;
}
function formatDisplayDate(value, locale = 'ar-SY') {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toLocaleDateString(locale);
}
function formatInputDate(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toISOString().split('T')[0];
}
function calculateReservationDays(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
return 0;
}
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
return days > 0 ? days : 0;
}
function pickFirstNumber(...values) {
for (const value of values) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return 0;
}
function pickFirstText(...values) {
for (const value of values) {
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
}
return '';
}
function normalizeCommissionType(value) {
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
if (value === 0) return 'من المالك';
if (value === 1) return 'من المستأجر';
if (value === 2) return 'من الطرفين';
return 'غير محدد';
}
function buildReservationDisplayName(reservation) {
return (
pickFirstText(
reservation?.customer?.name,
reservation?.tenant?.name,
reservation?.user?.name,
reservation?.customerName,
reservation?.tenantName,
reservation?.userName,
reservation?.fullName,
reservation?.name,
) || `الحجز #${reservation?.id ?? reservation?.reservationId ?? '—'}`
);
}
function normalizeReservation(reservation, property) {
const info = property?.propertyInformation ?? property ?? {};
const status = Number.isFinite(Number(reservation?.status))
? Number(reservation.status)
: RESERVATION_STATUS.pending;
const days = pickFirstNumber(
reservation?.days,
reservation?.durationInDays,
calculateReservationDays(reservation?.startDate, reservation?.endDate),
);
const totalAmount = pickFirstNumber(
reservation?.totalPrice,
reservation?.totalAmount,
reservation?.price,
);
const securityDeposit = pickFirstNumber(
reservation?.securityDeposit,
reservation?.deposit,
property?.deposit,
property?.securityDeposit,
);
const dailyPrice = pickFirstNumber(
reservation?.dailyPrice,
reservation?.pricePerDay,
days > 0 ? totalAmount / days : 0,
);
const reservationId = pickFirstNumber(reservation?.reservationId, reservation?.id);
return {
...reservation,
id: reservationId || reservation?.id,
reservationId: reservationId || reservation?.id,
user: buildReservationDisplayName(reservation),
userEmail: pickFirstText(
reservation?.customer?.email,
reservation?.tenant?.email,
reservation?.user?.email,
reservation?.customerEmail,
reservation?.tenantEmail,
reservation?.userEmail,
),
userPhone: pickFirstText(
reservation?.customer?.phoneNumber,
reservation?.tenant?.phoneNumber,
reservation?.user?.phoneNumber,
reservation?.customerPhone,
reservation?.tenantPhone,
reservation?.userPhone,
reservation?.phoneNumber,
),
userType: pickFirstText(
reservation?.customerTypeName,
reservation?.customerType,
reservation?.tenantType,
'غير محدد',
),
identityNumber: pickFirstText(
reservation?.customerIdentityNumber,
reservation?.tenantIdentityNumber,
reservation?.identityNumber,
),
property: pickFirstText(info?.address, reservation?.propertyName) || `العقار #${reservation?.propertyId ?? '—'}`,
propertyId: reservation?.propertyId,
startDate: formatInputDate(reservation?.startDate),
endDate: formatInputDate(reservation?.endDate),
days,
totalAmount,
totalPrice: totalAmount,
dailyPrice,
commissionRate: pickFirstNumber(reservation?.commissionRate),
commissionType: normalizeCommissionType(reservation?.commissionType),
commissionAmount: pickFirstNumber(reservation?.commissionAmount),
securityDeposit,
status,
requestDate: formatDisplayDate(reservation?.createdAt),
createdAt: reservation?.createdAt,
ownerApproved: status >= RESERVATION_STATUS.ownerConfirmed,
adminApproved: status >= RESERVATION_STATUS.depositConfirmed,
ownerDelivered: Boolean(reservation?.ownerDelivered),
tenantReceived: Boolean(reservation?.tenantReceived),
tenantLeft: Boolean(reservation?.tenantLeft),
ownerReceived: Boolean(reservation?.ownerReceived),
securityDepositPaid: status >= RESERVATION_STATUS.depositPaid,
securityDepositReturned: reservation?.securityDepositReturned ?? null,
contractSigned: Boolean(reservation?.contractSigned),
notes: pickFirstText(reservation?.comment, reservation?.notes),
actualStartDate: reservation?.actualStartDate ?? null,
actualEndDate: reservation?.actualEndDate ?? null,
_property: info,
};
}
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 DepositCommentDialog = ({
isOpen,
request,
isSubmitting,
onClose,
onConfirm,
}) => {
const [comment, setComment] = useState('');
useEffect(() => {
if (!isOpen) {
setComment('');
}
}, [isOpen, request?.id]);
if (!isOpen || !request) 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={isSubmitting ? undefined : onClose}
>
<motion.div
initial={{ scale: 0.95, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.95, y: 20 }}
className="bg-white rounded-2xl w-full max-w-lg p-6 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="text-center mb-5">
<div className="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-3">
<MessageCircle className="w-8 h-8 text-indigo-600" />
</div>
<h3 className="text-xl font-bold text-gray-900">تأكيد العربون</h3>
<p className="text-sm text-gray-500 mt-1">
يمكنك إضافة تعليق اختياري، أو ترك الحقل فارغاً ليتم إرسال <span className="font-semibold">null</span>.
</p>
</div>
<div className="rounded-xl bg-gray-50 px-4 py-3 text-sm text-gray-700 mb-4 space-y-1">
<div>رقم الحجز: <span className="font-semibold">#{request.id}</span></div>
<div>العقار: <span className="font-semibold">{request.property}</span></div>
<div>المستأجر: <span className="font-semibold">{request.user}</span></div>
</div>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="اكتب تعليقاً إذا أردت..."
className="w-full min-h-32 p-4 border rounded-xl resize-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
disabled={isSubmitting}
/>
<div className="flex gap-3 pt-4">
<button
onClick={() => onConfirm(comment)}
disabled={isSubmitting}
className="flex-1 bg-indigo-600 text-white py-3 rounded-xl font-medium hover:bg-indigo-700 transition-colors disabled:opacity-60 disabled:cursor-wait flex items-center justify-center gap-2"
>
{isSubmitting ? <Loader2 className="w-5 h-5 animate-spin" /> : <CreditCard className="w-5 h-5" />}
<span>{isSubmitting ? 'جاري الإرسال...' : 'تأكيد الإرسال'}</span>
</button>
<button
onClick={onClose}
disabled={isSubmitting}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors disabled:opacity-60"
>
إلغاء
</button>
</div>
</motion.div>
</motion.div>
);
};
const PDFExportButton = ({ request, onExportComplete }) => {
const [isExporting, setIsExporting] = useState(false);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const generatePDF = async () => {
if (!request) {
toast.error('لا توجد بيانات للتصدير');
return;
}
const statusMeta = getStatusMeta(request.status);
const userTypeLabel = request.userType || 'غير محدد';
setIsExporting(true);
const loadingToast = toast.loading('جاري إنشاء ملف PDF...', { id: 'pdf-export' });
try {
const printContent = document.createElement('div');
printContent.style.width = '800px';
printContent.style.padding = '40px';
printContent.style.backgroundColor = 'white';
printContent.style.fontFamily = 'Cairo, Arial, sans-serif';
printContent.style.direction = 'rtl';
printContent.style.position = 'absolute';
printContent.style.left = '-9999px';
printContent.style.top = '-9999px';
printContent.style.zIndex = '-9999';
printContent.innerHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Cairo', 'Arial', sans-serif;
}
body {
background: white;
padding: 20px;
}
.pdf-container {
max-width: 800px;
margin: 0 auto;
background: white;
}
.pdf-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 3px solid #f59e0b;
}
.pdf-logo {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #f59e0b, #d97706);
border-radius: 15px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.pdf-logo svg {
width: 32px;
height: 32px;
}
.pdf-title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
margin-bottom: 5px;
}
.pdf-subtitle {
font-size: 12px;
color: #6b7280;
}
.pdf-section {
margin-bottom: 25px;
background: #f9fafb;
border-radius: 16px;
padding: 20px;
break-inside: avoid;
}
.pdf-section-title {
font-size: 18px;
font-weight: bold;
color: #f59e0b;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #e5e7eb;
display: flex;
align-items: center;
gap: 8px;
}
.pdf-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.pdf-info-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f3f4f6;
}
.pdf-label {
font-weight: 600;
color: #4b5563;
font-size: 14px;
}
.pdf-value {
color: #1f2937;
font-weight: 500;
font-size: 14px;
}
.pdf-status {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.pdf-status-pending { background: #fef3c7; color: #d97706; }
.pdf-status-ownerConfirmed { background: #dbeafe; color: #2563eb; }
.pdf-status-depositPaid { background: #e0e7ff; color: #4338ca; }
.pdf-status-depositConfirmed { background: #d1fae5; color: #059669; }
.pdf-status-completed { background: #e5e7eb; color: #4b5563; }
.pdf-status-cancelled { background: #fee2e2; color: #dc2626; }
.pdf-status-unknown { background: #f3f4f6; color: #4b5563; }
.pdf-amount {
font-size: 18px;
font-weight: bold;
color: #f59e0b;
}
.pdf-footer {
margin-top: 30px;
text-align: center;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 11px;
color: #9ca3af;
}
.pdf-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
background: #f3f4f6;
color: #374151;
}
@media print {
body {
padding: 0;
margin: 0;
}
.pdf-section {
break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="pdf-container">
<div class="pdf-header">
<div class="pdf-logo">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M3 9L12 3L21 9L12 15L3 9Z" stroke="white" fill="none"/>
<path d="M12 15V21" stroke="white"/>
<path d="M8 12L4 14" stroke="white"/>
<path d="M16 12L20 14" stroke="white"/>
</svg>
</div>
<div class="pdf-title">تقرير طلب حجز #${request.id}</div>
<div class="pdf-subtitle">تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')} | ${new Date().toLocaleTimeString('ar-SA')}</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> معلومات المستأجر
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">الاسم الكامل:</span>
<span class="pdf-value">${request.user || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">نوع الهوية:</span>
<span class="pdf-value">${userTypeLabel}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">رقم الهوية:</span>
<span class="pdf-value">${request.identityNumber || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">البريد الإلكتروني:</span>
<span class="pdf-value">${request.userEmail || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">رقم الهاتف:</span>
<span class="pdf-value">${request.userPhone || 'غير محدد'}</span>
</div>
</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> معلومات العقار
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">العقار:</span>
<span class="pdf-value">${request.property || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">السعر اليومي:</span>
<span class="pdf-value">${formatCurrency(request.dailyPrice)}</span>
</div>
</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> تفاصيل الحجز
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">تاريخ البداية:</span>
<span class="pdf-value">${request.startDate || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">تاريخ النهاية:</span>
<span class="pdf-value">${request.endDate || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">عدد الأيام:</span>
<span class="pdf-value">${request.days || 0} يوم</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">الحالة:</span>
<span class="pdf-value">
<span class="pdf-status pdf-status-${statusMeta.pdf}">
${statusMeta.label}
</span>
</span>
</div>
</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> المعلومات المالية
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">سلفة الضمان:</span>
<span class="pdf-value">${formatCurrency(request.securityDeposit)}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">المبلغ الإجمالي:</span>
<span class="pdf-value pdf-amount">${formatCurrency(request.totalAmount)}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">نسبة العمولة:</span>
<span class="pdf-value">${request.commissionRate || 0}%</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">نوع العمولة:</span>
<span class="pdf-value">${request.commissionType || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">قيمة العمولة:</span>
<span class="pdf-value">${formatCurrency(request.commissionAmount)}</span>
</div>
</div>
</div>
${request.notes ? `
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> ملاحظات
</div>
<div class="pdf-info-item" style="display: block;">
<span class="pdf-value">${request.notes}</span>
</div>
</div>
` : ''}
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> سجل الإجراءات
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge"> ${request.requestDate}</span>
<span class="pdf-value">تم إنشاء الطلب</span>
</div>
${request.ownerApproved ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge">✓</span>
<span class="pdf-value">تمت موافقة المالك</span>
</div>
` : ''}
${request.adminApproved ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge">✓</span>
<span class="pdf-value">تمت موافقة الإدارة</span>
</div>
` : ''}
${request.ownerDelivered ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge"></span>
<span class="pdf-value">تم تسليم المفتاح</span>
</div>
` : ''}
${request.tenantReceived ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge"></span>
<span class="pdf-value">تم استلام العقار</span>
</div>
` : ''}
</div>
</div>
<div class="pdf-footer">
<div>تقرير صادر عن نظام SweetHome لإدارة العقارات</div>
<div style="margin-top: 5px;">جميع الحقوق محفوظة © ${new Date().getFullYear()} SweetHome</div>
<div style="margin-top: 5px; font-size: 9px;">هذا التقرير تم إنشاؤه تلقائياً ولا يحتاج إلى توقيع</div>
</div>
</div>
</body>
</html>
`;
document.body.appendChild(printContent);
await new Promise(resolve => setTimeout(resolve, 200));
const canvas = await html2canvas(printContent, {
scale: 2.5,
backgroundColor: '#ffffff',
logging: false,
useCORS: true,
windowWidth: printContent.scrollWidth,
windowHeight: printContent.scrollHeight,
onclone: (clonedDoc, element) => {
}
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
compress: true
});
const imgWidth = 210;
const pageHeight = 297;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
let pageCount = 1;
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
pageCount++;
}
const fileName = `تقرير_طلب_${request.id}_${new Date().toISOString().split('T')[0]}.pdf`;
pdf.save(fileName);
document.body.removeChild(printContent);
toast.success(`تم تصدير التقرير بنجاح! (${pageCount} صفحة)`, { id: 'pdf-export' });
if (onExportComplete) {
onExportComplete();
}
} catch (error) {
console.error('Error generating PDF:', error);
toast.error('حدث خطأ أثناء إنشاء ملف PDF', { id: 'pdf-export' });
} finally {
setIsExporting(false);
}
};
return (
<button
onClick={generatePDF}
disabled={isExporting}
className="flex-1 bg-green-600 text-white py-3 rounded-xl font-medium hover:bg-green-700 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md"
>
{isExporting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري التصدير...
</>
) : (
<>
<Download className="w-5 h-5" />
تصدير PDF
</>
)}
</button>
);
};
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-gradient-to-r from-amber-500 to-amber-600 p-5 text-white flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<FileText className="w-5 h-5" />
تفاصيل الطلب #{request.id}
</h2>
<p className="text-amber-100 text-sm mt-1">جميع معلومات الطلب في مكان واحد</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-white/20 rounded-full transition-colors"
>
<XCircle className="w-6 h-6" />
</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-1 md: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 || 'غير محدد'}
<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-1 md: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-1 md: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-1 md: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 px-3 py-1 rounded-full text-sm inline-block ${
request.securityDepositPaid
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{request.securityDepositPaid ? '✓ تم الدفع' : '⏳ في انتظار الدفع'}
</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="bg-gray-50 p-4 rounded-xl">
<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-white rounded-lg border">
<p className="text-sm text-gray-600">{request.notes}</p>
</div>
)}
</div>
</div>
</div>
<div className="sticky bottom-0 bg-white border-t p-4 flex gap-3 shadow-lg">
<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>
<PDFExportButton request={request} />
</div>
</motion.div>
</motion.div>
);
};
const RequestCard = ({ request, onConfirmDeposit, onViewDetails, confirmingDepositId }) => {
const [expanded, setExpanded] = useState(
Number(request.status) === RESERVATION_STATUS.depositPaid,
);
const isConfirmingDeposit = confirmingDepositId === request.id;
const statusMeta = getStatusMeta(request.status);
const canConfirmDeposit = Number(request.status) === RESERVATION_STATUS.depositPaid;
const formatCurrency = (amount) => {
const value = Number(amount) || 0;
return value.toLocaleString() + ' ل.س';
};
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 ${statusMeta.card}`}
>
<div className="p-4 cursor-pointer" onClick={() => setExpanded((prev) => !prev)}>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span className="text-lg font-bold text-gray-900">طلب #{request.id}</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium shadow-sm ${statusMeta.badge}`}>
{statusMeta.label}
</span>
</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-4 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={`p-2 rounded-lg ${request.securityDepositPaid ? 'bg-green-50' : 'bg-yellow-50'}`}>
<div className="text-xs text-gray-500">حالة العربون</div>
<div className={`font-bold text-xs ${request.securityDepositPaid ? 'text-green-600' : 'text-yellow-600'}`}>
{request.securityDepositPaid ? 'تم دفع العربون' : 'بانتظار الدفع'}
</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 text-xs">{request.startDate} إلى {request.endDate}</div>
</div>
</div>
{(request.userEmail || request.userPhone) && (
<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-1 md:grid-cols-2 gap-2 text-sm">
<div className="flex items-center gap-1">
<Mail className="w-3 h-3 text-gray-500" />
<span>{request.userEmail || 'غير متوفر'}</span>
</div>
<div className="flex items-center gap-1">
<Phone className="w-3 h-3 text-gray-500" />
<span>{request.userPhone || 'غير متوفر'}</span>
</div>
</div>
</div>
)}
<div className="space-y-3">
<div className={`grid gap-3 ${canConfirmDeposit ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'}`}>
<button
onClick={() => onViewDetails(request)}
className="bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
>
<FileText className="w-5 h-5" />
عرض التفاصيل
</button>
{canConfirmDeposit && (
<button
onClick={() => onConfirmDeposit(request)}
disabled={isConfirmingDeposit}
className={`py-3 rounded-xl font-medium flex items-center justify-center gap-2 transition-all ${
isConfirmingDeposit
? 'bg-indigo-300 text-white cursor-wait'
: 'bg-indigo-600 text-white hover:bg-indigo-700 transform hover:scale-[1.01]'
}`}
>
{isConfirmingDeposit ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<CreditCard className="w-5 h-5" />
)}
<span>تأكيد العربون</span>
</button>
)}
</div>
{Number(request.status) === RESERVATION_STATUS.depositConfirmed && (
<div className="rounded-xl bg-green-50 px-4 py-3 text-sm text-green-800">
تم تأكيد العربون من قبل الإدارة.
</div>
)}
{request.notes && (
<div className="rounded-xl bg-gray-50 px-4 py-3 text-sm text-gray-700">
{request.notes}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
export default function BookingRequests() {
/*
Legacy dummy data kept for reference only. The page now loads real
reservations from the API instead of rendering these local examples.
const DUMMY_REQUESTS = [
{
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,
securityDepositPaid: 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: 'admin_approved',
requestDate: '2024-02-24',
ownerApproved: true,
adminApproved: true,
ownerDelivered: false,
tenantReceived: false,
tenantLeft: false,
ownerReceived: false,
securityDepositPaid: 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,
securityDepositPaid: true,
securityDepositReturned: null,
contractSigned: true,
notes: 'عقد موقع إلكترونياً',
actualStartDate: '2024-02-20',
actualEndDate: null
}
];
*/
const [requests, setRequests] = useState([]);
const [filter, setFilter] = useState('depositPaid');
const [detailsDialog, setDetailsDialog] = useState({ isOpen: false, request: null });
const [depositDialog, setDepositDialog] = useState({ isOpen: false, request: null });
const [confirmingDepositId, setConfirmingDepositId] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState('');
const loadReservations = useCallback(async () => {
setIsLoading(true);
setLoadError('');
try {
if (!AuthService.isAdmin()) {
throw new Error('هذه الصفحة متاحة للإدارة فقط');
}
let list = await getReservations();
if (!Array.isArray(list)) {
list = [];
}
const propertyCache = new Map();
const uniquePropertyIds = [...new Set(list.map((item) => item?.propertyId).filter((id) => id != null))];
await Promise.all(
uniquePropertyIds.map(async (propertyId) => {
try {
const property = await getRentProperty(propertyId);
propertyCache.set(propertyId, property);
} catch (error) {
console.warn('[Admin] Failed to load property details for reservation:', propertyId, error);
propertyCache.set(propertyId, null);
}
}),
);
const normalizedRequests = list
.map((reservation) => normalizeReservation(reservation, propertyCache.get(reservation?.propertyId)))
.filter((reservation) => reservation?.id != null)
.sort((left, right) => {
const leftDate = new Date(left.createdAt || 0).getTime();
const rightDate = new Date(right.createdAt || 0).getTime();
return rightDate - leftDate;
});
setRequests(normalizedRequests);
} catch (error) {
console.error('[Admin] Failed to load reservations:', error);
setLoadError(error.message || 'فشل تحميل الحجوزات');
setRequests([]);
toast.error(error.message || 'فشل تحميل الحجوزات');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
loadReservations();
}, [loadReservations]);
const openDepositConfirmationDialog = (request) => {
setDepositDialog({ isOpen: true, request });
};
const closeDepositConfirmationDialog = () => {
if (confirmingDepositId != null) return;
setDepositDialog({ isOpen: false, request: null });
};
const handleDepositConfirmation = async (request, commentInput = null) => {
if (!AuthService.isAdmin()) {
console.warn('[Admin] Deposit confirmation blocked: current user is not admin', {
user: AuthService.getUser(),
roles: AuthService.getRoles?.(),
request,
});
toast.error('هذا الإجراء متاح للإدارة فقط');
return;
}
if (Number(request?.status) !== RESERVATION_STATUS.depositPaid) {
console.warn('[Admin] Deposit confirmation blocked: reservation is not in depositPaid state', {
requestId: request?.id,
reservationId: request?.reservationId,
status: request?.status,
});
toast.error('يمكن تأكيد العربون فقط عندما تكون الحالة depositPaid');
return;
}
const reservationId = Number(request?.reservationId ?? request?.id);
if (!Number.isFinite(reservationId)) {
toast.error('تعذر تحديد رقم الحجز المطلوب');
return;
}
const adminUser = AuthService.getUser();
const parsedAdminId = Number(adminUser?.id);
const adminId = Number.isFinite(parsedAdminId) ? parsedAdminId : adminUser?.id;
const normalizedComment =
typeof commentInput === 'string' && commentInput.trim()
? commentInput.trim()
: null;
console.log('[Admin] Preparing admin confirm deposit request', {
requestId: request?.id,
reservationId,
adminId,
adminUser,
status: request?.status,
endpoint: '/Reservations/AdminConfirmDeposit/admin-confirm-deposit',
payload: {
reservationId,
adminId,
comment: normalizedComment,
},
});
if (adminId == null || adminId === '') {
console.warn('[Admin] Deposit confirmation blocked: adminId is missing', {
adminUser,
parsedAdminId,
});
toast.error('لم نتمكن من تحديد هوية المدير');
return;
}
setConfirmingDepositId(request.id);
try {
const result = await adminConfirmDeposit(reservationId, adminId, normalizedComment);
console.log('[Admin] Deposit confirmation response', {
requestId: request?.id,
reservationId,
adminId,
comment: normalizedComment,
status: result?.status,
ok: result?.ok,
message: result?.message,
data: result?.data,
});
if (!result.ok) {
throw new Error(result.message || result.data?.message || `HTTP ${result.status}`);
}
setRequests((prev) =>
prev.map((req) =>
req.id === request.id
? {
...req,
status: RESERVATION_STATUS.depositConfirmed,
adminApproved: true,
securityDepositPaid: true,
notes: normalizedComment || 'تم تأكيد العربون من قبل الإدارة',
}
: req,
),
);
setDepositDialog({ isOpen: false, request: null });
toast.success('تم تأكيد العربون بنجاح');
} catch (err) {
console.error('[Admin] Deposit confirmation failed:', err);
toast.error(err.message || 'فشل تأكيد العربون');
} finally {
setConfirmingDepositId(null);
}
};
const filteredRequests = requests.filter((req) =>
filter === 'all' ? true : getStatusKey(req.status) === filter,
);
const counts = FILTER_TABS.reduce((acc, tab) => {
acc[tab.id] =
tab.id === 'all'
? requests.length
: requests.filter((req) => getStatusKey(req.status) === tab.id).length;
return acc;
}, {});
const stats = {
total: requests.length,
depositPaid: counts.depositPaid || 0,
depositConfirmed: counts.depositConfirmed || 0,
completed: counts.completed || 0,
};
return (
<div className="space-y-6">
<Toaster position="top-center" reverseOrder={false} />
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">متابعة حجوزات العربون</h2>
<p className="text-sm text-gray-600 mt-1">
يتم جلب جميع الحجوزات من الخادم، مع فتح الحجوزات ذات الحالة <span className="font-semibold">depositPaid (2)</span> بشكل افتراضي.
</p>
</div>
<button
onClick={loadReservations}
disabled={isLoading}
className="self-start rounded-xl bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 disabled:cursor-wait disabled:opacity-60"
>
{isLoading ? 'جاري التحديث...' : 'تحديث الحجوزات'}
</button>
</div>
{loadError && (
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{loadError}
</div>
)}
<div className="grid grid-cols-2 xl: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-indigo-600 to-indigo-700 text-white rounded-2xl p-4 shadow-lg"
>
<div className="text-3xl font-bold mb-1">{stats.depositPaid}</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-green-600 to-green-700 text-white rounded-2xl p-4 shadow-lg"
>
<div className="text-3xl font-bold mb-1">{stats.depositConfirmed}</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-gray-600 to-gray-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">
{FILTER_TABS.map((tab) => (
<button
key={tab.id}
onClick={() => setFilter(tab.id)}
className={`rounded-xl px-4 py-2 text-sm font-medium transition-all transform hover:scale-[1.01] ${
filter === tab.id ? tab.tabActive : tab.tabIdle
}`}
>
{tab.label} ({counts[tab.id] || 0})
</button>
))}
</div>
</div>
{isLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-10 h-10 text-indigo-600 animate-spin" />
</div>
) : (
<div className="space-y-4">
{filteredRequests.map((request) => (
<RequestCard
key={request.id}
request={request}
onConfirmDeposit={openDepositConfirmationDialog}
onViewDetails={(selectedRequest) => setDetailsDialog({ isOpen: true, request: selectedRequest })}
confirmingDepositId={confirmingDepositId}
/>
))}
</div>
)}
{!isLoading && 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>
)}
<RequestDetailsDialog
request={detailsDialog.request}
isOpen={detailsDialog.isOpen}
onClose={() => setDetailsDialog({ isOpen: false, request: null })}
/>
<DepositCommentDialog
isOpen={depositDialog.isOpen}
request={depositDialog.request}
isSubmitting={confirmingDepositId === depositDialog.request?.id}
onClose={closeDepositConfirmationDialog}
onConfirm={(comment) => handleDepositConfirmation(depositDialog.request, comment)}
/>
</div>
);
}