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 }) => {
+ يتم جلب جميع الحجوزات من الخادم، مع فتح الحجوزات ذات الحالة depositPaid (2) بشكل افتراضي. +
+لا توجد طلبات حجز في هذه الفئة
++ لم يتم العثور على حجوزات ضمن الحالة الحالية. +
)} -