+
- {request.status === 'owner_approved' && (
-
+ {canConfirmDeposit && (
-
-
-
- )}
-
- {request.status === 'admin_approved' && (
-
-
-
-
-
-
+ )}
+
تأكيد العربون
+
+ )}
+
+
+ {Number(request.status) === RESERVATION_STATUS.depositConfirmed && (
+
+ تم تأكيد العربون من قبل الإدارة.
)}
- {request.status === 'active' && (
-
-
-
-
-
-
- {request.actualStartDate && (
-
-
- بدأ الإيجار: {request.actualStartDate}
- المدة: {request.days} يوم
-
-
-
- )}
+ {request.notes && (
+
+ {request.notes}
)}
@@ -1023,330 +1197,352 @@ const RequestCard = ({ request, onAction, onViewDetails }) => {
};
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: 'owner_approved',
- requestDate: '2024-02-24',
- ownerApproved: true,
- adminApproved: false,
- 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.id);
- 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);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadReservations();
+ }, [loadReservations]);
+
+ const handleDepositConfirmation = async (request) => {
+ if (!AuthService.isAdmin()) {
+ console.warn('[Admin] Deposit confirmation blocked: current user is not admin', {
+ user: AuthService.getUser(),
+ roles: AuthService.getRoles?.(),
+ request,
+ });
+ toast.error('هذا الإجراء متاح للإدارة فقط');
+ return;
+ }
+
+ if (Number(request?.status) !== RESERVATION_STATUS.depositPaid) {
+ console.warn('[Admin] Deposit confirmation blocked: reservation is not in depositPaid state', {
+ requestId: request?.id,
+ reservationId: request?.reservationId,
+ status: request?.status,
+ });
+ toast.error('يمكن تأكيد العربون فقط عندما تكون الحالة depositPaid');
+ return;
+ }
+
+ const reservationId = Number(request?.reservationId ?? request?.id);
+ if (!Number.isFinite(reservationId)) {
+ toast.error('تعذر تحديد رقم الحجز المطلوب');
+ return;
+ }
+
+ const adminUser = AuthService.getUser();
+ const parsedAdminId = Number(adminUser?.id);
+ const adminId = Number.isFinite(parsedAdminId) ? parsedAdminId : adminUser?.id;
+ const comment =
+ typeof request?.comment === 'string' && request.comment.trim()
+ ? request.comment.trim()
+ : null;
+
+ console.log('[Admin] Preparing admin confirm deposit request', {
+ requestId: request?.id,
+ reservationId,
+ adminId,
+ adminUser,
+ status: request?.status,
+ endpoint: '/Reservations/AdminConfirmDeposit/admin-confirm-deposit',
+ payload: {
+ reservationId,
+ adminId,
+ comment,
+ },
+ });
+
+ if (adminId == null || adminId === '') {
+ console.warn('[Admin] Deposit confirmation blocked: adminId is missing', {
+ adminUser,
+ parsedAdminId,
+ });
+ toast.error('لم نتمكن من تحديد هوية المدير');
+ return;
+ }
+
+ setConfirmingDepositId(request.id);
+
+ try {
+ const result = await adminConfirmDeposit(reservationId, adminId, comment);
+
+ console.log('[Admin] Deposit confirmation response', {
+ requestId: request?.id,
+ reservationId,
+ adminId,
+ comment,
+ status: result?.status,
+ ok: result?.ok,
+ message: result?.message,
+ data: result?.data,
+ });
+
+ if (!result.ok) {
+ throw new Error(result.message || result.data?.message || `HTTP ${result.status}`);
+ }
+
+ setRequests((prev) =>
+ prev.map((req) =>
+ req.id === request.id
+ ? {
+ ...req,
+ status: RESERVATION_STATUS.depositConfirmed,
+ adminApproved: true,
+ securityDepositPaid: true,
+ notes: comment || 'تم تأكيد العربون من قبل الإدارة',
+ }
+ : req,
+ ),
+ );
+
+ toast.success('تم تأكيد العربون بنجاح');
+ } catch (err) {
+ console.error('[Admin] Deposit confirmation failed:', err);
+ toast.error(err.message || 'فشل تأكيد العربون');
+ } finally {
+ setConfirmingDepositId(null);
}
};
- const handleRejectWithReason = (reason) => {
- const { requestId, type } = reasonDialog;
-
- setRequests(prev =>
- prev.map(req =>
- req.id === requestId
- ? {
- ...req,
- status: 'rejected',
- [type === 'owner' ? 'ownerApproved' : 'adminApproved']: false,
- rejectionReason: reason,
- rejectionType: type,
- notes: `${type === 'owner' ? 'رفض من المالك' : 'رفض إداري'}: ${reason}`
- }
- : req
- )
- );
-
- setReasonDialog({ isOpen: false, requestId: null, type: null });
- };
-
- const handleOwnerApprove = (requestId) => {
- setRequests(prev =>
- prev.map(req =>
- req.id === requestId
- ? {
- ...req,
- ownerApproved: true,
- status: 'owner_approved',
- notes: 'تمت الموافقة من قبل المالك'
- }
- : req
- )
- );
- };
-
- const handleAdminApprove = (requestId) => {
- setRequests(prev =>
- prev.map(req =>
- req.id === requestId
- ? {
- ...req,
- adminApproved: true,
- status: 'admin_approved',
- notes: 'تمت الموافقة من قبل الإدارة'
- }
- : req
- )
- );
- };
- const handleDepositConfirmation = (requestId) => {
- setRequests(prev =>
- prev.map(req => {
- if (req.id === requestId) {
- toast.success('✓ تم تأكيد دفع الرعبون بنجاح!', { id: requestId });
- return { ...req, securityDepositPaid: true };
- }
- return req;
- })
- );
- };
-
- const handleKeyDelivery = (requestId, userType) => {
- setRequests(prev =>
- prev.map(req => {
- if (req.id === requestId) {
- const updates = {};
-
- if (userType === 'owner') {
- updates.ownerDelivered = true;
- updates.notes = 'تم تسليم المفتاح من قبل المالك';
- }
- if (userType === 'tenant') {
- updates.tenantReceived = true;
- updates.notes = 'تم استلام العقار من قبل المستأجر';
- }
-
- if ((userType === 'owner' && req.tenantReceived) ||
- (userType === 'tenant' && req.ownerDelivered)) {
- updates.status = 'active';
- updates.contractSigned = true;
- updates.actualStartDate = new Date().toISOString().split('T')[0];
- updates.notes = 'بدأت فترة الإيجار الفعلية';
- }
-
- return { ...req, ...updates };
- }
- return req;
- })
- );
- };
-
- const handleEndRental = (requestId, userType) => {
- setRequests(prev =>
- prev.map(req => {
- if (req.id === requestId) {
- const updates = {};
-
- if (userType === 'tenant') {
- updates.tenantLeft = true;
- updates.notes = 'غادر المستأجر العقار';
- }
- if (userType === 'owner') {
- updates.ownerReceived = true;
- updates.notes = 'استلم المالك العقار';
- }
-
- if ((userType === 'tenant' && req.ownerReceived) ||
- (userType === 'owner' && req.tenantLeft)) {
-
- const actualEndDate = new Date();
- const actualStartDate = new Date(req.actualStartDate || req.startDate);
- const actualDays = Math.ceil((actualEndDate - actualStartDate) / (1000 * 60 * 60 * 24));
- const actualAmount = req.dailyPrice * actualDays;
-
- const damageDeduction = 0;
- const refundAmount = req.securityDeposit - damageDeduction;
-
- updates.status = 'completed';
- updates.actualEndDate = actualEndDate.toISOString().split('T')[0];
- updates.actualDays = actualDays;
- updates.actualAmount = actualAmount;
- updates.securityDepositReturned = refundAmount;
- updates.damageDeduction = damageDeduction;
- updates.notes = `انتهى الإيجار بعد ${actualDays} يوم - المبلغ الفعلي: ${actualAmount.toLocaleString()} ل.س - مسترد الضمان: ${refundAmount.toLocaleString()} ل.س`;
- }
-
- return { ...req, ...updates };
- }
- return req;
- })
- );
- };
-
- const filteredRequests = requests.filter(req =>
- filter === 'all' ? true : req.status === filter
+ const 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}
منتهية
@@ -1358,42 +1554,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) => (
- setDetailsDialog({ isOpen: true, request })}
- />
- ))}
-
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {filteredRequests.map((request) => (
+ 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' ? 'رفض من المالك' : 'رفض إداري'}
- />
-
);
-}
\ No newline at end of file
+}
diff --git a/app/services/AuthService.js b/app/services/AuthService.js
index ef5ca0c..041ba40 100644
--- a/app/services/AuthService.js
+++ b/app/services/AuthService.js
@@ -93,6 +93,18 @@ const AuthService = Object.freeze({
};
},
+ /**
+ * Get current authenticated user id
+ * @returns {number|string|null}
+ */
+ getUserId() {
+ const user = this.getUser();
+ if (!user?.id) return null;
+
+ const parsedId = Number(user.id);
+ return Number.isFinite(parsedId) ? parsedId : user.id;
+ },
+
/**
* Get roles array from JWT
* @returns {string[]}
diff --git a/app/utils/api.js b/app/utils/api.js
index 0f915ed..69493bc 100644
--- a/app/utils/api.js
+++ b/app/utils/api.js
@@ -134,7 +134,7 @@ export async function getAvailableDateRanges(propertyId) {
}
export async function getReservations() {
- return apiFetch('/Reservations/GetReservations');
+ return apiFetch('/Reservations/GetAllReservations');
}
export async function getReservation(id) {
@@ -380,6 +380,68 @@ export async function confirmDepositPayment(bookingId) {
});
}
+export async function adminConfirmDeposit(reservationId, adminId, comment = null) {
+ const token = AuthService.getToken();
+ const endpoint = `${API_BASE}/Reservations/AdminConfirmDeposit/admin-confirm-deposit`;
+ const normalizedComment =
+ typeof comment === 'string' && comment.trim()
+ ? comment.trim()
+ : null;
+ const payload = {
+ reservationId,
+ adminId,
+ comment: normalizedComment,
+ };
+
+ console.log('[API] AdminConfirmDeposit request', {
+ method: 'PUT',
+ endpoint,
+ payload,
+ adminIdSource: 'jwt-user-id',
+ hasToken: Boolean(token),
+ tokenPreview: token ? `${token.slice(0, 18)}...${token.slice(-8)}` : null,
+ });
+
+ const res = await fetch(endpoint, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token && { Authorization: `Bearer ${token}` }),
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const text = await res.text();
+ let data = null;
+
+ console.log('[API] AdminConfirmDeposit raw response', {
+ status: res.status,
+ ok: res.ok,
+ endpoint,
+ rawText: text,
+ });
+
+ try {
+ data = text ? JSON.parse(text) : null;
+ if (data && typeof data === 'object' && 'data' in data) {
+ data = data.data;
+ }
+ } catch {
+ data = text;
+ }
+
+ const message = typeof data === 'object' && data?.message ? data.message : null;
+
+ console.log('[API] AdminConfirmDeposit parsed response', {
+ status: res.status,
+ ok: res.ok,
+ message,
+ data,
+ });
+
+ return { status: res.status, data, ok: res.ok, message };
+}
+
export async function updateBookingStatus(bookingId, status) {
return apiFetch('/Reservations/UpdateStatus', {
method: 'PUT',