2026-04-16 21:18:31 +03:00
|
|
|
|
'use client';
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
2026-02-15 01:53:37 +03:00
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
|
|
import {
|
|
|
|
|
|
CheckCircle,
|
|
|
|
|
|
XCircle,
|
|
|
|
|
|
Clock,
|
|
|
|
|
|
User,
|
|
|
|
|
|
Home,
|
|
|
|
|
|
Calendar,
|
|
|
|
|
|
DollarSign,
|
|
|
|
|
|
AlertCircle,
|
|
|
|
|
|
Key,
|
|
|
|
|
|
DoorOpen,
|
|
|
|
|
|
Shield,
|
|
|
|
|
|
Phone,
|
|
|
|
|
|
Mail,
|
|
|
|
|
|
ChevronDown,
|
|
|
|
|
|
ChevronUp,
|
|
|
|
|
|
FileText,
|
|
|
|
|
|
Download,
|
|
|
|
|
|
Printer,
|
2026-03-27 00:34:59 +03:00
|
|
|
|
History,
|
2026-04-15 12:28:01 +03:00
|
|
|
|
Loader2,
|
|
|
|
|
|
CreditCard
|
2026-02-15 01:53:37 +03:00
|
|
|
|
} from 'lucide-react';
|
2026-03-27 00:34:59 +03:00
|
|
|
|
import jsPDF from 'jspdf';
|
|
|
|
|
|
import html2canvas from 'html2canvas';
|
|
|
|
|
|
import toast, { Toaster } from 'react-hot-toast';
|
2026-04-16 21:15:21 +03:00
|
|
|
|
import AuthService from '../../services/AuthService';
|
2026-04-16 22:06:57 +03:00
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
|
|
|
|
|
const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' }) => {
|
|
|
|
|
|
const [reason, setReason] = useState(defaultReason);
|
|
|
|
|
|
const [otherReason, setOtherReason] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
const commonReasons = [
|
2026-04-16 21:30:22 +03:00
|
|
|
|
'أعمال صيانة في العقار',
|
|
|
|
|
|
'العقار غير متاح في هذه التواريخ',
|
|
|
|
|
|
'مشكلة في وثائق المستأجر',
|
|
|
|
|
|
'المالك غير متاح للتسليم',
|
|
|
|
|
|
'تأخر في دفع الضمان',
|
|
|
|
|
|
'سبب آخر'
|
2026-02-15 01:53:37 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<p className="text-sm text-gray-500 mt-1">يرجى تحديد سبب الرفض</p>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{commonReasons.map((r) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={r}
|
|
|
|
|
|
onClick={() => {
|
2026-04-16 21:30:22 +03:00
|
|
|
|
if (r === 'سبب آخر') {
|
2026-02-15 01:53:37 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
onConfirm(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="w-full text-right p-3 border rounded-xl hover:bg-gray-50 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
{r}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
<textarea
|
2026-04-16 21:30:22 +03:00
|
|
|
|
placeholder="اكتب سبباً آخر..."
|
2026-02-15 01:53:37 +03:00
|
|
|
|
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()}
|
|
|
|
|
|
>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
تأكيد الرفض
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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"
|
|
|
|
|
|
>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
إلغاء
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 00:34:59 +03:00
|
|
|
|
const PDFExportButton = ({ request, onExportComplete }) => {
|
|
|
|
|
|
const [isExporting, setIsExporting] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (amount) => {
|
2026-04-16 21:30:22 +03:00
|
|
|
|
return amount?.toLocaleString() + ' ل.س';
|
2026-03-27 00:34:59 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const generatePDF = async () => {
|
|
|
|
|
|
if (!request) {
|
2026-04-16 21:30:22 +03:00
|
|
|
|
toast.error('لا توجد بيانات للتصدير');
|
2026-03-27 00:34:59 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const statusMeta = getStatusMeta(request.status);
|
|
|
|
|
|
const userTypeLabel = request.userType || 'غير محدد';
|
2026-03-27 00:34:59 +03:00
|
|
|
|
setIsExporting(true);
|
2026-04-16 21:30:22 +03:00
|
|
|
|
const loadingToast = toast.loading('جاري إنشاء ملف PDF...', { id: 'pdf-export' });
|
2026-03-27 00:34:59 +03:00
|
|
|
|
|
|
|
|
|
|
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; }
|
2026-04-16 22:06:57 +03:00
|
|
|
|
.pdf-status-ownerConfirmed { background: #dbeafe; color: #2563eb; }
|
|
|
|
|
|
.pdf-status-depositPaid { background: #e0e7ff; color: #4338ca; }
|
|
|
|
|
|
.pdf-status-depositConfirmed { background: #d1fae5; color: #059669; }
|
2026-03-27 00:34:59 +03:00
|
|
|
|
.pdf-status-completed { background: #e5e7eb; color: #4b5563; }
|
2026-04-16 22:06:57 +03:00
|
|
|
|
.pdf-status-cancelled { background: #fee2e2; color: #dc2626; }
|
|
|
|
|
|
.pdf-status-unknown { background: #f3f4f6; color: #4b5563; }
|
2026-03-27 00:34:59 +03:00
|
|
|
|
.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>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<div class="pdf-title">تقرير طلب حجز #${request.id}</div>
|
|
|
|
|
|
<div class="pdf-subtitle">تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')} | ${new Date().toLocaleTimeString('ar-SA')}</div>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pdf-section">
|
|
|
|
|
|
<div class="pdf-section-title">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span></span> معلومات المستأجر
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-grid">
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">الاسم الكامل:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.user || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">نوع الهوية:</span>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<span class="pdf-value">${userTypeLabel}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">رقم الهوية:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.identityNumber || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">البريد الإلكتروني:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.userEmail || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">رقم الهاتف:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.userPhone || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pdf-section">
|
|
|
|
|
|
<div class="pdf-section-title">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span></span> معلومات العقار
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-grid">
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">العقار:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.property || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">السعر اليومي:</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<span class="pdf-value">${formatCurrency(request.dailyPrice)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pdf-section">
|
|
|
|
|
|
<div class="pdf-section-title">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span></span> تفاصيل الحجز
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-grid">
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">تاريخ البداية:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.startDate || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">تاريخ النهاية:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.endDate || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">عدد الأيام:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.days || 0} يوم</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">الحالة:</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<span class="pdf-value">
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<span class="pdf-status pdf-status-${statusMeta.pdf}">
|
|
|
|
|
|
${statusMeta.label}
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pdf-section">
|
|
|
|
|
|
<div class="pdf-section-title">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span></span> المعلومات المالية
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-grid">
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">سلفة الضمان:</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<span class="pdf-value">${formatCurrency(request.securityDeposit)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">المبلغ الإجمالي:</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<span class="pdf-value pdf-amount">${formatCurrency(request.totalAmount)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">نسبة العمولة:</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<span class="pdf-value">${request.commissionRate || 0}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">نوع العمولة:</span>
|
|
|
|
|
|
<span class="pdf-value">${request.commissionType || 'غير محدد'}</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="pdf-info-item">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-label">قيمة العمولة:</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<span class="pdf-value">${formatCurrency(request.commissionAmount)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
${request.notes ? `
|
|
|
|
|
|
<div class="pdf-section">
|
|
|
|
|
|
<div class="pdf-section-title">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span></span> ملاحظات
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</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">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span></span> سجل الإجراءات
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</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>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-value">تم إنشاء الطلب</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
${request.ownerApproved ? `
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
2026-04-16 21:15:21 +03:00
|
|
|
|
<span class="pdf-badge">✓</span>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-value">تمت موافقة المالك</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${request.adminApproved ? `
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
2026-04-16 21:15:21 +03:00
|
|
|
|
<span class="pdf-badge">✓</span>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-value">تمت موافقة الإدارة</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${request.ownerDelivered ? `
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
|
|
|
|
<span class="pdf-badge"></span>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-value">تم تسليم المفتاح</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${request.tenantReceived ? `
|
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
|
|
|
|
<span class="pdf-badge"></span>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span class="pdf-value">تم استلام العقار</span>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pdf-footer">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<div>تقرير صادر عن نظام SweetHome لإدارة العقارات</div>
|
|
|
|
|
|
<div style="margin-top: 5px;">جميع الحقوق محفوظة © ${new Date().getFullYear()} SweetHome</div>
|
|
|
|
|
|
<div style="margin-top: 5px; font-size: 9px;">هذا التقرير تم إنشاؤه تلقائياً ولا يحتاج إلى توقيع</div>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</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++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 21:30:22 +03:00
|
|
|
|
const fileName = `تقرير_طلب_${request.id}_${new Date().toISOString().split('T')[0]}.pdf`;
|
2026-03-27 00:34:59 +03:00
|
|
|
|
pdf.save(fileName);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.removeChild(printContent);
|
|
|
|
|
|
|
2026-04-16 21:30:22 +03:00
|
|
|
|
toast.success(`تم تصدير التقرير بنجاح! (${pageCount} صفحة)`, { id: 'pdf-export' });
|
2026-03-27 00:34:59 +03:00
|
|
|
|
|
|
|
|
|
|
if (onExportComplete) {
|
|
|
|
|
|
onExportComplete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error generating PDF:', error);
|
2026-04-16 21:30:22 +03:00
|
|
|
|
toast.error('حدث خطأ أثناء إنشاء ملف PDF', { id: 'pdf-export' });
|
2026-03-27 00:34:59 +03:00
|
|
|
|
} 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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
جاري التصدير...
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Download className="w-5 h-5" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
تصدير PDF
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
|
|
|
|
|
|
if (!isOpen || !request) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (amount) => {
|
2026-04-16 21:30:22 +03:00
|
|
|
|
return amount?.toLocaleString() + ' ل.س';
|
2026-02-15 01:53:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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()}
|
|
|
|
|
|
>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
تفاصيل الطلب #{request.id}
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</h2>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<p className="text-amber-100 text-sm mt-1">جميع معلومات الطلب في مكان واحد</p>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
2026-03-27 00:34:59 +03:00
|
|
|
|
className="p-2 hover:bg-white/20 rounded-full transition-colors"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<XCircle className="w-6 h-6" />
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
معلومات المستأجر
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</h3>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">الاسم الكامل</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium">{request.user}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">نوع الهوية</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium">
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{request.userType || 'غير محدد'}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<span className="text-xs text-gray-500 mr-2">{request.identityNumber}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">البريد الإلكتروني</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium flex items-center gap-1">
|
|
|
|
|
|
<Mail className="w-3 h-3 text-gray-400" />
|
|
|
|
|
|
{request.userEmail}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">رقم الهاتف</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
معلومات العقار
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</h3>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">العقار</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium">{request.property}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">السعر اليومي</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
تفاصيل الحجز
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</h3>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">تاريخ البداية</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium">{request.startDate}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">تاريخ النهاية</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium">{request.endDate}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">عدد الأيام</label>
|
|
|
|
|
|
<div className="font-medium">{request.days} يوم</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">المبلغ الإجمالي</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
المعلومات المالية
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</h3>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">سلفة الضمان</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium text-blue-600">{formatCurrency(request.securityDeposit)}</div>
|
|
|
|
|
|
</div>
|
2026-04-15 12:28:01 +03:00
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">حالة العربون</label>
|
2026-04-15 12:28:01 +03:00
|
|
|
|
<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'
|
|
|
|
|
|
}`}>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
{request.securityDepositPaid ? '✓ تم الدفع' : '⏳ في انتظار الدفع'}
|
2026-04-15 12:28:01 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">نسبة العمولة</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium">{request.commissionRate}%</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">نوع العمولة</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium text-amber-600">{request.commissionType}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<label className="text-xs text-gray-500">قيمة العمولة</label>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-medium text-amber-600">{formatCurrency(request.commissionAmount)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="bg-gray-50 p-4 rounded-xl">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<h3 className="font-bold mb-3 flex items-center gap-2">
|
|
|
|
|
|
<History className="w-4 h-4" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
سجل الإجراءات
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span className="text-gray-600">تم إنشاء الطلب: {request.requestDate}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
{request.ownerApproved && (
|
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span className="text-gray-600">موافقة المالك</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{request.adminApproved && (
|
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span className="text-gray-600">موافقة الإدارة</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{request.ownerDelivered && (
|
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
|
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span className="text-gray-600">تم تسليم المفتاح من المالك</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{request.notes && (
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="mt-3 p-3 bg-white rounded-lg border">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<p className="text-sm text-gray-600">{request.notes}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-04-16 22:40:59 +03:00
|
|
|
|
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<div className="sticky bottom-0 bg-white border-t p-4 flex gap-3 shadow-lg">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
طباعة التفاصيل
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</button>
|
2026-03-27 00:34:59 +03:00
|
|
|
|
|
|
|
|
|
|
<PDFExportButton request={request} />
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const RequestCard = ({ request, onConfirmDeposit, onViewDetails, confirmingDepositId }) => {
|
2026-04-16 21:30:22 +03:00
|
|
|
|
const [expanded, setExpanded] = useState(
|
2026-04-16 22:06:57 +03:00
|
|
|
|
Number(request.status) === RESERVATION_STATUS.depositPaid,
|
2026-04-16 21:30:22 +03:00
|
|
|
|
);
|
2026-04-16 21:15:21 +03:00
|
|
|
|
const isConfirmingDeposit = confirmingDepositId === request.id;
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const statusMeta = getStatusMeta(request.status);
|
|
|
|
|
|
const canConfirmDeposit = Number(request.status) === RESERVATION_STATUS.depositPaid;
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (amount) => {
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const value = Number(amount) || 0;
|
|
|
|
|
|
return value.toLocaleString() + ' ل.س';
|
2026-02-15 01:53:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
2026-04-16 22:06:57 +03:00
|
|
|
|
className={`bg-white border-2 rounded-2xl overflow-hidden transition-all hover:shadow-xl ${statusMeta.card}`}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<div className="p-4 cursor-pointer" onClick={() => setExpanded((prev) => !prev)}>
|
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="flex items-center gap-3">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span className="text-lg font-bold text-gray-900">طلب #{request.id}</span>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium shadow-sm ${statusMeta.badge}`}>
|
|
|
|
|
|
{statusMeta.label}
|
|
|
|
|
|
</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<span>{request.days} أيام</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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"
|
|
|
|
|
|
>
|
2026-04-15 12:28:01 +03:00
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="bg-blue-50 p-2 rounded-lg">
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<div className="text-xs text-gray-500">العربون</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-bold text-blue-600">{formatCurrency(request.securityDeposit)}</div>
|
|
|
|
|
|
</div>
|
2026-04-15 12:28:01 +03:00
|
|
|
|
<div className={`p-2 rounded-lg ${request.securityDepositPaid ? 'bg-green-50' : 'bg-yellow-50'}`}>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<div className="text-xs text-gray-500">حالة العربون</div>
|
2026-04-15 12:28:01 +03:00
|
|
|
|
<div className={`font-bold text-xs ${request.securityDepositPaid ? 'text-green-600' : 'text-yellow-600'}`}>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{request.securityDepositPaid ? 'تم دفع العربون' : 'بانتظار الدفع'}
|
2026-04-15 12:28:01 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="bg-amber-50 p-2 rounded-lg">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<div className="text-xs text-gray-500">العمولة</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="font-bold text-amber-600">{request.commissionRate}% ({request.commissionType})</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="bg-purple-50 p-2 rounded-lg">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<div className="text-xs text-gray-500">مدة الإيجار</div>
|
|
|
|
|
|
<div className="font-bold text-purple-600 text-xs">{request.startDate} إلى {request.endDate}</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{(request.userEmail || request.userPhone) && (
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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" />
|
2026-04-16 21:30:22 +03:00
|
|
|
|
معلومات الاتصال
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</h4>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<Mail className="w-3 h-3 text-gray-500" />
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<span>{request.userEmail || 'غير متوفر'}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<Phone className="w-3 h-3 text-gray-500" />
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<span>{request.userPhone || 'غير متوفر'}</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<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>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{canConfirmDeposit && (
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<button
|
2026-04-16 22:06:57 +03:00
|
|
|
|
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]'
|
|
|
|
|
|
}`}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{isConfirmingDeposit ? (
|
|
|
|
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<CreditCard className="w-5 h-5" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span>تأكيد العربون</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</button>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{Number(request.status) === RESERVATION_STATUS.depositConfirmed && (
|
|
|
|
|
|
<div className="rounded-xl bg-green-50 px-4 py-3 text-sm text-green-800">
|
|
|
|
|
|
تم تأكيد العربون من قبل الإدارة.
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{request.notes && (
|
|
|
|
|
|
<div className="rounded-xl bg-gray-50 px-4 py-3 text-sm text-gray-700">
|
|
|
|
|
|
{request.notes}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default function BookingRequests() {
|
2026-04-16 22:06:57 +03:00
|
|
|
|
/*
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
*/
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const [requests, setRequests] = useState([]);
|
|
|
|
|
|
const [filter, setFilter] = useState('depositPaid');
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const [detailsDialog, setDetailsDialog] = useState({ isOpen: false, request: null });
|
2026-04-16 21:15:21 +03:00
|
|
|
|
const [confirmingDepositId, setConfirmingDepositId] = useState(null);
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
const [loadError, setLoadError] = useState('');
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const loadReservations = useCallback(async () => {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
setLoadError('');
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
try {
|
|
|
|
|
|
if (!AuthService.isAdmin()) {
|
|
|
|
|
|
throw new Error('هذه الصفحة متاحة للإدارة فقط');
|
|
|
|
|
|
}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
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]);
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:49:15 +03:00
|
|
|
|
const handleDepositConfirmation = async (request) => {
|
2026-04-16 21:15:21 +03:00
|
|
|
|
if (!AuthService.isAdmin()) {
|
2026-04-16 22:33:19 +03:00
|
|
|
|
console.warn('[Admin] Deposit confirmation blocked: current user is not admin', {
|
|
|
|
|
|
user: AuthService.getUser(),
|
|
|
|
|
|
roles: AuthService.getRoles?.(),
|
|
|
|
|
|
request,
|
|
|
|
|
|
});
|
2026-04-16 21:15:21 +03:00
|
|
|
|
toast.error('هذا الإجراء متاح للإدارة فقط');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
if (Number(request?.status) !== RESERVATION_STATUS.depositPaid) {
|
2026-04-16 22:33:19 +03:00
|
|
|
|
console.warn('[Admin] Deposit confirmation blocked: reservation is not in depositPaid state', {
|
|
|
|
|
|
requestId: request?.id,
|
|
|
|
|
|
reservationId: request?.reservationId,
|
|
|
|
|
|
status: request?.status,
|
|
|
|
|
|
});
|
2026-04-16 22:06:57 +03:00
|
|
|
|
toast.error('يمكن تأكيد العربون فقط عندما تكون الحالة depositPaid');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 21:15:21 +03:00
|
|
|
|
const reservationId = Number(request?.reservationId ?? request?.id);
|
|
|
|
|
|
if (!Number.isFinite(reservationId)) {
|
2026-04-16 22:06:57 +03:00
|
|
|
|
toast.error('تعذر تحديد رقم الحجز المطلوب');
|
2026-04-16 21:15:21 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const adminUser = AuthService.getUser();
|
|
|
|
|
|
const parsedAdminId = Number(adminUser?.id);
|
|
|
|
|
|
const adminId = Number.isFinite(parsedAdminId) ? parsedAdminId : adminUser?.id;
|
2026-04-16 22:49:15 +03:00
|
|
|
|
const comment =
|
|
|
|
|
|
typeof request?.comment === 'string' && request.comment.trim()
|
|
|
|
|
|
? request.comment.trim()
|
2026-04-16 22:40:59 +03:00
|
|
|
|
: null;
|
2026-04-16 21:15:21 +03:00
|
|
|
|
|
2026-04-16 22:33:19 +03:00
|
|
|
|
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,
|
2026-04-16 22:49:15 +03:00
|
|
|
|
comment,
|
2026-04-16 22:33:19 +03:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-16 21:15:21 +03:00
|
|
|
|
if (adminId == null || adminId === '') {
|
2026-04-16 22:33:19 +03:00
|
|
|
|
console.warn('[Admin] Deposit confirmation blocked: adminId is missing', {
|
|
|
|
|
|
adminUser,
|
|
|
|
|
|
parsedAdminId,
|
|
|
|
|
|
});
|
2026-04-16 21:15:21 +03:00
|
|
|
|
toast.error('لم نتمكن من تحديد هوية المدير');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setConfirmingDepositId(request.id);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-16 22:49:15 +03:00
|
|
|
|
const result = await adminConfirmDeposit(reservationId, adminId, comment);
|
2026-04-16 21:15:21 +03:00
|
|
|
|
|
2026-04-16 22:33:19 +03:00
|
|
|
|
console.log('[Admin] Deposit confirmation response', {
|
|
|
|
|
|
requestId: request?.id,
|
|
|
|
|
|
reservationId,
|
2026-04-16 22:40:59 +03:00
|
|
|
|
adminId,
|
2026-04-16 22:49:15 +03:00
|
|
|
|
comment,
|
2026-04-16 22:33:19 +03:00
|
|
|
|
status: result?.status,
|
|
|
|
|
|
ok: result?.ok,
|
|
|
|
|
|
message: result?.message,
|
|
|
|
|
|
data: result?.data,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-16 21:15:21 +03:00
|
|
|
|
if (!result.ok) {
|
|
|
|
|
|
throw new Error(result.message || result.data?.message || `HTTP ${result.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setRequests((prev) =>
|
|
|
|
|
|
prev.map((req) =>
|
|
|
|
|
|
req.id === request.id
|
|
|
|
|
|
? {
|
|
|
|
|
|
...req,
|
2026-04-16 22:06:57 +03:00
|
|
|
|
status: RESERVATION_STATUS.depositConfirmed,
|
|
|
|
|
|
adminApproved: true,
|
2026-04-16 21:15:21 +03:00
|
|
|
|
securityDepositPaid: true,
|
2026-04-16 22:49:15 +03:00
|
|
|
|
notes: comment || 'تم تأكيد العربون من قبل الإدارة',
|
2026-04-16 21:15:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
: req,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success('تم تأكيد العربون بنجاح');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('[Admin] Deposit confirmation failed:', err);
|
|
|
|
|
|
toast.error(err.message || 'فشل تأكيد العربون');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setConfirmingDepositId(null);
|
|
|
|
|
|
}
|
2026-04-15 12:28:01 +03:00
|
|
|
|
};
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
const filteredRequests = requests.filter((req) =>
|
|
|
|
|
|
filter === 'all' ? true : getStatusKey(req.status) === filter,
|
2026-02-15 01:53:37 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
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;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
2026-02-15 01:53:37 +03:00
|
|
|
|
const stats = {
|
|
|
|
|
|
total: requests.length,
|
2026-04-16 22:06:57 +03:00
|
|
|
|
depositPaid: counts.depositPaid || 0,
|
|
|
|
|
|
depositConfirmed: counts.depositConfirmed || 0,
|
|
|
|
|
|
completed: counts.completed || 0,
|
2026-02-15 01:53:37 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-6">
|
2026-03-27 00:34:59 +03:00
|
|
|
|
<Toaster position="top-center" reverseOrder={false} />
|
2026-04-16 22:06:57 +03:00
|
|
|
|
|
|
|
|
|
|
<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">
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<div className="text-sm opacity-90">إجمالي الحجوزات</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.1 }}
|
2026-04-16 22:06:57 +03:00
|
|
|
|
className="bg-gradient-to-br from-indigo-600 to-indigo-700 text-white rounded-2xl p-4 shadow-lg"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<div className="text-3xl font-bold mb-1">{stats.depositPaid}</div>
|
|
|
|
|
|
<div className="text-sm opacity-90">تم دفع العربون</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.2 }}
|
2026-04-16 22:06:57 +03:00
|
|
|
|
className="bg-gradient-to-br from-green-600 to-green-700 text-white rounded-2xl p-4 shadow-lg"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<div className="text-3xl font-bold mb-1">{stats.depositConfirmed}</div>
|
|
|
|
|
|
<div className="text-sm opacity-90">تم تأكيد العربون</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.3 }}
|
2026-04-16 22:06:57 +03:00
|
|
|
|
className="bg-gradient-to-br from-gray-600 to-gray-700 text-white rounded-2xl p-4 shadow-lg"
|
2026-02-15 01:53:37 +03:00
|
|
|
|
>
|
|
|
|
|
|
<div className="text-3xl font-bold mb-1">{stats.completed}</div>
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<div className="text-sm opacity-90">منتهية</div>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</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">
|
2026-04-16 21:30:22 +03:00
|
|
|
|
<h3 className="font-bold text-gray-700">تصفية حسب الحالة</h3>
|
|
|
|
|
|
<span className="text-sm text-gray-500">{filteredRequests.length} طلب</span>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</div>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<div className="flex flex-wrap gap-2">
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{FILTER_TABS.map((tab) => (
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<button
|
|
|
|
|
|
key={tab.id}
|
|
|
|
|
|
onClick={() => setFilter(tab.id)}
|
2026-04-16 22:06:57 +03:00
|
|
|
|
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
|
2026-02-15 01:53:37 +03:00
|
|
|
|
}`}
|
|
|
|
|
|
>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{tab.label} ({counts[tab.id] || 0})
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{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) => (
|
2026-04-16 21:15:21 +03:00
|
|
|
|
<RequestCard
|
|
|
|
|
|
key={request.id}
|
|
|
|
|
|
request={request}
|
2026-04-16 22:49:15 +03:00
|
|
|
|
onConfirmDeposit={handleDepositConfirmation}
|
2026-04-16 22:06:57 +03:00
|
|
|
|
onViewDetails={(selectedRequest) => setDetailsDialog({ isOpen: true, request: selectedRequest })}
|
2026-04-16 21:15:21 +03:00
|
|
|
|
confirmingDepositId={confirmingDepositId}
|
|
|
|
|
|
/>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-15 01:53:37 +03:00
|
|
|
|
|
2026-04-16 22:06:57 +03:00
|
|
|
|
{!isLoading && filteredRequests.length === 0 && (
|
2026-02-15 01:53:37 +03:00
|
|
|
|
<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>
|
2026-04-16 22:06:57 +03:00
|
|
|
|
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد حجوزات مطابقة</h3>
|
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
|
لم يتم العثور على حجوزات ضمن الحالة الحالية.
|
|
|
|
|
|
</p>
|
2026-02-15 01:53:37 +03:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<RequestDetailsDialog
|
|
|
|
|
|
request={detailsDialog.request}
|
|
|
|
|
|
isOpen={detailsDialog.isOpen}
|
|
|
|
|
|
onClose={() => setDetailsDialog({ isOpen: false, request: null })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2026-04-16 21:15:21 +03:00
|
|
|
|
}
|