1521 lines
46 KiB
JavaScript
1521 lines
46 KiB
JavaScript
|
|
// 'use client';
|
|
|
|
// import { useState, useEffect, useCallback } from 'react';
|
|
// import { motion } from 'framer-motion';
|
|
// import { useRouter } from 'next/navigation';
|
|
// import {
|
|
// Calendar,
|
|
// Clock,
|
|
// CheckCircle,
|
|
// XCircle,
|
|
// Eye,
|
|
// Loader2,
|
|
// Search,
|
|
// MapPin,
|
|
// DollarSign,
|
|
// Home,
|
|
// ArrowLeft,
|
|
// RefreshCw,
|
|
// } from 'lucide-react';
|
|
// import toast, { Toaster } from 'react-hot-toast';
|
|
// import AuthService from '../../services/AuthService';
|
|
// import { getRentProperty } from '../../utils/api';
|
|
|
|
// const API_BASE =
|
|
// process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
|
|
|
// const STATUS_MAP = [
|
|
// 'pending',
|
|
// 'ownerConfirmed',
|
|
// 'depositPaid',
|
|
// 'depositConfirmed',
|
|
// 'completed',
|
|
// 'cancelled',
|
|
// ];
|
|
|
|
// const STATUS_UI = {
|
|
// pending: {
|
|
// label: 'قيد الانتظار',
|
|
// color: 'bg-yellow-100 text-yellow-800',
|
|
// icon: Clock,
|
|
// },
|
|
// ownerConfirmed: {
|
|
// label: 'مؤكد',
|
|
// color: 'bg-green-100 text-green-800',
|
|
// icon: CheckCircle,
|
|
// },
|
|
// depositPaid: {
|
|
// label: 'تم دفع السلفة',
|
|
// color: 'bg-indigo-100 text-indigo-800',
|
|
// icon: DollarSign,
|
|
// },
|
|
// depositConfirmed: {
|
|
// label: 'مؤكد نهائياً',
|
|
// color: 'bg-green-100 text-green-800',
|
|
// icon: CheckCircle,
|
|
// },
|
|
// completed: {
|
|
// label: 'منتهي',
|
|
// color: 'bg-blue-100 text-blue-800',
|
|
// icon: CheckCircle,
|
|
// },
|
|
// cancelled: {
|
|
// label: 'ملغي',
|
|
// color: 'bg-gray-100 text-gray-800',
|
|
// icon: XCircle,
|
|
// },
|
|
// };
|
|
|
|
// const getStatusKey = (code) => STATUS_MAP[Number(code)] || 'pending';
|
|
|
|
// const sLabel = (code) => STATUS_UI[getStatusKey(code)]?.label ?? String(code);
|
|
|
|
// const sColor = (code) =>
|
|
// STATUS_UI[getStatusKey(code)]?.color ?? 'bg-gray-100 text-gray-700';
|
|
|
|
// const sIcon = (code) => STATUS_UI[getStatusKey(code)]?.icon ?? Clock;
|
|
|
|
// function StatusBadge({ code }) {
|
|
// const Icon = sIcon(code);
|
|
|
|
// return (
|
|
// <span
|
|
// className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${sColor(
|
|
// code
|
|
// )}`}
|
|
// >
|
|
// <Icon className="w-3 h-3" /> {sLabel(code)}
|
|
// </span>
|
|
// );
|
|
// }
|
|
|
|
// async function enrich(r) {
|
|
// if (!r?.propertyId) return r;
|
|
|
|
// try {
|
|
// const prop = await getRentProperty(r.propertyId);
|
|
|
|
// return {
|
|
// ...r,
|
|
// _prop: prop?.propertyInformation ?? prop ?? null,
|
|
// };
|
|
// } catch {
|
|
// return {
|
|
// ...r,
|
|
// _prop: null,
|
|
// };
|
|
// }
|
|
// }
|
|
|
|
// const pAddr = (p) => p?.address ?? '';
|
|
// const pImgs = (p) => (Array.isArray(p?.images) ? p.images : []);
|
|
// const pBeds = (p) => p?.numberOfBedRooms ?? 0;
|
|
// const pBaths = (p) => p?.numberOfBathRooms ?? 0;
|
|
|
|
// const API = (token, method, path, body) => {
|
|
// return fetch(`${API_BASE}${path}`, {
|
|
// method: method || 'GET',
|
|
// headers: {
|
|
// 'Content-Type': 'application/json',
|
|
// ...(token && { Authorization: `Bearer ${token}` }),
|
|
// },
|
|
// ...(body && { body: JSON.stringify(body) }),
|
|
// });
|
|
// };
|
|
|
|
// function buildImageUrl(path) {
|
|
// if (!path) return null;
|
|
|
|
// if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
// return path;
|
|
// }
|
|
|
|
// return `${API_BASE}${path}`;
|
|
// }
|
|
|
|
// function OwnerCard({
|
|
// r,
|
|
// onViewDetails,
|
|
// onConfirm,
|
|
// onReject,
|
|
// actionLoadingId,
|
|
// }) {
|
|
// const p = r._prop;
|
|
// const imgs = pImgs(p);
|
|
// const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null;
|
|
// const addr = pAddr(p);
|
|
// const isPending = Number(r.status) === 0;
|
|
// const isActionLoading = actionLoadingId === r.id;
|
|
|
|
// return (
|
|
// <motion.div
|
|
// initial={{ opacity: 0, y: 20 }}
|
|
// animate={{ opacity: 1, y: 0 }}
|
|
// className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden"
|
|
// >
|
|
// <div className="p-5">
|
|
// {img && (
|
|
// <div className="mb-4 w-full h-40 rounded-xl overflow-hidden">
|
|
// <img src={img} alt="" className="w-full h-full object-cover" />
|
|
// </div>
|
|
// )}
|
|
|
|
// <div className="flex justify-between items-start mb-3">
|
|
// <div>
|
|
// <StatusBadge code={r.status} />
|
|
|
|
// {addr && (
|
|
// <div className="flex items-center gap-1 text-gray-500 text-sm mt-1">
|
|
// <MapPin className="w-4 h-4" />
|
|
// {addr}
|
|
// </div>
|
|
// )}
|
|
// </div>
|
|
|
|
// <div className="text-left">
|
|
// <div className="text-lg font-bold text-amber-600">
|
|
// {r.totalPrice?.toLocaleString() ?? '—'}
|
|
// </div>
|
|
// <div className="text-xs text-gray-500">السعر الإجمالي</div>
|
|
// </div>
|
|
// </div>
|
|
|
|
// {(pBeds(p) || pBaths(p)) && (
|
|
// <div className="flex gap-3 mb-3 text-sm text-gray-600">
|
|
// {pBeds(p) > 0 && <span>{pBeds(p)} غرف</span>}
|
|
// {pBaths(p) > 0 && <span>{pBaths(p)} حمامات</span>}
|
|
// </div>
|
|
// )}
|
|
|
|
// <div className="grid grid-cols-2 gap-3 mb-4 text-center">
|
|
// <div className="bg-gray-50 p-2 rounded-lg">
|
|
// <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
|
// <div className="text-xs text-gray-500">من</div>
|
|
// <div className="text-sm font-medium">
|
|
// {r.startDate
|
|
// ? new Date(r.startDate).toLocaleDateString('ar')
|
|
// : '—'}
|
|
// </div>
|
|
// </div>
|
|
|
|
// <div className="bg-gray-50 p-2 rounded-lg">
|
|
// <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
|
// <div className="text-xs text-gray-500">إلى</div>
|
|
// <div className="text-sm font-medium">
|
|
// {r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
|
|
// </div>
|
|
// </div>
|
|
// </div>
|
|
|
|
// <div
|
|
// className={`flex gap-3 pt-3 border-t border-gray-100 ${
|
|
// !isPending ? 'justify-center' : ''
|
|
// }`}
|
|
// >
|
|
// <button
|
|
// type="button"
|
|
// onClick={() => onViewDetails(r)}
|
|
// className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
|
|
// >
|
|
// <Eye className="w-4 h-4" />
|
|
// التفاصيل
|
|
// </button>
|
|
|
|
// {isPending && (
|
|
// <>
|
|
// <button
|
|
// type="button"
|
|
// disabled={isActionLoading}
|
|
// onClick={() => onConfirm(r)}
|
|
// className="flex-1 bg-green-500 text-white py-2 rounded-xl text-sm font-medium hover:bg-green-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
// >
|
|
// {isActionLoading ? (
|
|
// <Loader2 className="w-4 h-4 animate-spin" />
|
|
// ) : (
|
|
// <CheckCircle className="w-4 h-4" />
|
|
// )}
|
|
// قبول
|
|
// </button>
|
|
|
|
// <button
|
|
// type="button"
|
|
// disabled={isActionLoading}
|
|
// onClick={() => onReject(r)}
|
|
// className="flex-1 bg-red-500 text-white py-2 rounded-xl text-sm font-medium hover:bg-red-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
// >
|
|
// {isActionLoading ? (
|
|
// <Loader2 className="w-4 h-4 animate-spin" />
|
|
// ) : (
|
|
// <XCircle className="w-4 h-4" />
|
|
// )}
|
|
// رفض
|
|
// </button>
|
|
// </>
|
|
// )}
|
|
// </div>
|
|
// </div>
|
|
// </motion.div>
|
|
// );
|
|
// }
|
|
|
|
// function DetailsModal({ r, isOpen, onClose }) {
|
|
// if (!isOpen || !r) return null;
|
|
|
|
// const p = r._prop;
|
|
|
|
// 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-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
|
|
// onClick={(e) => e.stopPropagation()}
|
|
// >
|
|
// <div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
|
|
// <div className="flex justify-between items-center">
|
|
// <h2 className="text-xl font-bold">طلب حجز #{r.id}</h2>
|
|
|
|
// <button
|
|
// type="button"
|
|
// onClick={onClose}
|
|
// className="p-1 hover:bg-white/20 rounded-full"
|
|
// >
|
|
// <XCircle className="w-6 h-6" />
|
|
// </button>
|
|
// </div>
|
|
// </div>
|
|
|
|
// <div className="p-6 space-y-6">
|
|
// {p && (
|
|
// <div className="bg-gray-50 p-4 rounded-xl">
|
|
// <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
|
|
// <Home className="w-5 h-5 text-amber-500" />
|
|
// معلومات العقار
|
|
// </h3>
|
|
|
|
// <p>
|
|
// <span className="text-gray-500">العنوان:</span>{' '}
|
|
// {pAddr(p) || '—'}
|
|
// </p>
|
|
|
|
// {(pBeds(p) || pBaths(p)) && (
|
|
// <div className="flex gap-3 mt-2">
|
|
// {pBeds(p) > 0 && (
|
|
// <span className="text-sm bg-white px-2 py-1 rounded-lg">
|
|
// {pBeds(p)} غرف
|
|
// </span>
|
|
// )}
|
|
|
|
// {pBaths(p) > 0 && (
|
|
// <span className="text-sm bg-white px-2 py-1 rounded-lg">
|
|
// {pBaths(p)} حمامات
|
|
// </span>
|
|
// )}
|
|
// </div>
|
|
// )}
|
|
// </div>
|
|
// )}
|
|
|
|
// <div className="bg-gray-50 p-4 rounded-xl">
|
|
// <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
|
|
// <Calendar className="w-5 h-5 text-amber-500" />
|
|
// تفاصيل الحجز
|
|
// </h3>
|
|
|
|
// <div className="grid grid-cols-2 gap-4">
|
|
// <div>
|
|
// <p className="text-gray-500">تاريخ البداية</p>
|
|
// <p className="font-medium">
|
|
// {r.startDate
|
|
// ? new Date(r.startDate).toLocaleDateString('ar')
|
|
// : '—'}
|
|
// </p>
|
|
// </div>
|
|
|
|
// <div>
|
|
// <p className="text-gray-500">تاريخ النهاية</p>
|
|
// <p className="font-medium">
|
|
// {r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
|
|
// </p>
|
|
// </div>
|
|
|
|
// <div>
|
|
// <p className="text-gray-500">الحالة</p>
|
|
// <StatusBadge code={r.status} />
|
|
// </div>
|
|
|
|
// <div>
|
|
// <p className="text-gray-500">تاريخ الإنشاء</p>
|
|
// <p className="font-medium">
|
|
// {r.createdAt
|
|
// ? new Date(r.createdAt).toLocaleDateString('ar')
|
|
// : '—'}
|
|
// </p>
|
|
// </div>
|
|
// </div>
|
|
// </div>
|
|
|
|
// <div className="bg-amber-50 p-4 rounded-xl">
|
|
// <h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2">
|
|
// <DollarSign className="w-5 h-5" />
|
|
// المعلومات المالية
|
|
// </h3>
|
|
|
|
// <div className="flex justify-between font-bold">
|
|
// <span className="text-gray-900">الإجمالي</span>
|
|
// <span className="text-amber-600 text-lg">
|
|
// {r.totalPrice?.toLocaleString() ?? '—'}
|
|
// </span>
|
|
// </div>
|
|
// </div>
|
|
// </div>
|
|
// </motion.div>
|
|
// </motion.div>
|
|
// );
|
|
// }
|
|
|
|
// export default function OwnerReservationRequestsPage() {
|
|
// const router = useRouter();
|
|
|
|
// const [reservations, setReservations] = useState([]);
|
|
// const [filtered, setFiltered] = useState([]);
|
|
// const [loading, setLoading] = useState(true);
|
|
// const [selected, setSelected] = useState(null);
|
|
// const [filterStatus, setFilterStatus] = useState('all');
|
|
// const [searchTerm, setSearchTerm] = useState('');
|
|
// const [actionLoadingId, setActionLoadingId] = useState(null);
|
|
|
|
// const loadReservations = useCallback(async () => {
|
|
// try {
|
|
// setLoading(true);
|
|
|
|
// const token = AuthService.getToken();
|
|
|
|
// const res = await fetch(
|
|
// `${API_BASE}/Reservations/GetOwnerResevationRequests`,
|
|
// {
|
|
// method: 'GET',
|
|
// headers: {
|
|
// Authorization: `Bearer ${token}`,
|
|
// 'Content-Type': 'application/json',
|
|
// },
|
|
// }
|
|
// );
|
|
|
|
// if (!res.ok) {
|
|
// const errorText = await res.text();
|
|
// throw new Error(errorText || `HTTP ${res.status}`);
|
|
// }
|
|
|
|
// const json = await res.json();
|
|
|
|
// let list = json.data || json || [];
|
|
// if (!Array.isArray(list)) list = [];
|
|
|
|
// const enriched = await Promise.all(list.map(enrich));
|
|
|
|
// setReservations(enriched);
|
|
// setFiltered(enriched);
|
|
// } catch (err) {
|
|
// console.error(err);
|
|
// toast.error('فشل تحميل طلبات الحجز');
|
|
// setReservations([]);
|
|
// setFiltered([]);
|
|
// } finally {
|
|
// setLoading(false);
|
|
// }
|
|
// }, []);
|
|
|
|
// useEffect(() => {
|
|
// if (!AuthService.getUser() || !AuthService.isOwner()) {
|
|
// router.push('/auth/choose-role');
|
|
// return;
|
|
// }
|
|
|
|
// loadReservations();
|
|
// }, [router, loadReservations]);
|
|
|
|
// useEffect(() => {
|
|
// let result = reservations;
|
|
|
|
// if (filterStatus !== 'all') {
|
|
// result = result.filter((x) => getStatusKey(x.status) === filterStatus);
|
|
// }
|
|
|
|
// if (searchTerm.trim()) {
|
|
// const q = searchTerm.trim().toLowerCase();
|
|
|
|
// result = result.filter(
|
|
// (x) =>
|
|
// pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)
|
|
// );
|
|
// }
|
|
|
|
// setFiltered(result);
|
|
// }, [reservations, filterStatus, searchTerm]);
|
|
|
|
// const handleConfirm = async (r) => {
|
|
// try {
|
|
// setActionLoadingId(r.id);
|
|
|
|
// const res = await API(
|
|
// AuthService.getToken(),
|
|
// 'PUT',
|
|
// `/Reservations/OwnerConfirmReservation/owner-confirm/${r.id}`
|
|
// );
|
|
|
|
// if (!res.ok) {
|
|
// const errorText = await res.text();
|
|
// throw new Error(errorText || `HTTP ${res.status}`);
|
|
// }
|
|
|
|
// toast.success('تم قبول الحجز بنجاح');
|
|
// await loadReservations();
|
|
// } catch (err) {
|
|
// console.error(err);
|
|
// toast.error('فشل قبول الحجز');
|
|
// } finally {
|
|
// setActionLoadingId(null);
|
|
// }
|
|
// };
|
|
|
|
// const handleReject = async (r) => {
|
|
// if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return;
|
|
|
|
// try {
|
|
// setActionLoadingId(r.id);
|
|
|
|
// const res = await API(
|
|
// AuthService.getToken(),
|
|
// 'POST',
|
|
// `/Reservations/ChangeReservationStatus?id=${r.id}&newStatus=5`
|
|
// );
|
|
|
|
// if (!res.ok) {
|
|
// const errorText = await res.text();
|
|
// throw new Error(errorText || `HTTP ${res.status}`);
|
|
// }
|
|
|
|
// toast.success('تم رفض الحجز');
|
|
// await loadReservations();
|
|
// } catch (err) {
|
|
// console.error(err);
|
|
// toast.error('فشل رفض الحجز');
|
|
// } finally {
|
|
// setActionLoadingId(null);
|
|
// }
|
|
// };
|
|
|
|
// const allStatuses = [
|
|
// ...new Set(reservations.map((r) => getStatusKey(r.status)).filter(Boolean)),
|
|
// ];
|
|
|
|
// const counts = {
|
|
// all: reservations.length,
|
|
// ...Object.fromEntries(
|
|
// allStatuses.map((s) => [
|
|
// s,
|
|
// reservations.filter((r) => getStatusKey(r.status) === s).length,
|
|
// ])
|
|
// ),
|
|
// };
|
|
|
|
// if (loading) {
|
|
// return (
|
|
// <div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
// <Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
// </div>
|
|
// );
|
|
// }
|
|
|
|
// return (
|
|
// <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
|
// <Toaster position="top-center" reverseOrder={false} />
|
|
|
|
// <DetailsModal
|
|
// r={selected}
|
|
// isOpen={!!selected}
|
|
// onClose={() => setSelected(null)}
|
|
// />
|
|
|
|
// <div className="container mx-auto px-4">
|
|
// <motion.div
|
|
// initial={{ opacity: 0, y: -20 }}
|
|
// animate={{ opacity: 1, y: 0 }}
|
|
// className="mb-8"
|
|
// >
|
|
// <button
|
|
// type="button"
|
|
// onClick={() => router.back()}
|
|
// className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"
|
|
// >
|
|
// <ArrowLeft className="w-5 h-5" />
|
|
// الرجوع
|
|
// </button>
|
|
|
|
// <div className="flex items-center justify-between mb-2">
|
|
// <div>
|
|
// <h1 className="text-3xl font-bold text-gray-900">
|
|
// طلبات الحجز
|
|
// </h1>
|
|
// <p className="text-gray-600">لديك {reservations.length} طلب</p>
|
|
// </div>
|
|
|
|
// <button
|
|
// type="button"
|
|
// onClick={loadReservations}
|
|
// className="p-2 bg-white shadow rounded-xl hover:shadow-md transition-all"
|
|
// >
|
|
// <RefreshCw className="w-5 h-5 text-gray-600" />
|
|
// </button>
|
|
// </div>
|
|
// </motion.div>
|
|
|
|
// <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
// {Object.entries(counts).map(([s, c]) => (
|
|
// <motion.div
|
|
// key={s}
|
|
// initial={{ opacity: 0, y: 20 }}
|
|
// animate={{ opacity: 1, y: 0 }}
|
|
// className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${
|
|
// filterStatus === s
|
|
// ? 'border-amber-500 bg-amber-50'
|
|
// : 'border-gray-200'
|
|
// }`}
|
|
// onClick={() => setFilterStatus(s)}
|
|
// >
|
|
// <div className="text-2xl font-bold text-amber-600">{c}</div>
|
|
// <div className="text-sm text-gray-600">
|
|
// {s === 'all' ? 'الكل' : STATUS_UI[s]?.label || s}
|
|
// </div>
|
|
// </motion.div>
|
|
// ))}
|
|
// </div>
|
|
|
|
// <div className="mb-6 relative">
|
|
// <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
// <input
|
|
// type="text"
|
|
// placeholder="ابحث بعنوان العقار أو رقم الحجز..."
|
|
// value={searchTerm}
|
|
// onChange={(e) => setSearchTerm(e.target.value)}
|
|
// className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
// />
|
|
// </div>
|
|
|
|
// {filtered.length === 0 ? (
|
|
// <div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
|
// <Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4" />
|
|
// <h3 className="text-xl font-bold text-gray-900 mb-2">
|
|
// لا توجد طلبات
|
|
// </h3>
|
|
// <p className="text-gray-600">
|
|
// لم يتم استلام أي طلبات حجز حتى الآن
|
|
// </p>
|
|
// </div>
|
|
// ) : (
|
|
// <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
// {filtered.map((r) => (
|
|
// <OwnerCard
|
|
// key={r.id}
|
|
// r={r}
|
|
// onViewDetails={setSelected}
|
|
// onConfirm={handleConfirm}
|
|
// onReject={handleReject}
|
|
// actionLoadingId={actionLoadingId}
|
|
// />
|
|
// ))}
|
|
// </div>
|
|
// )}
|
|
// </div>
|
|
// </div>
|
|
// );
|
|
// }
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
Calendar,
|
|
Clock,
|
|
CheckCircle,
|
|
XCircle,
|
|
Eye,
|
|
Loader2,
|
|
Search,
|
|
MapPin,
|
|
DollarSign,
|
|
Home,
|
|
ArrowLeft,
|
|
RefreshCw,
|
|
Flag,
|
|
} from 'lucide-react';
|
|
import toast, { Toaster } from 'react-hot-toast';
|
|
import AuthService from '../../services/AuthService';
|
|
import { getRentProperty } from '../../utils/api';
|
|
|
|
const API_BASE =
|
|
process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
|
|
|
const STATUS_MAP = [
|
|
'pending',
|
|
'ownerConfirmed',
|
|
'depositPaid',
|
|
'depositConfirmed',
|
|
'completed',
|
|
'cancelled',
|
|
];
|
|
|
|
const STATUS_UI = {
|
|
pending: {
|
|
label: 'قيد الانتظار',
|
|
color: 'bg-yellow-100 text-yellow-800',
|
|
icon: Clock,
|
|
},
|
|
ownerConfirmed: {
|
|
label: 'مؤكد',
|
|
color: 'bg-green-100 text-green-800',
|
|
icon: CheckCircle,
|
|
},
|
|
depositPaid: {
|
|
label: 'تم دفع السلفة',
|
|
color: 'bg-indigo-100 text-indigo-800',
|
|
icon: DollarSign,
|
|
},
|
|
depositConfirmed: {
|
|
label: 'مؤكد نهائياً',
|
|
color: 'bg-green-100 text-green-800',
|
|
icon: CheckCircle,
|
|
},
|
|
completed: {
|
|
label: 'منتهي',
|
|
color: 'bg-blue-100 text-blue-800',
|
|
icon: CheckCircle,
|
|
},
|
|
cancelled: {
|
|
label: 'ملغي',
|
|
color: 'bg-gray-100 text-gray-800',
|
|
icon: XCircle,
|
|
},
|
|
};
|
|
|
|
const getStatusKey = (code) => STATUS_MAP[Number(code)] || 'pending';
|
|
|
|
const sLabel = (code) => STATUS_UI[getStatusKey(code)]?.label ?? String(code);
|
|
|
|
const sColor = (code) =>
|
|
STATUS_UI[getStatusKey(code)]?.color ?? 'bg-gray-100 text-gray-700';
|
|
|
|
const sIcon = (code) => STATUS_UI[getStatusKey(code)]?.icon ?? Clock;
|
|
|
|
function StatusBadge({ code }) {
|
|
const Icon = sIcon(code);
|
|
|
|
return (
|
|
<span
|
|
className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${sColor(
|
|
code
|
|
)}`}
|
|
>
|
|
<Icon className="w-3 h-3" /> {sLabel(code)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
async function enrich(r) {
|
|
if (!r?.propertyId) return r;
|
|
|
|
try {
|
|
const prop = await getRentProperty(r.propertyId);
|
|
|
|
return {
|
|
...r,
|
|
_prop: prop?.propertyInformation ?? prop ?? null,
|
|
};
|
|
} catch {
|
|
return {
|
|
...r,
|
|
_prop: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
const pAddr = (p) => p?.address ?? '';
|
|
const pImgs = (p) => (Array.isArray(p?.images) ? p.images : []);
|
|
const pBeds = (p) => p?.numberOfBedRooms ?? 0;
|
|
const pBaths = (p) => p?.numberOfBathRooms ?? 0;
|
|
|
|
const API = (token, method, path, body) => {
|
|
return fetch(`${API_BASE}${path}`, {
|
|
method: method || 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token && { Authorization: `Bearer ${token}` }),
|
|
},
|
|
...(body && { body: JSON.stringify(body) }),
|
|
});
|
|
};
|
|
|
|
function buildImageUrl(path) {
|
|
if (!path) return null;
|
|
|
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
return path;
|
|
}
|
|
|
|
return `${API_BASE}${path}`;
|
|
}
|
|
|
|
const getAuthToken = () => {
|
|
if (typeof window === 'undefined') return '';
|
|
return (
|
|
AuthService.getToken?.() ||
|
|
AuthService.getAccessToken?.() ||
|
|
localStorage.getItem('token') ||
|
|
localStorage.getItem('accessToken') ||
|
|
localStorage.getItem('authToken') ||
|
|
''
|
|
);
|
|
};
|
|
|
|
const readStoredUser = () => {
|
|
if (typeof window === 'undefined') return null;
|
|
const keys = ['user', 'currentUser', 'authUser', 'profile'];
|
|
for (const key of keys) {
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) continue;
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return raw;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const extractNumericUserId = (value) => {
|
|
if (!value) return null;
|
|
if (typeof value === 'number') return Number.isInteger(value) ? value : null;
|
|
if (typeof value === 'string') {
|
|
const n = Number(value);
|
|
return Number.isInteger(n) ? n : null;
|
|
}
|
|
if (typeof value === 'object') {
|
|
const candidates = [
|
|
value.id,
|
|
value.userId,
|
|
value.userID,
|
|
value.user?.id,
|
|
value.user?.userId,
|
|
value.profile?.id,
|
|
value.profile?.userId,
|
|
value.data?.id,
|
|
];
|
|
for (const candidate of candidates) {
|
|
const id = extractNumericUserId(candidate);
|
|
if (id !== null) return id;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
async function reportReservation(reservationId, message) {
|
|
const user = AuthService.getUser?.() ?? readStoredUser();
|
|
const reporter = extractNumericUserId(user);
|
|
const rid = Number(reservationId);
|
|
|
|
if (!Number.isInteger(rid)) {
|
|
throw new Error('رقم الحجز غير صالح');
|
|
}
|
|
if (!Number.isInteger(reporter)) {
|
|
throw new Error('تعذر تحديد المستخدم الحالي');
|
|
}
|
|
|
|
const token = getAuthToken();
|
|
const res = await fetch(`${API_BASE}/ReservationReports/ReportReservation`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
reservationId: rid,
|
|
message: message ?? null,
|
|
reporter,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let errorMessage = 'فشل إرسال البلاغ';
|
|
try {
|
|
const data = await res.json();
|
|
errorMessage = data?.message || data?.title || errorMessage;
|
|
} catch (_) {
|
|
try {
|
|
const text = await res.text();
|
|
if (text) errorMessage = text;
|
|
} catch (_) {}
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
try {
|
|
return await res.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function OwnerCard({
|
|
r,
|
|
onViewDetails,
|
|
onConfirm,
|
|
onReject,
|
|
onReport,
|
|
actionLoadingId,
|
|
reportingId,
|
|
}) {
|
|
const p = r._prop;
|
|
const imgs = pImgs(p);
|
|
const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null;
|
|
const addr = pAddr(p);
|
|
const isPending = Number(r.status) === 0;
|
|
const isActionLoading = actionLoadingId === r.id;
|
|
const isReporting = reportingId === r.id;
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden"
|
|
>
|
|
<div className="p-5">
|
|
{img && (
|
|
<div className="mb-4 w-full h-40 rounded-xl overflow-hidden">
|
|
<img src={img} alt="" className="w-full h-full object-cover" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-between items-start mb-3">
|
|
<div>
|
|
<StatusBadge code={r.status} />
|
|
|
|
{addr && (
|
|
<div className="flex items-center gap-1 text-gray-500 text-sm mt-1">
|
|
<MapPin className="w-4 h-4" />
|
|
{addr}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-left">
|
|
<div className="text-lg font-bold text-amber-600">
|
|
{r.totalPrice?.toLocaleString() ?? '—'}
|
|
</div>
|
|
<div className="text-xs text-gray-500">السعر الإجمالي</div>
|
|
</div>
|
|
</div>
|
|
|
|
{(pBeds(p) || pBaths(p)) && (
|
|
<div className="flex gap-3 mb-3 text-sm text-gray-600">
|
|
{pBeds(p) > 0 && <span>{pBeds(p)} غرف</span>}
|
|
{pBaths(p) > 0 && <span>{pBaths(p)} حمامات</span>}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-3 mb-4 text-center">
|
|
<div className="bg-gray-50 p-2 rounded-lg">
|
|
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
|
<div className="text-xs text-gray-500">من</div>
|
|
<div className="text-sm font-medium">
|
|
{r.startDate
|
|
? new Date(r.startDate).toLocaleDateString('ar')
|
|
: '—'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray-50 p-2 rounded-lg">
|
|
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" />
|
|
<div className="text-xs text-gray-500">إلى</div>
|
|
<div className="text-sm font-medium">
|
|
{r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`flex gap-3 pt-3 border-t border-gray-100 ${
|
|
!isPending ? 'justify-center' : ''
|
|
}`}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => onViewDetails(r)}
|
|
className="flex-1 bg-gray-100 text-gray-700 py-2 rounded-xl text-sm font-medium hover:bg-gray-200 transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
التفاصيل
|
|
</button>
|
|
|
|
{isPending && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={isActionLoading}
|
|
onClick={() => onConfirm(r)}
|
|
className="flex-1 bg-green-500 text-white py-2 rounded-xl text-sm font-medium hover:bg-green-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
>
|
|
{isActionLoading ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<CheckCircle className="w-4 h-4" />
|
|
)}
|
|
قبول
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
disabled={isActionLoading}
|
|
onClick={() => onReject(r)}
|
|
className="flex-1 bg-red-500 text-white py-2 rounded-xl text-sm font-medium hover:bg-red-600 transition-colors flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
>
|
|
{isActionLoading ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<XCircle className="w-4 h-4" />
|
|
)}
|
|
رفض
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
disabled={isReporting}
|
|
onClick={() => onReport(r)}
|
|
className={`w-full mt-3 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
|
|
isReporting
|
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
: 'bg-red-50 text-red-700 hover:bg-red-100'
|
|
}`}
|
|
>
|
|
{isReporting ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Flag className="w-4 h-4" />
|
|
)}
|
|
{isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
|
|
if (!isOpen || !r) return null;
|
|
|
|
const p = r._prop;
|
|
const isReporting = reportingId === r.id;
|
|
|
|
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-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-bold">طلب حجز #{r.id}</h2>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="p-1 hover:bg-white/20 rounded-full"
|
|
>
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-6">
|
|
{p && (
|
|
<div className="bg-gray-50 p-4 rounded-xl">
|
|
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
|
|
<Home className="w-5 h-5 text-amber-500" />
|
|
معلومات العقار
|
|
</h3>
|
|
|
|
<p>
|
|
<span className="text-gray-500">العنوان:</span>{' '}
|
|
{pAddr(p) || '—'}
|
|
</p>
|
|
|
|
{(pBeds(p) || pBaths(p)) && (
|
|
<div className="flex gap-3 mt-2">
|
|
{pBeds(p) > 0 && (
|
|
<span className="text-sm bg-white px-2 py-1 rounded-lg">
|
|
{pBeds(p)} غرف
|
|
</span>
|
|
)}
|
|
|
|
{pBaths(p) > 0 && (
|
|
<span className="text-sm bg-white px-2 py-1 rounded-lg">
|
|
{pBaths(p)} حمامات
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-gray-50 p-4 rounded-xl">
|
|
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
|
|
<Calendar className="w-5 h-5 text-amber-500" />
|
|
تفاصيل الحجز
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-gray-500">تاريخ البداية</p>
|
|
<p className="font-medium">
|
|
{r.startDate
|
|
? new Date(r.startDate).toLocaleDateString('ar')
|
|
: '—'}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-gray-500">تاريخ النهاية</p>
|
|
<p className="font-medium">
|
|
{r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-gray-500">الحالة</p>
|
|
<StatusBadge code={r.status} />
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-gray-500">تاريخ الإنشاء</p>
|
|
<p className="font-medium">
|
|
{r.createdAt
|
|
? new Date(r.createdAt).toLocaleDateString('ar')
|
|
: '—'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-amber-50 p-4 rounded-xl">
|
|
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2">
|
|
<DollarSign className="w-5 h-5" />
|
|
المعلومات المالية
|
|
</h3>
|
|
|
|
<div className="flex justify-between font-bold">
|
|
<span className="text-gray-900">الإجمالي</span>
|
|
<span className="text-amber-600 text-lg">
|
|
{r.totalPrice?.toLocaleString() ?? '—'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function ReportDialog({ isOpen, reservation, onClose, onSubmit, submitting }) {
|
|
const [message, setMessage] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (isOpen) setMessage('');
|
|
}, [isOpen, reservation?.id]);
|
|
|
|
if (!isOpen || !reservation) return null;
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
|
onClick={onClose}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.95, y: 16 }}
|
|
animate={{ scale: 1, y: 0 }}
|
|
exit={{ scale: 0.95, y: 16 }}
|
|
className="w-full max-w-lg rounded-2xl bg-white shadow-2xl overflow-hidden"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="bg-gradient-to-r from-red-500 to-red-600 p-6 text-white">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold">الإبلاغ عن الحجز</h2>
|
|
<p className="text-red-100 text-sm mt-1">رقم الحجز: #{reservation.id}</p>
|
|
</div>
|
|
<button onClick={onClose} className="rounded-full p-1 hover:bg-white/20">
|
|
<XCircle className="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
<p className="text-gray-700 mb-4 leading-7">
|
|
اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
|
|
</p>
|
|
|
|
<textarea
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
placeholder="اكتب تفاصيل البلاغ هنا..."
|
|
rows={5}
|
|
className="w-full resize-none rounded-xl border border-gray-300 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
/>
|
|
|
|
<div className="mt-5 flex gap-3">
|
|
<button
|
|
onClick={() => onSubmit(message)}
|
|
disabled={submitting}
|
|
className={`flex-1 rounded-xl py-2.5 text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
|
|
submitting ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-red-600 text-white hover:bg-red-700'
|
|
}`}
|
|
>
|
|
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Flag className="h-4 w-4" />}
|
|
{submitting ? 'جاري الإرسال...' : 'إرسال البلاغ'}
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
disabled={submitting}
|
|
className="rounded-xl bg-gray-200 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-300 transition-colors disabled:cursor-not-allowed"
|
|
>
|
|
إلغاء
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export default function OwnerReservationRequestsPage() {
|
|
const router = useRouter();
|
|
|
|
const [reservations, setReservations] = useState([]);
|
|
const [filtered, setFiltered] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selected, setSelected] = useState(null);
|
|
const [filterStatus, setFilterStatus] = useState('all');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [actionLoadingId, setActionLoadingId] = useState(null);
|
|
const [reportDialog, setReportDialog] = useState({ open: false, reservation: null });
|
|
const [reportingId, setReportingId] = useState(null);
|
|
|
|
const loadReservations = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
const token = AuthService.getToken();
|
|
|
|
const res = await fetch(
|
|
`${API_BASE}/Reservations/GetOwnerResevationRequests`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text();
|
|
throw new Error(errorText || `HTTP ${res.status}`);
|
|
}
|
|
|
|
const json = await res.json();
|
|
|
|
let list = json.data || json || [];
|
|
if (!Array.isArray(list)) list = [];
|
|
|
|
const enriched = await Promise.all(list.map(enrich));
|
|
|
|
setReservations(enriched);
|
|
setFiltered(enriched);
|
|
} catch (err) {
|
|
console.error(err);
|
|
toast.error('فشل تحميل طلبات الحجز');
|
|
setReservations([]);
|
|
setFiltered([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!AuthService.getUser() || !AuthService.isOwner()) {
|
|
router.push('/auth/choose-role');
|
|
return;
|
|
}
|
|
|
|
loadReservations();
|
|
}, [router, loadReservations]);
|
|
|
|
useEffect(() => {
|
|
let result = reservations;
|
|
|
|
if (filterStatus !== 'all') {
|
|
result = result.filter((x) => getStatusKey(x.status) === filterStatus);
|
|
}
|
|
|
|
if (searchTerm.trim()) {
|
|
const q = searchTerm.trim().toLowerCase();
|
|
|
|
result = result.filter(
|
|
(x) =>
|
|
pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)
|
|
);
|
|
}
|
|
|
|
setFiltered(result);
|
|
}, [reservations, filterStatus, searchTerm]);
|
|
|
|
const handleConfirm = async (r) => {
|
|
try {
|
|
setActionLoadingId(r.id);
|
|
|
|
const res = await API(
|
|
AuthService.getToken(),
|
|
'PUT',
|
|
`/Reservations/OwnerConfirmReservation/owner-confirm/${r.id}`
|
|
);
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text();
|
|
throw new Error(errorText || `HTTP ${res.status}`);
|
|
}
|
|
|
|
toast.success('تم قبول الحجز بنجاح');
|
|
await loadReservations();
|
|
} catch (err) {
|
|
console.error(err);
|
|
toast.error('فشل قبول الحجز');
|
|
} finally {
|
|
setActionLoadingId(null);
|
|
}
|
|
};
|
|
|
|
const handleReject = async (r) => {
|
|
if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return;
|
|
|
|
try {
|
|
setActionLoadingId(r.id);
|
|
|
|
const res = await API(
|
|
AuthService.getToken(),
|
|
'POST',
|
|
`/Reservations/ChangeReservationStatus?id=${r.id}&newStatus=5`
|
|
);
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text();
|
|
throw new Error(errorText || `HTTP ${res.status}`);
|
|
}
|
|
|
|
toast.success('تم رفض الحجز');
|
|
await loadReservations();
|
|
} catch (err) {
|
|
console.error(err);
|
|
toast.error('فشل رفض الحجز');
|
|
} finally {
|
|
setActionLoadingId(null);
|
|
}
|
|
};
|
|
|
|
const openReportDialog = (r) => {
|
|
setReportDialog({ open: true, reservation: r });
|
|
};
|
|
|
|
const closeReportDialog = () => {
|
|
setReportDialog({ open: false, reservation: null });
|
|
};
|
|
|
|
const handleSubmitReport = async (message) => {
|
|
if (!reportDialog.reservation) return;
|
|
|
|
setReportingId(reportDialog.reservation.id);
|
|
try {
|
|
await reportReservation(reportDialog.reservation.id, message.trim() || null);
|
|
toast.success('تم إرسال البلاغ بنجاح');
|
|
closeReportDialog();
|
|
} catch (err) {
|
|
console.error(err);
|
|
toast.error(err?.message || 'فشل إرسال البلاغ');
|
|
} finally {
|
|
setReportingId(null);
|
|
}
|
|
};
|
|
|
|
const allStatuses = [
|
|
...new Set(reservations.map((r) => getStatusKey(r.status)).filter(Boolean)),
|
|
];
|
|
|
|
const counts = {
|
|
all: reservations.length,
|
|
...Object.fromEntries(
|
|
allStatuses.map((s) => [
|
|
s,
|
|
reservations.filter((r) => getStatusKey(r.status) === s).length,
|
|
])
|
|
),
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
<Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
|
<Toaster position="top-center" reverseOrder={false} />
|
|
|
|
<DetailsModal
|
|
r={selected}
|
|
isOpen={!!selected}
|
|
onClose={() => setSelected(null)}
|
|
onReport={openReportDialog}
|
|
reportingId={reportingId}
|
|
/>
|
|
<ReportDialog
|
|
isOpen={reportDialog.open}
|
|
reservation={reportDialog.reservation}
|
|
onClose={closeReportDialog}
|
|
onSubmit={handleSubmitReport}
|
|
submitting={!!reportingId}
|
|
/>
|
|
|
|
<div className="container mx-auto px-4">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="mb-8"
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => router.back()}
|
|
className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"
|
|
>
|
|
<ArrowLeft className="w-5 h-5" />
|
|
الرجوع
|
|
</button>
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">
|
|
طلبات الحجز
|
|
</h1>
|
|
<p className="text-gray-600">لديك {reservations.length} طلب</p>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={loadReservations}
|
|
className="p-2 bg-white shadow rounded-xl hover:shadow-md transition-all"
|
|
>
|
|
<RefreshCw className="w-5 h-5 text-gray-600" />
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
{Object.entries(counts).map(([s, c]) => (
|
|
<motion.div
|
|
key={s}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${
|
|
filterStatus === s
|
|
? 'border-amber-500 bg-amber-50'
|
|
: 'border-gray-200'
|
|
}`}
|
|
onClick={() => setFilterStatus(s)}
|
|
>
|
|
<div className="text-2xl font-bold text-amber-600">{c}</div>
|
|
<div className="text-sm text-gray-600">
|
|
{s === 'all' ? 'الكل' : STATUS_UI[s]?.label || s}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mb-6 relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
<input
|
|
type="text"
|
|
placeholder="ابحث بعنوان العقار أو رقم الحجز..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
/>
|
|
</div>
|
|
|
|
{filtered.length === 0 ? (
|
|
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
|
<Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4" />
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
|
لا توجد طلبات
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
لم يتم استلام أي طلبات حجز حتى الآن
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{filtered.map((r) => (
|
|
<OwnerCard
|
|
key={r.id}
|
|
r={r}
|
|
onViewDetails={setSelected}
|
|
onConfirm={handleConfirm}
|
|
onReject={handleReject}
|
|
onReport={openReportDialog}
|
|
actionLoadingId={actionLoadingId}
|
|
reportingId={reportingId}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |