Files
Claw AI 0891974440
All checks were successful
Build frontend / build (push) Successful in 1m21s
fix: properly enrich reservations with property data using status integer codes
2026-04-05 19:11:59 +00:00

227 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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,
} 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-blue-100 text-blue-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-green-100 text-green-800', icon: CheckCircle },
cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
};
function statusLabel(code) { return STATUS_UI[STATUS_MAP[code]]?.label ?? String(code); }
function statusColor(code) { return STATUS_UI[STATUS_MAP[code]]?.color ?? 'bg-gray-100 text-gray-700'; }
function statusIcon(code) { return STATUS_UI[STATUS_MAP[code]]?.icon ?? Clock; }
function StatusBadge({ code }) {
const Icon = statusIcon(code);
return (
<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" /> {statusLabel(code)}
</span>
);
}
async function enrich(reservation) {
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 (
<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>
{(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>}
<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">{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">{new Date(r.endDate).toLocaleDateString('ar')}</div>
</div>
</div>
<div className="flex gap-3 pt-3 border-t border-gray-100">
<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>
</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">تفاصيل الحجز</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full"><XCircle className="w-6 h-6"/></button>
</div>
<p className="text-amber-100 text-sm mt-1">رقم الحجز: #{r.id}</p>
</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> {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">
<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">{new Date(r.startDate).toLocaleDateString('ar')}</p></div>
<div><p className="text-gray-500">تاريخ النهاية</p><p className="font-medium">{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">{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 UserReservationsPage() {
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('');
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
const loadReservations = useCallback(async () => {
try {
const res = await fetch(`${API_BASE}/Reservations/GetUserResevations`, {
headers: { Authorization: `Bearer ${AuthService.getToken()}` },
});
if (!res.ok) throw new Error(`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([]);
}
setLoading(false);
}, []);
useEffect(() => {
let r = reservations;
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
if (searchTerm) { const q = searchTerm.toLowerCase(); r = r.filter(x => propAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)); }
setFiltered(r);
}, [reservations, filterStatus, searchTerm]);
const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[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 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>
<h1 className="text-3xl font-bold text-gray-900 mb-2">حجوزاتي</h1>
<p className="text-gray-600">لديك {reservations.length} حجز</p>
</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 => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} />)}
</div>
)}
</div>
</div>
);
}