fix: properly enrich reservations with property data using status integer codes
All checks were successful
Build frontend / build (push) Successful in 1m21s

This commit is contained in:
Claw AI
2026-04-05 19:11:59 +00:00
parent f925af0272
commit 0891974440
2 changed files with 225 additions and 509 deletions

View File

@ -4,21 +4,8 @@ import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
Calendar, Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
Clock, MapPin, DollarSign, Home, ArrowLeft, User, RefreshCw, Mail, Phone,
CheckCircle,
XCircle,
Eye,
Loader2,
Search,
MapPin,
DollarSign,
Home,
ArrowLeft,
User,
RefreshCw,
Mail,
Phone
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
@ -26,212 +13,133 @@ import { getRentProperty } from '../../utils/api';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
const statusConfig = { const STATUS_MAP = ['pending','ownerConfirmed','depositPaid','depositConfirmed','completed','cancelled'];
const STATUS_UI = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock }, pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
ownerConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle }, ownerConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign }, depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
depositConfirmed: { label: 'مؤكد نهائياً', color: 'bg-green-100 text-green-800', icon: CheckCircle }, depositConfirmed: { label: 'مؤكد نهائياً', color: 'bg-green-100 text-green-800', icon: CheckCircle },
rejected: { label: 'مرفوض', color: 'bg-red-100 text-red-800', icon: XCircle },
cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
completed: { label: 'منتهي', color: 'bg-blue-100 text-blue-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 },
}; };
function StatusBadge({ status }) { const sLabel = c => STATUS_UI[STATUS_MAP[c]]?.label ?? String(c);
const config = statusConfig[status] || { label: status, color: 'bg-gray-100 text-gray-700', icon: Clock }; const sColor = c => STATUS_UI[STATUS_MAP[c]]?.color ?? 'bg-gray-100 text-gray-700';
const Icon = config.icon; const sIcon = c => STATUS_UI[STATUS_MAP[c]]?.icon ?? Clock;
return (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${config.color}`}> function StatusBadge({ code }) {
<Icon className="w-3 h-3" /> const Icon = sIcon(code);
{config.label} 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>;
</span>
);
} }
function OwnerReservationCard({ reservation, onViewDetails, onConfirm, onReject }) { async function enrich(r) {
const property = reservation.property || reservation.propertyInformation; if (!r.propertyId) return r;
const user = reservation.user; try {
const isPending = reservation.status === 'pending'; const prop = await getRentProperty(r.propertyId);
r._prop = prop?.propertyInformation ?? prop ?? null;
} catch { /* skip */ }
return r;
}
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) => fetch(`${API_BASE}${path}`, {
method: method || 'GET',
headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) },
...(body && { body: JSON.stringify(body) }),
});
function OwnerCard({ r, onViewDetails, onConfirm, onReject }) {
const p = r._prop;
const imgs = pImgs(p);
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
const addr = pAddr(p);
const isPending = r.status === 0; // Pending
return ( return (
<motion.div <motion.div initial={{opacity:0,y:20}} animate={{opacity:1,y:0}}
initial={{ opacity: 0, y: 20 }} className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden">
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"> <div className="p-5">
<div className="flex justify-between items-start mb-4"> {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> <div>
<StatusBadge status={reservation.status} /> <StatusBadge code={r.status}/>
{property && ( {addr && <div className="flex items-center gap-1 text-gray-500 text-sm mt-1"><MapPin className="w-4 h-4"/>{addr}</div>}
<div className="flex items-center gap-1 text-gray-500 text-sm mt-2">
<MapPin className="w-4 h-4" />
{property.address || 'عقار'}
</div>
)}
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-lg font-bold text-amber-600"> <div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString()??'—'}</div>
{reservation.totalPrice?.toLocaleString() || '—'}
</div>
<div className="text-xs text-gray-500">السعر الإجمالي</div> <div className="text-xs text-gray-500">السعر الإجمالي</div>
</div> </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>}
{user && (
<div className="bg-gray-50 rounded-xl p-3 mb-4">
<div className="flex items-center gap-2">
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="font-medium text-gray-900">{user.name || user.email || 'مستخدم'}</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
{user.phoneNumber && <><Phone className="w-3 h-3" />{user.phoneNumber}</>}
{user.email && <><Mail className="w-3 h-3 mr-1" />{user.email}</>}
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-3 mb-4 text-center"> <div className="grid grid-cols-2 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg"> <div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" /> <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">من</div>
<div className="text-xs text-gray-500">من</div> <div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
<div className="text-sm font-medium">
{new Date(reservation.startDate).toLocaleDateString('ar')}
</div>
</div> </div>
<div className="bg-gray-50 p-2 rounded-lg"> <div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" /> <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">إلى</div>
<div className="text-xs text-gray-500">إلى</div> <div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
<div className="text-sm font-medium">
{new Date(reservation.endDate).toLocaleDateString('ar')}
</div> </div>
</div> </div>
</div> <div className={`flex gap-3 pt-3 border-t border-gray-100 ${!isPending?'justify-center':''}`}>
<button onClick={()=>onViewDetails(r)}
<div className={`flex gap-3 pt-3 border-t border-gray-100 ${!isPending ? '' : ''}`}> 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">
<button <Eye className="w-4 h-4"/> التفاصيل
onClick={() => onViewDetails(reservation)}
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> </button>
{isPending && <>
{isPending && ( <button 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">
<button <CheckCircle className="w-4 h-4"/> قبول
onClick={() => onConfirm(reservation)}
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"
>
<CheckCircle className="w-4 h-4" />
قبول
</button> </button>
<button <button onClick={()=>onReject(r)}
onClick={() => onReject(reservation)} 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">
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" <XCircle className="w-4 h-4"/> رفض
>
<XCircle className="w-4 h-4" />
رفض
</button> </button>
</> </>}
)}
</div> </div>
</div> </div>
</motion.div> </motion.div>
); );
} }
function DetailsModal({ reservation, isOpen, onClose }) { function DetailsModal({ r, isOpen, onClose }) {
if (!isOpen || !reservation) return null; if (!isOpen || !r) return null;
const property = reservation.property || reservation.propertyInformation; const p = r._prop;
const user = reservation.user;
return ( return (
<motion.div <motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
initial={{ opacity: 0 }} className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50" onClick={onClose}>
animate={{ opacity: 1 }} <motion.div initial={{scale:0.9,y:20}} animate={{scale:1,y:0}} exit={{scale:0.9,y:20}}
exit={{ opacity: 0 }} className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e=>e.stopPropagation()}>
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="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-xl font-bold">طلب حجز #{reservation.id}</h2> <h2 className="text-xl font-bold">طلب حجز #{r.id}</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"> <button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
<XCircle className="w-6 h-6" />
</button>
</div> </div>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{user && ( {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"> <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"> <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> تفاصيل الحجز</h3>
<User className="w-5 h-5 text-amber-500" /> معلومات المستأجر
</h3>
<p><span className="text-gray-500">الاسم:</span> {user.name || ''}</p>
<p><span className="text-gray-500">البريد:</span> {user.email || ''}</p>
{user.phoneNumber && <p><span className="text-gray-500">الهاتف:</span> {user.phoneNumber}</p>}
</div>
)}
{property && (
<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> {property.address || ''}</p>
{property.numberOfBedRooms && (
<div className="flex gap-3 mt-2">
<span className="text-sm bg-white px-2 py-1 rounded-lg">{property.numberOfBedRooms} غرف</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{property.numberOfBathRooms} حمامات</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 className="grid grid-cols-2 gap-4">
<div> <div><p className="text-gray-500">تاريخ البداية</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
<p className="text-gray-500">تاريخ البداية</p> <div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
<p className="font-medium">{new Date(reservation.startDate).toLocaleDateString('ar')}</p> <div><p className="text-gray-500">الحالة</p><StatusBadge code={r.status}/></div>
</div> <div><p className="text-gray-500">تاريخ الإنشاء</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
<div>
<p className="text-gray-500">تاريخ النهاية</p>
<p className="font-medium">{new Date(reservation.endDate).toLocaleDateString('ar')}</p>
</div>
<div>
<p className="text-gray-500">الحالة</p>
<StatusBadge status={reservation.status} />
</div>
<div>
<p className="text-gray-500">تاريخ الإنشاء</p>
<p className="font-medium">{new Date(reservation.createdAt).toLocaleDateString('ar')}</p>
</div> </div>
</div> </div>
</div>
<div className="bg-amber-50 p-4 rounded-xl"> <div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"> <h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/>المعلومات المالية</h3>
<DollarSign className="w-5 h-5" /> المعلومات المالية <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>
</h3>
<div className="flex justify-between font-bold">
<span className="text-gray-900">الإجمالي</span>
<span className="text-amber-600 text-lg">{reservation.totalPrice?.toLocaleString() || '—'}</span>
</div>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -249,165 +157,94 @@ export default function OwnerReservationRequestsPage() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
useEffect(() => { useEffect(() => {
const user = AuthService.getUser(); if (!AuthService.getUser() || !AuthService.isOwner()) { router.push('/auth/choose-role'); return; }
if (!user || !AuthService.isOwner()) {
router.push('/auth/choose-role');
return;
}
loadReservations(); loadReservations();
}, [router]); }, [router]);
const loadReservations = async () => { const loadReservations = useCallback(async () => {
try { try {
const token = AuthService.getToken(); const token = AuthService.getToken();
const res = await fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, { const res = await fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
let list = json.data || json || []; let list = json.data || json || [];
if (!Array.isArray(list)) list = []; if (!Array.isArray(list)) list = [];
const enriched = await Promise.all(list.map(enrich));
// Enrich each reservation with property details
const enriched = await Promise.all(
list.map(async (r) => {
if (!r.property && r.propertyId) {
try {
const propRes = await getRentProperty(r.propertyId);
r.property = propRes?.data?.propertyInformation || propRes?.data || propRes || null;
} catch (e) {
console.warn('Failed to load property', r.propertyId);
}
}
return r;
})
);
setReservations(enriched); setReservations(enriched);
setFiltered(enriched); setFiltered(enriched);
} catch (err) { } catch (err) {
console.error('Failed to load owner reservations:', err); console.error(err);
toast.error('فشل تحميل طلبات الحجز'); toast.error('فشل تحميل طلبات الحجز');
} }
setLoading(false); setLoading(false);
}; }, []);
useEffect(() => { useEffect(() => {
let result = reservations; let r = reservations;
if (filterStatus !== 'all') result = result.filter(r => r.status === filterStatus); if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
if (searchTerm) { if (searchTerm) {
const q = searchTerm.toLowerCase(); const q = searchTerm.toLowerCase();
result = result.filter(r => { r = r.filter(x => pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q));
const addr = (r.property?.address || '').toLowerCase();
const userName = ((r.user?.name) || (r.user?.email) || '').toLowerCase();
const sid = String(r.id).toLowerCase();
return addr.includes(q) || userName.includes(q) || sid.includes(q);
});
} }
setFiltered(result); setFiltered(r);
}, [reservations, filterStatus, searchTerm]); }, [reservations, filterStatus, searchTerm]);
const handleConfirm = async (reservation) => { const handleConfirm = async (r) => {
try { try {
const token = AuthService.getToken(); const res = await API(AuthService.getToken(), 'PUT', `/Reservations/owner-confirm/${r.id}`);
const id = typeof reservation.id === 'number' ? reservation.id : parseInt(reservation.id);
const res = await fetch(`${API_BASE}/Reservations/owner-confirm/${id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
toast.success('تم قبول الحجز بنجاح'); toast.success('تم قبول الحجز بنجاح');
await loadReservations(); await loadReservations();
} catch (err) { } catch (err) { console.error(err); toast.error('فشل قبول الحجز'); }
console.error('Failed to confirm:', err);
toast.error('فشل قبول الحجز');
}
}; };
const handleReject = async (reservation) => { const handleReject = async (r) => {
if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return; if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return;
try { try {
const token = AuthService.getToken(); const res = await API(AuthService.getToken(), 'PUT', `/Reservations/reject/${r.id}`);
const id = typeof reservation.id === 'number' ? reservation.id : parseInt(reservation.id);
const res = await fetch(`${API_BASE}/Reservations/reject/${id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
toast.success('تم رفض الحجز'); toast.success('تم رفض الحجز');
await loadReservations(); await loadReservations();
} catch (err) { } catch (err) { console.error(err); toast.error('فشل رفض الحجز'); }
console.error('Failed to reject:', err);
toast.error('فشل رفض الحجز');
}
}; };
const statuses = ['all', ...new Set(reservations.map(r => r.status))]; const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
const statusCounts = { const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
all: reservations.length,
...Object.fromEntries(
[...new Set(reservations.map(r => r.status))].map(s => [s, reservations.filter(r => 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>; 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 ( return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl"> <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<DetailsModal reservation={selected} isOpen={!!selected} onClose={() => setSelected(null)} /> <DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} />
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8"> <motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
<button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"> <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>
<ArrowLeft className="w-5 h-5" /> الرجوع
</button>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">طلبات الحجز</h1> <h1 className="text-3xl font-bold text-gray-900">طلبات الحجز</h1>
<p className="text-gray-600">لديك {reservations.length} طلب</p> <p className="text-gray-600">لديك {reservations.length} طلب</p>
</div> </div>
<button <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>
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> </div>
</motion.div> </motion.div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{Object.entries(statusCounts).map(([status, count]) => ( {Object.entries(counts).map(([s, c]) => (
<motion.div <motion.div key={s} initial={{opacity:0,y:20}} animate={{opacity:1,y:0}}
key={status} 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'}`}
initial={{ opacity: 0, y: 20 }} onClick={() => setFilterStatus(s)}>
animate={{ opacity: 1, y: 0 }} <div className="text-2xl font-bold text-amber-600">{c}</div>
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${ <div className="text-sm text-gray-600">{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}</div>
filterStatus === status ? 'border-amber-500 bg-amber-50' : 'border-gray-200'
}`}
onClick={() => setFilterStatus(status)}
>
<div className="text-2xl font-bold text-amber-600">{count}</div>
<div className="text-sm text-gray-600">{status === 'all' ? 'الكل' : (statusConfig[status]?.label || status)}</div>
</motion.div> </motion.div>
))} ))}
</div> </div>
<div className="mb-6 relative"> <div className="mb-6 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/>
<input <input type="text" placeholder="ابحث بعنوان العقار أو رقم الحجز..." value={searchTerm} onChange={e=>setSearchTerm(e.target.value)}
type="text" placeholder="ابحث بعنوان العقار أو المستأجر أو رقم الحجز..." 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"/>
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> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"> <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"/> <Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4"/>
@ -416,15 +253,7 @@ export default function OwnerReservationRequestsPage() {
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filtered.map((r) => ( {filtered.map(r => <OwnerCard key={r.id} r={r} onViewDetails={setSelected} onConfirm={handleConfirm} onReject={handleReject} />)}
<OwnerReservationCard
key={r.id}
reservation={r}
onViewDetails={setSelected}
onConfirm={handleConfirm}
onReject={handleReject}
/>
))}
</div> </div>
)} )}
</div> </div>

View File

@ -4,18 +4,8 @@ import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
Calendar, Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
Clock, MapPin, DollarSign, Home, ArrowLeft,
CheckCircle,
XCircle,
Eye,
Loader2,
Search,
MapPin,
DollarSign,
Home,
ArrowLeft,
Building2
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService'; import AuthService from '../services/AuthService';
@ -23,91 +13,82 @@ import { getRentProperty } from '../utils/api';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
const statusConfig = { const STATUS_MAP = ['pending','ownerConfirmed','depositPaid','depositConfirmed','completed','cancelled'];
const STATUS_UI = {
pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock }, pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800', icon: Clock },
ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800', icon: CheckCircle }, ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800', icon: CheckCircle },
depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign }, depositPaid: { label: 'تم دفع السلفة', color: 'bg-indigo-100 text-indigo-800', icon: DollarSign },
depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle }, depositConfirmed: { label: 'مؤكد', color: 'bg-green-100 text-green-800', icon: CheckCircle },
rejected: { label: 'مرفوض', color: 'bg-red-100 text-red-800', icon: XCircle },
cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
completed: { label: 'منتهي', color: 'bg-green-100 text-green-800', icon: CheckCircle }, completed: { label: 'منتهي', color: 'bg-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
}; };
function StatusBadge({ status }) { function statusLabel(code) { return STATUS_UI[STATUS_MAP[code]]?.label ?? String(code); }
const config = statusConfig[status] || { label: status, color: 'bg-gray-100 text-gray-700', icon: Clock }; function statusColor(code) { return STATUS_UI[STATUS_MAP[code]]?.color ?? 'bg-gray-100 text-gray-700'; }
const Icon = config.icon; function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
function StatusBadge({ code }) {
const Icon = statusIcon(code);
return ( return (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${config.color}`}> <span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium ${statusColor(code)}`}>
<Icon className="w-3 h-3" /> <Icon className="w-3 h-3" /> {statusLabel(code)}
{config.label}
</span> </span>
); );
} }
function ReservationCard({ reservation, onViewDetails }) { async function enrich(reservation) {
const property = reservation.property || reservation.propertyInformation; if (!reservation.propertyId) return reservation;
try {
const prop = await getRentProperty(reservation.propertyId);
reservation._prop = prop?.propertyInformation ?? prop ?? null;
} catch { /* skip */ }
return reservation;
}
const propAddr = (p) => p?.address ?? '';
const propImages = (p) => Array.isArray(p?.images) ? p.images : [];
const propBeds = (p) => p?.numberOfBedRooms ?? 0;
const propBaths = (p) => p?.numberOfBathRooms ?? 0;
function ReservationCard({ r, onViewDetails }) {
const p = r._prop;
const imgs = propImages(p);
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
const addr = propAddr(p);
const beds = propBeds(p);
const baths = propBaths(p);
return ( return (
<motion.div <motion.div initial={{ opacity:0,y:20 }} animate={{ opacity:1,y:0 }}
initial={{ opacity: 0, y: 20 }} className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-all border border-gray-200 overflow-hidden">
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"> <div className="p-5">
<div className="flex justify-between items-start mb-4"> {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> <div>
<div className="flex items-center gap-2 mb-2"> <StatusBadge code={r.status} />
<StatusBadge status={reservation.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>
{property && (
<div className="flex items-center gap-1 text-gray-500 text-sm">
<MapPin className="w-4 h-4" />
{property.address || 'عقار'}
</div>
)}
</div> </div>
<div className="text-left"> <div className="text-left">
<div className="text-lg font-bold text-amber-600"> <div className="text-lg font-bold text-amber-600">{r.totalPrice?.toLocaleString() ?? '—'}</div>
{reservation.totalPrice?.toLocaleString() || '—'}
</div>
<div className="text-xs text-gray-500">السعر الإجمالي</div> <div className="text-xs text-gray-500">السعر الإجمالي</div>
</div> </div>
</div> </div>
{(beds||baths) && <div className="flex gap-3 mb-3 text-sm text-gray-600">{beds>0&&<span>{beds} غرف</span>}{baths>0&&<span>{baths} حمامات</span>}</div>}
{property?.images?.length > 0 && (
<div className="mb-4 relative w-full h-40 rounded-xl overflow-hidden">
<img
src={`${API_BASE}${property.images[0]}`}
alt=""
className="w-full h-full object-cover"
/>
</div>
)}
<div className="grid grid-cols-2 gap-3 mb-4 text-center"> <div className="grid grid-cols-2 gap-3 mb-4 text-center">
<div className="bg-gray-50 p-2 rounded-lg"> <div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" /> <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">من</div>
<div className="text-xs text-gray-500">من</div> <div className="text-sm font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</div>
<div className="text-sm font-medium">
{new Date(reservation.startDate).toLocaleDateString('ar')}
</div>
</div> </div>
<div className="bg-gray-50 p-2 rounded-lg"> <div className="bg-gray-50 p-2 rounded-lg">
<Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1" /> <Calendar className="w-4 h-4 text-amber-500 mx-auto mb-1"/><div className="text-xs text-gray-500">إلى</div>
<div className="text-xs text-gray-500">إلى</div> <div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
<div className="text-sm font-medium">
{new Date(reservation.endDate).toLocaleDateString('ar')}
</div> </div>
</div> </div>
</div>
<div className="flex gap-3 pt-3 border-t border-gray-100"> <div className="flex gap-3 pt-3 border-t border-gray-100">
<button <button onClick={() => onViewDetails(r)}
onClick={() => onViewDetails(reservation)} 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">
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"/> التفاصيل
>
<Eye className="w-4 h-4" />
التفاصيل
</button> </button>
</div> </div>
</div> </div>
@ -115,83 +96,43 @@ function ReservationCard({ reservation, onViewDetails }) {
); );
} }
function DetailsModal({ reservation, isOpen, onClose }) { function DetailsModal({ r, isOpen, onClose }) {
if (!isOpen || !reservation) return null; if (!isOpen || !r) return null;
const property = reservation.property || reservation.propertyInformation; const p = r._prop;
return ( return (
<motion.div <motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
initial={{ opacity: 0 }} className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50" onClick={onClose}>
animate={{ opacity: 1 }} <motion.div initial={{scale:0.9,y:20}} animate={{scale:1,y:0}} exit={{scale:0.9,y:20}}
exit={{ opacity: 0 }} className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e=>e.stopPropagation()}>
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="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تفاصيل الحجز</h2> <h2 className="text-xl font-bold">تفاصيل الحجز</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"> <button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
<XCircle className="w-6 h-6" />
</button>
</div> </div>
<p className="text-amber-100 text-sm mt-1">رقم الحجز: #{reservation.id}</p> <p className="text-amber-100 text-sm mt-1">رقم الحجز: #{r.id}</p>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{property && ( {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> {propAddr(p)||''}</p>
{(propBeds(p)||propBaths(p)) && <div className="flex gap-3 mt-2">
{propBeds(p)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p)} غرف</span>}
{propBaths(p)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p)} حمامات</span>}
</div>}
</div>}
<div className="bg-gray-50 p-4 rounded-xl"> <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"> <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2"><Calendar className="w-5 h-5 text-amber-500"/> تفاصيل الحجز</h3>
<Home className="w-5 h-5 text-amber-500" /> معلومات العقار
</h3>
<p><span className="text-gray-500">العنوان:</span> {property.address || ''}</p>
{property.numberOfBedRooms && (
<div className="flex gap-3 mt-2">
<span className="text-sm bg-white px-2 py-1 rounded-lg">{property.numberOfBedRooms} غرف</span>
<span className="text-sm bg-white px-2 py-1 rounded-lg">{property.numberOfBathRooms} حمامات</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 className="grid grid-cols-2 gap-4">
<div> <div><p className="text-gray-500">تاريخ البداية</p><p className="font-medium">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
<p className="text-gray-500">تاريخ البداية</p> <div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</p></div>
<p className="font-medium">{new Date(reservation.startDate).toLocaleDateString('ar')}</p> <div><p className="text-gray-500">الحالة</p><StatusBadge code={r.status}/></div>
</div> <div><p className="text-gray-500">تاريخ الإنشاء</p><p className="font-medium">{new Date(r.createdAt).toLocaleDateString('ar')}</p></div>
<div>
<p className="text-gray-500">تاريخ النهاية</p>
<p className="font-medium">{new Date(reservation.endDate).toLocaleDateString('ar')}</p>
</div>
<div>
<p className="text-gray-500">الحالة</p>
<p><StatusBadge status={reservation.status} /></p>
</div>
<div>
<p className="text-gray-500">تاريخ الإنشاء</p>
<p className="font-medium">{new Date(reservation.createdAt).toLocaleDateString('ar')}</p>
</div> </div>
</div> </div>
</div>
<div className="bg-amber-50 p-4 rounded-xl"> <div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"> <h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
<DollarSign className="w-5 h-5" /> المعلومات المالية <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>
</h3>
<div className="flex justify-between pt-2 font-bold">
<span className="text-gray-900">الإجمالي</span>
<span className="text-amber-600 text-lg">{reservation.totalPrice?.toLocaleString() || '—'}</span>
</div>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -208,118 +149,66 @@ export default function UserReservationsPage() {
const [filterStatus, setFilterStatus] = useState('all'); const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
useEffect(() => { useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
const user = AuthService.getUser();
if (!user) {
router.push('/login');
return;
}
loadReservations();
}, [router]);
const loadReservations = async () => { const loadReservations = useCallback(async () => {
try { try {
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}/Reservations/GetUserResevations`, { const res = await fetch(`${API_BASE}/Reservations/GetUserResevations`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${AuthService.getToken()}` },
}); });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json(); const json = await res.json();
let list = json.data || json || []; let list = json.data || json || [];
if (!Array.isArray(list)) list = []; if (!Array.isArray(list)) list = [];
const enriched = await Promise.all(list.map(enrich));
// Enrich each reservation with property details
const enriched = await Promise.all(
list.map(async (r) => {
if (!r.property && r.propertyId) {
try {
const propRes = await getRentProperty(r.propertyId);
r.property = propRes?.data?.propertyInformation || propRes?.data || propRes || null;
} catch (e) {
console.warn('Failed to load property', r.propertyId);
}
}
return r;
})
);
setReservations(enriched); setReservations(enriched);
setFiltered(enriched); setFiltered(enriched);
} catch (err) { } catch (err) {
console.error('Failed to load reservations:', err); console.error(err);
toast.error('فشل تحميل الحجوزات'); toast.error('فشل تحميل الحجوزات');
setReservations([]); setReservations([]);
setFiltered([]); setFiltered([]);
} }
setLoading(false); setLoading(false);
}; }, []);
useEffect(() => { useEffect(() => {
let result = reservations; let r = reservations;
if (filterStatus !== 'all') result = result.filter(r => r.status === filterStatus); if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
if (searchTerm) { if (searchTerm) { const q = searchTerm.toLowerCase(); r = r.filter(x => propAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)); }
const q = searchTerm.toLowerCase(); setFiltered(r);
result = result.filter(r => {
const addr = (r.property?.address || '').toLowerCase();
const sid = String(r.id).toLowerCase();
return addr.includes(q) || sid.includes(q);
});
}
setFiltered(result);
}, [reservations, filterStatus, searchTerm]); }, [reservations, filterStatus, searchTerm]);
const statuses = ['all', ...new Set(reservations.map(r => r.status))]; const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
const statusCounts = { const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
all: reservations.length,
...Object.fromEntries(
[...new Set(reservations.map(r => r.status))].map(s => [s, reservations.filter(r => 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>; 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 ( return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl"> <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
<DetailsModal reservation={selected} isOpen={!!selected} onClose={() => setSelected(null)} /> <DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} />
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8"> <motion.div initial={{opacity:0,y:-20}} animate={{opacity:1,y:0}} className="mb-8">
<button onClick={() => router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"> <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>
<ArrowLeft className="w-5 h-5" /> الرجوع
</button>
<h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
<p className="text-gray-600">لديك {reservations.length} حجز</p> <p className="text-gray-600">لديك {reservations.length} حجز</p>
</motion.div> </motion.div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{Object.entries(statusCounts).map(([status, count]) => ( {Object.entries(counts).map(([s, c]) => (
<motion.div <motion.div key={s} initial={{opacity:0,y:20}} animate={{opacity:1,y:0}}
key={status} 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'}`}
initial={{ opacity: 0, y: 20 }} onClick={() => setFilterStatus(s)}>
animate={{ opacity: 1, y: 0 }} <div className="text-2xl font-bold text-amber-600">{c}</div>
className={`bg-white rounded-xl shadow-sm p-4 text-center border cursor-pointer hover:shadow-md transition-all ${ <div className="text-sm text-gray-600">{s==='all'?'الكل':(STATUS_UI[s]?.label||s)}</div>
filterStatus === status ? 'border-amber-500 bg-amber-50' : 'border-gray-200'
}`}
onClick={() => setFilterStatus(status)}
>
<div className="text-2xl font-bold text-amber-600">{count}</div>
<div className="text-sm text-gray-600">{status === 'all' ? 'الكل' : (statusConfig[status]?.label || status)}</div>
</motion.div> </motion.div>
))} ))}
</div> </div>
<div className="mb-6 relative"> <div className="mb-6 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"/>
<input <input type="text" placeholder="ابحث بعنوان العقار أو رقم الحجز..." value={searchTerm} onChange={e=>setSearchTerm(e.target.value)}
type="text" placeholder="ابحث بعنوان العقار أو رقم الحجز..." 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"/>
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> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"> <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"/> <Calendar className="w-12 h-12 text-amber-600 mx-auto mb-4"/>
@ -328,9 +217,7 @@ export default function UserReservationsPage() {
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{filtered.map((r) => ( {filtered.map(r => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} />)}
<ReservationCard key={r.id} reservation={r} onViewDetails={setSelected} />
))}
</div> </div>
)} )}
</div> </div>