diff --git a/app/components/admin/BookingRequests.js b/app/components/admin/BookingRequests.js index e4be69d..f9b2c45 100644 --- a/app/components/admin/BookingRequests.js +++ b/app/components/admin/BookingRequests.js @@ -1,6 +1,6 @@ 'use client'; -import { useState, useRef } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { CheckCircle, @@ -30,7 +30,311 @@ import jsPDF from 'jspdf'; import html2canvas from 'html2canvas'; import toast, { Toaster } from 'react-hot-toast'; import AuthService from '../../services/AuthService'; -import { adminConfirmDeposit } from '../../utils/api'; +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); @@ -128,6 +432,8 @@ const PDFExportButton = ({ request, onExportComplete }) => { return; } + const statusMeta = getStatusMeta(request.status); + const userTypeLabel = request.userType || 'غير محدد'; setIsExporting(true); const loadingToast = toast.loading('جاري إنشاء ملف PDF...', { id: 'pdf-export' }); @@ -241,11 +547,12 @@ const PDFExportButton = ({ request, onExportComplete }) => { font-weight: 600; } .pdf-status-pending { background: #fef3c7; color: #d97706; } - .pdf-status-owner_approved { background: #dbeafe; color: #2563eb; } - .pdf-status-admin_approved { background: #d1fae5; color: #059669; } - .pdf-status-active { background: #e0e7ff; color: #4f46e5; } + .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-rejected { background: #fee2e2; color: #dc2626; } + .pdf-status-cancelled { background: #fee2e2; color: #dc2626; } + .pdf-status-unknown { background: #f3f4f6; color: #4b5563; } .pdf-amount { font-size: 18px; font-weight: bold; @@ -305,7 +612,7 @@ const PDFExportButton = ({ request, onExportComplete }) => {
نوع الهوية: - ${request.userType === 'syrian' ? 'هوية سورية' : 'جواز سفر'} + ${userTypeLabel}
رقم الهوية: @@ -358,12 +665,8 @@ const PDFExportButton = ({ request, onExportComplete }) => {
الحالة: - - ${request.status === 'pending' ? 'قيد الانتظار' : - request.status === 'owner_approved' ? 'موافقة المالك' : - request.status === 'admin_approved' ? 'موافقة الإدارة' : - request.status === 'active' ? 'إيجار نشط' : - request.status === 'completed' ? 'منتهي' : 'مرفوض'} + + ${statusMeta.label}
@@ -588,7 +891,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
- {request.userType === 'syrian' ? '🇸🇾 هوية سورية' : 'جواز سفر'} + {request.userType || 'غير محدد'} {request.identityNumber}
@@ -739,65 +1042,32 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => { ); }; -const RequestCard = ({ request, onAction, onViewDetails, confirmingDepositId }) => { +const RequestCard = ({ request, onConfirmDeposit, onViewDetails, confirmingDepositId }) => { const [expanded, setExpanded] = useState( - request.status === 'admin_approved' && !request.securityDepositPaid, + 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) => { - 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 ( - - {labels[status]} - - ); + const value = Number(amount) || 0; + return value.toLocaleString() + ' ل.س'; }; return ( -
setExpanded(!expanded)}> -
+
setExpanded((prev) => !prev)}> +
طلب #{request.id} - {getStatusBadge(request.status)} + + {statusMeta.label} +
{request.requestDate} @@ -839,13 +1109,13 @@ const RequestCard = ({ request, onAction, onViewDetails, confirmingDepositId }) >
-
سلفة ضمان
+
العربون
{formatCurrency(request.securityDeposit)}
حالة العربون
- {request.securityDepositPaid ? 'تم الدفع' : ' بانتظار'} + {request.securityDepositPaid ? 'تم دفع العربون' : 'بانتظار الدفع'}
@@ -858,171 +1128,70 @@ const RequestCard = ({ request, onAction, onViewDetails, confirmingDepositId })
- {(request.ownerApproved || request.adminApproved) && ( + {(request.userEmail || request.userPhone) && (

معلومات الاتصال

-
+
- {request.userEmail} + {request.userEmail || 'غير متوفر'}
- {request.userPhone} + {request.userPhone || 'غير متوفر'}
)}
- {request.status === 'pending' && ( -
+
+ + + {canConfirmDeposit && ( - - + )} +
+ + {canConfirmDeposit && ( +
+ يظهر زر تأكيد العربون فقط عندما تكون حالة الحجز depositPaid (2).
)} - {request.status === 'owner_approved' && ( -
- - - + {Number(request.status) === RESERVATION_STATUS.depositConfirmed && ( +
+ تم تأكيد العربون من قبل الإدارة.
)} - {request.status === 'admin_approved' && ( -
-
- - - -
-
- )} - - {request.status === 'active' && ( -
-
- - -
- - {request.actualStartDate && ( -
-
- بدأ الإيجار: {request.actualStartDate} - المدة: {request.days} يوم -
-
-
-
-
- )} + {request.notes && ( +
+ {request.notes}
)}
@@ -1034,209 +1203,187 @@ const RequestCard = ({ request, onAction, onViewDetails, confirmingDepositId }) }; 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, - 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 - } - ]); + /* + Legacy dummy data kept for reference only. The page now loads real + reservations from the API instead of rendering these local examples. - const [filter, setFilter] = useState('all'); - const [reasonDialog, setReasonDialog] = useState({ isOpen: false, requestId: null, type: null }); + 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 [confirmingDepositId, setConfirmingDepositId] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [loadError, setLoadError] = useState(''); - 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 'confirm_deposit': - handleDepositConfirmation(data); - 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 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); } - }; + }, []); - 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 }); - }; + useEffect(() => { + loadReservations(); + }, [loadReservations]); - 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 handleDepositConfirmation = async (request) => { if (!AuthService.isAdmin()) { toast.error('هذا الإجراء متاح للإدارة فقط'); return; } + if (Number(request?.status) !== RESERVATION_STATUS.depositPaid) { + toast.error('يمكن تأكيد العربون فقط عندما تكون الحالة depositPaid'); + return; + } + const reservationId = Number(request?.reservationId ?? request?.id); if (!Number.isFinite(reservationId)) { - toast.error('هذا الطلب تجريبي ولا يحتوي على reservationId من الخادم'); + toast.error('تعذر تحديد رقم الحجز المطلوب'); return; } @@ -1263,6 +1410,8 @@ export default function BookingRequests() { req.id === request.id ? { ...req, + status: RESERVATION_STATUS.depositConfirmed, + adminApproved: true, securityDepositPaid: true, notes: 'تم تأكيد العربون من قبل الإدارة', } @@ -1279,125 +1428,84 @@ export default function BookingRequests() { } }; - 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 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, - pending: requests.filter(r => r.status === 'pending').length, - active: requests.filter(r => r.status === 'active').length, - completed: requests.filter(r => r.status === 'completed').length + depositPaid: counts.depositPaid || 0, + depositConfirmed: counts.depositConfirmed || 0, + completed: counts.completed || 0, }; return (
- -
+ +
+
+

متابعة حجوزات العربون

+

+ يتم جلب جميع الحجوزات من الخادم، مع فتح الحجوزات ذات الحالة depositPaid (2) بشكل افتراضي. +

+
+ + +
+ + {loadError && ( +
+ {loadError} +
+ )} + +
{stats.total}
-
إجمالي الطلبات
+
إجمالي الحجوزات
-
{stats.pending}
-
قيد الانتظار
+
{stats.depositPaid}
+
تم دفع العربون
-
{stats.active}
-
إيجارات نشطة
+
{stats.depositConfirmed}
+
تم تأكيد العربون
{stats.completed}
منتهية
@@ -1409,43 +1517,41 @@ export default function BookingRequests() {

تصفية حسب الحالة

{filteredRequests.length} طلب
+
- {[ - { 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) => ( + {FILTER_TABS.map((tab) => ( ))}
-
- {filteredRequests.map((request) => ( + {isLoading ? ( +
+ +
+ ) : ( +
+ {filteredRequests.map((request) => ( setDetailsDialog({ isOpen: true, request })} + onConfirmDeposit={handleDepositConfirmation} + onViewDetails={(selectedRequest) => setDetailsDialog({ isOpen: true, request: selectedRequest })} confirmingDepositId={confirmingDepositId} /> - ))} -
+ ))} +
+ )} - {filteredRequests.length === 0 && ( + {!isLoading && filteredRequests.length === 0 && (
-

لا توجد طلبات حجز

-

لا توجد طلبات حجز في هذه الفئة

+

لا توجد حجوزات مطابقة

+

+ لم يتم العثور على حجوزات ضمن الحالة الحالية. +

)} - setReasonDialog({ isOpen: false, requestId: null, type: null })} - onConfirm={handleRejectWithReason} - title={reasonDialog.type === 'owner' ? 'رفض من المالك' : 'رفض إداري'} - /> -