added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 44s
All checks were successful
Build frontend / build (push) Successful in 44s
This commit is contained in:
@ -5,11 +5,11 @@ import { motion } from 'framer-motion';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
|
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
|
||||||
MapPin, DollarSign, Home, ArrowLeft,
|
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer,
|
||||||
} 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';
|
||||||
import { getRentProperty } from '../utils/api';
|
import { getRentProperty, getUserReservations, payDeposit } 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';
|
||||||
|
|
||||||
@ -51,13 +51,52 @@ const propImages = (p) => Array.isArray(p?.images) ? p.images : [];
|
|||||||
const propBeds = (p) => p?.numberOfBedRooms ?? 0;
|
const propBeds = (p) => p?.numberOfBedRooms ?? 0;
|
||||||
const propBaths = (p) => p?.numberOfBathRooms ?? 0;
|
const propBaths = (p) => p?.numberOfBathRooms ?? 0;
|
||||||
|
|
||||||
function ReservationCard({ r, onViewDetails }) {
|
function parseTimeSpan(str) {
|
||||||
|
if (!str) return 0;
|
||||||
|
const clean = str.replace(/-/g, '');
|
||||||
|
const dotIdx = clean.indexOf('.');
|
||||||
|
let days = 0, timePart = clean;
|
||||||
|
if (dotIdx !== -1) {
|
||||||
|
days = parseInt(clean.substring(0, dotIdx), 10) || 0;
|
||||||
|
timePart = clean.substring(dotIdx + 1);
|
||||||
|
}
|
||||||
|
const parts = timePart.split(':');
|
||||||
|
if (parts.length < 2) return days * 86400000;
|
||||||
|
const hh = parseInt(parts[0], 10) || 0;
|
||||||
|
const mm = parseInt(parts[1], 10) || 0;
|
||||||
|
const ss = parts.length > 2 ? (parseInt(parts[2], 10) || 0) : 0;
|
||||||
|
return ((days * 86400) + (hh * 3600) + (mm * 60) + ss) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CountdownTimer({ deadline }) {
|
||||||
|
const [remaining, setRemaining] = useState(deadline ? Math.max(0, deadline - Date.now()) : 0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!deadline) return;
|
||||||
|
const tick = () => setRemaining(Math.max(0, deadline - Date.now()));
|
||||||
|
tick();
|
||||||
|
const id = setInterval(tick, 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [deadline]);
|
||||||
|
if (remaining <= 0) return <span className="text-red-500 text-sm font-medium">انتهت المهلة</span>;
|
||||||
|
const h = Math.floor(remaining / 3600000);
|
||||||
|
const m = Math.floor((remaining % 3600000) / 60000);
|
||||||
|
const s = Math.floor((remaining % 60000) / 1000);
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return <span className="text-amber-600 text-sm font-mono font-bold" dir="ltr">{pad(h)}:{pad(m)}:{pad(s)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
||||||
const p = r._prop;
|
const p = r._prop;
|
||||||
const imgs = propImages(p);
|
const imgs = propImages(p);
|
||||||
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
||||||
const addr = propAddr(p);
|
const addr = propAddr(p);
|
||||||
const beds = propBeds(p);
|
const beds = propBeds(p);
|
||||||
const baths = propBaths(p);
|
const baths = propBaths(p);
|
||||||
|
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||||
|
const deadline = isOwnerConfirmed && r.ownerApprovalDate
|
||||||
|
? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(r.allowedPaymentPeriod)
|
||||||
|
: null;
|
||||||
|
const isPaying = payingId === r.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div initial={{ opacity:0,y:20 }} animate={{ opacity:1,y:0 }}
|
<motion.div initial={{ opacity:0,y:20 }} animate={{ opacity:1,y:0 }}
|
||||||
@ -85,20 +124,33 @@ function ReservationCard({ r, onViewDetails }) {
|
|||||||
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
|
<div className="text-sm font-medium">{new Date(r.endDate).toLocaleDateString('ar')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isOwnerConfirmed && <div className="flex items-center justify-between bg-blue-50 p-3 rounded-xl mb-3">
|
||||||
|
<span className="text-sm text-blue-800 font-medium flex items-center gap-1"><Timer className="w-4 h-4"/> متبقي للدفع:</span>
|
||||||
|
<CountdownTimer deadline={deadline} />
|
||||||
|
</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 onClick={() => onViewDetails(r)}
|
<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">
|
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>
|
||||||
|
{isOwnerConfirmed && <button onClick={() => onPay(r)} disabled={isPaying}
|
||||||
|
className={`flex-1 py-2 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
|
||||||
|
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||||
|
</button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailsModal({ r, isOpen, onClose }) {
|
function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
||||||
if (!isOpen || !r) return null;
|
if (!isOpen || !r) return null;
|
||||||
const p = r._prop;
|
const p = r._prop;
|
||||||
|
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||||
|
const deadline = isOwnerConfirmed && r.ownerApprovalDate
|
||||||
|
? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(r.allowedPaymentPeriod)
|
||||||
|
: null;
|
||||||
|
const isPaying = payingId === r.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
|
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
|
||||||
@ -134,6 +186,14 @@ function DetailsModal({ r, isOpen, onClose }) {
|
|||||||
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2"><DollarSign className="w-5 h-5"/> المعلومات المالية</h3>
|
<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 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>
|
||||||
|
{isOwnerConfirmed && <div className="flex items-center justify-between bg-blue-50 p-4 rounded-xl">
|
||||||
|
<span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
|
||||||
|
<CountdownTimer deadline={deadline} />
|
||||||
|
<button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
|
||||||
|
className={`px-6 py-2 rounded-xl font-medium transition-colors flex items-center gap-2 ${isPaying ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-amber-500 text-white hover:bg-amber-600'}`}>
|
||||||
|
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||||
|
</button>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -148,18 +208,14 @@ export default function UserReservationsPage() {
|
|||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
const [filterStatus, setFilterStatus] = useState('all');
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [payingId, setPayingId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
||||||
|
|
||||||
const loadReservations = useCallback(async () => {
|
const loadReservations = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/Reservations/GetUserResevations`, {
|
const data = await getUserReservations();
|
||||||
headers: { Authorization: `Bearer ${AuthService.getToken()}` },
|
const list = Array.isArray(data) ? data : [];
|
||||||
});
|
|
||||||
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));
|
const enriched = await Promise.all(list.map(enrich));
|
||||||
setReservations(enriched);
|
setReservations(enriched);
|
||||||
setFiltered(enriched);
|
setFiltered(enriched);
|
||||||
@ -182,12 +238,25 @@ export default function UserReservationsPage() {
|
|||||||
const allStatuses = [...new Set(reservations.map(r => STATUS_MAP[r.status]))];
|
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])) };
|
const counts = { all: reservations.length, ...Object.fromEntries(allStatuses.map(s => [s, reservations.filter(r => STATUS_MAP[r.status] === s).length])) };
|
||||||
|
|
||||||
|
const handlePay = async (r) => {
|
||||||
|
setPayingId(r.id);
|
||||||
|
try {
|
||||||
|
await payDeposit({ reservationId: r.id });
|
||||||
|
toast.success('تم دفع السلفة بنجاح!');
|
||||||
|
loadReservations();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err?.message || 'فشل عملية الدفع');
|
||||||
|
} finally {
|
||||||
|
setPayingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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 r={selected} isOpen={!!selected} onClose={() => setSelected(null)} />
|
<DetailsModal r={selected} isOpen={!!selected} onClose={() => setSelected(null)} onPay={handlePay} payingId={payingId} />
|
||||||
<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"><ArrowLeft className="w-5 h-5"/> الرجوع</button>
|
<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>
|
||||||
@ -217,7 +286,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 => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} />)}
|
{filtered.map(r => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} onPay={handlePay} payingId={payingId} />)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user