added a swicher on the properties
All checks were successful
Build frontend / build (push) Successful in 44s

This commit is contained in:
mouazkh
2026-05-26 20:54:25 +03:00
parent 82e39e6f90
commit e21dc53227

View File

@ -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>