Compare commits
3 Commits
a9eb1cc684
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 01ac4f8d6c | |||
| f2724a5cd2 | |||
| bef133ad5b |
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,155 @@
|
||||
// 'use client';
|
||||
|
||||
// import { useEffect, useState, useCallback } from 'react';
|
||||
// import { useRouter } from 'next/navigation';
|
||||
// import { motion } from 'framer-motion';
|
||||
// import { CreditCard, Loader2, Home, Calendar, Check, X, Clock } from 'lucide-react';
|
||||
// import toast, { Toaster } from 'react-hot-toast';
|
||||
// import AuthService from '@/app/services/AuthService';
|
||||
// import { getUserReservations, payDeposit } from '@/app/utils/api';
|
||||
|
||||
// const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||
|
||||
// const STATUS_CONFIG = {
|
||||
// pending: { label: 'قيد الانتظار', color: 'bg-yellow-100 text-yellow-800 border-yellow-300', depositPaid: false },
|
||||
// ownerConfirmed: { label: 'مؤكد من المالك', color: 'bg-blue-100 text-blue-800 border-blue-300', depositPaid: false },
|
||||
// depositPaid: { label: 'تم دفع السلفة', color: 'bg-orange-100 text-orange-800 border-orange-300', depositPaid: true },
|
||||
// depositConfirmed: { label: 'تم تأكيد الدفع', color: 'bg-green-100 text-green-800 border-green-300', depositPaid: true },
|
||||
// completed: { label: 'منتهي', color: 'bg-teal-100 text-teal-800 border-teal-300', depositPaid: true },
|
||||
// cancelled: { label: 'ملغي', color: 'bg-red-100 text-red-800 border-red-300', depositPaid: false },
|
||||
// };
|
||||
|
||||
// export default function PaymentsPage() {
|
||||
// const router = useRouter();
|
||||
// const [reservations, setReservations] = useState([]);
|
||||
// const [loading, setLoading] = useState(true);
|
||||
// const [payingId, setPayingId] = useState(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// // Admin check removed
|
||||
// // if (AuthService.isAdmin()) {
|
||||
// // router.push('/');
|
||||
// // return;
|
||||
// // }
|
||||
// loadReservations();
|
||||
// }, [router]);
|
||||
|
||||
// const loadReservations = useCallback(async () => {
|
||||
// try {
|
||||
// const data = await getUserReservations();
|
||||
// setReservations(Array.isArray(data) ? data : []);
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// toast.error('فشل تحميل المدفوعات');
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// const handlePayDeposit = async (reservation) => {
|
||||
// setPayingId(reservation.id);
|
||||
// try {
|
||||
// await payDeposit({ reservationId: reservation.id });
|
||||
// toast.success('تم دفع السلفة بنجاح!');
|
||||
// loadReservations();
|
||||
// } catch (err) {
|
||||
// toast.error(err?.message || 'فشل عملية الدفع');
|
||||
// } finally {
|
||||
// setPayingId(null);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const formatCurrency = (v) => (v ?? 0).toLocaleString() + ' ل.س';
|
||||
|
||||
// if (loading) {
|
||||
// return (
|
||||
// <div className="min-h-screen bg-gray-50 flex items-center justify-center" dir="rtl">
|
||||
// <Loader2 className="w-12 h-12 text-amber-500 animate-spin" />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// const canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
|
||||
|
||||
// return (
|
||||
// <div className="min-h-screen bg-gray-50 py-8" dir="rtl">
|
||||
// <Toaster position="top-center" reverseOrder={false} />
|
||||
// <div className="container mx-auto px-4 max-w-4xl">
|
||||
// <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||
// <h1 className="text-3xl font-bold text-gray-900 mb-2">المدفوعات</h1>
|
||||
// <p className="text-gray-600">إدارة مدفوعات الحجوزات والدفعات المقدمة</p>
|
||||
// </motion.div>
|
||||
|
||||
// {reservations.length === 0 ? (
|
||||
// <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||
// className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||
// <CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
// <h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
|
||||
// <p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
||||
// </motion.div>
|
||||
// ) : (
|
||||
// <div className="space-y-4">
|
||||
// {reservations.map((r, i) => {
|
||||
// const statusKey = STATUS_MAP[r.status] || 'pending';
|
||||
// const cfg = STATUS_CONFIG[statusKey];
|
||||
// const amount = r.depositAmount || r.totalPrice || 0;
|
||||
|
||||
// return (
|
||||
// <motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||
// className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
||||
// <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
// <div className="flex items-start gap-3">
|
||||
// <div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
// <Home className="w-5 h-5 text-amber-600" />
|
||||
// </div>
|
||||
// <div>
|
||||
// <h3 className="font-bold text-gray-900">
|
||||
// {r.propertyAddress || r._prop?.address || `عقار #${r.propertyId || r.id}`}
|
||||
// </h3>
|
||||
// <p className="text-sm text-gray-500 mt-1">حجز #{r.id}</p>
|
||||
// <div className="flex items-center gap-3 mt-1 text-xs text-gray-400">
|
||||
// <span className="flex items-center gap-1"><Calendar className="w-3 h-3" /> {new Date(r.startDate).toLocaleDateString('ar')}</span>
|
||||
// <span className="flex items-center gap-1"><Clock className="w-3 h-3" /> {new Date(r.endDate).toLocaleDateString('ar')}</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className="text-right w-full md:w-auto">
|
||||
// <div className="text-xl font-bold text-amber-600">{formatCurrency(amount)}</div>
|
||||
// <span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium border ${cfg.color}`}>
|
||||
// {cfg.depositPaid ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
||||
// {cfg.label}
|
||||
// </span>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {canPay(r.status) && (
|
||||
// <div className="mt-4 pt-4 border-t border-gray-100 flex justify-end">
|
||||
// <button onClick={() => handlePayDeposit(r)} disabled={payingId === r.id}
|
||||
// className="flex items-center gap-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white px-6 py-2.5 rounded-xl text-sm font-medium transition">
|
||||
// {payingId === r.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
|
||||
// {payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
||||
// </button>
|
||||
// </div>
|
||||
// )}
|
||||
// </motion.div>
|
||||
// );
|
||||
// })}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
@ -6,7 +158,7 @@ import { motion } from 'framer-motion';
|
||||
import { CreditCard, Loader2, Home, Calendar, Check, X, Clock } from 'lucide-react';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import AuthService from '@/app/services/AuthService';
|
||||
import { getUserReservations, payDeposit } from '@/app/utils/api';
|
||||
import { payDeposit } from '@/app/utils/api';
|
||||
|
||||
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||
|
||||
@ -25,19 +177,58 @@ export default function PaymentsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [payingId, setPayingId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Admin check removed
|
||||
// if (AuthService.isAdmin()) {
|
||||
// router.push('/');
|
||||
// return;
|
||||
// }
|
||||
loadReservations();
|
||||
}, [router]);
|
||||
const getAuthToken = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return (
|
||||
AuthService?.getToken?.() ||
|
||||
AuthService?.getAccessToken?.() ||
|
||||
localStorage.getItem('token') ||
|
||||
localStorage.getItem('accessToken') ||
|
||||
localStorage.getItem('authToken') ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
const loadReservations = useCallback(async () => {
|
||||
try {
|
||||
const data = await getUserReservations();
|
||||
setReservations(Array.isArray(data) ? data : []);
|
||||
const token = getAuthToken();
|
||||
|
||||
const res = await fetch('http://45.93.137.91/api/Customer/GetMyTransaction', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('فشل تحميل المدفوعات');
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
|
||||
|
||||
const mapped = items.map((item) => {
|
||||
const deposit = item?.diposit || item?.deposit || {};
|
||||
const reservation = deposit?.reservation || {};
|
||||
const transaction = deposit?.transaction || {};
|
||||
const currency = item?.currency || {};
|
||||
|
||||
return {
|
||||
id: reservation.id ?? deposit.reservationId ?? deposit?.reservation?.id ?? item?.reservationId ?? deposit?.id,
|
||||
reservationId: reservation.id ?? deposit.reservationId ?? item?.reservationId ?? deposit?.id,
|
||||
status: reservation.status ?? 0,
|
||||
startDate: reservation.startDate,
|
||||
endDate: reservation.endDate,
|
||||
totalPrice: reservation.totalPrice ?? transaction.amount ?? 0,
|
||||
depositAmount: transaction.amount ?? reservation.totalPrice ?? 0,
|
||||
currencySign: currency.sign || 'ل.س',
|
||||
currencyName: currency.name || '',
|
||||
currencyRate: currency.rate,
|
||||
_deposit: deposit,
|
||||
};
|
||||
});
|
||||
|
||||
setReservations(mapped);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('فشل تحميل المدفوعات');
|
||||
@ -46,6 +237,15 @@ export default function PaymentsPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Admin check removed
|
||||
// if (AuthService.isAdmin()) {
|
||||
// router.push('/');
|
||||
// return;
|
||||
// }
|
||||
loadReservations();
|
||||
}, [router, loadReservations]);
|
||||
|
||||
const handlePayDeposit = async (reservation) => {
|
||||
setPayingId(reservation.id);
|
||||
try {
|
||||
@ -59,7 +259,14 @@ export default function PaymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (v) => (v ?? 0).toLocaleString() + ' ل.س';
|
||||
const formatCurrency = (v, sign = 'ل.س') => `${sign} ${Number(v ?? 0).toLocaleString()}`;
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleDateString('en-GB');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -81,8 +288,11 @@ export default function PaymentsPage() {
|
||||
</motion.div>
|
||||
|
||||
{reservations.length === 0 ? (
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300"
|
||||
>
|
||||
<CreditCard className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
|
||||
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
||||
@ -95,42 +305,73 @@ export default function PaymentsPage() {
|
||||
const amount = r.depositAmount || r.totalPrice || 0;
|
||||
|
||||
return (
|
||||
<motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Home className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900">
|
||||
{r.propertyAddress || r._prop?.address || `عقار #${r.propertyId || r.id}`}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">حجز #{r.id}</p>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs text-gray-400">
|
||||
<span className="flex items-center gap-1"><Calendar className="w-3 h-3" /> {new Date(r.startDate).toLocaleDateString('ar')}</span>
|
||||
<span className="flex items-center gap-1"><Clock className="w-3 h-3" /> {new Date(r.endDate).toLocaleDateString('ar')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right w-full md:w-auto">
|
||||
<div className="text-xl font-bold text-amber-600">{formatCurrency(amount)}</div>
|
||||
<motion.div
|
||||
key={r.id || i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium border ${cfg.color}`}>
|
||||
{cfg.depositPaid ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
||||
{cfg.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Home className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">Reservation</div>
|
||||
<div className="font-bold text-gray-900">#{r.reservationId || r.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-gray-400 mb-1">{r.currencyName}</div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-gray-900">
|
||||
{formatCurrency(amount, r.currencySign)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 pt-4">
|
||||
<div className="flex items-center justify-between text-sm text-gray-700">
|
||||
<span className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-gray-400" />
|
||||
الفترة
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(r.startDate)} - {formatDate(r.endDate)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-700 mt-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-gray-400" />
|
||||
Reservation
|
||||
</span>
|
||||
<span className="font-medium">#{r.reservationId || r.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canPay(r.status) && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex justify-end">
|
||||
<button onClick={() => handlePayDeposit(r)} disabled={payingId === r.id}
|
||||
className="flex items-center gap-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white px-6 py-2.5 rounded-xl text-sm font-medium transition">
|
||||
<div className="pt-2 flex justify-end">
|
||||
<button
|
||||
onClick={() => handlePayDeposit(r)}
|
||||
disabled={payingId === r.id}
|
||||
className="flex items-center gap-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-300 text-white px-6 py-2.5 rounded-xl text-sm font-medium transition"
|
||||
>
|
||||
{payingId === r.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
|
||||
{payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
@ -69,23 +69,70 @@ import { useFavorites } from "@/app/contexts/FavoritesContext";
|
||||
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums";
|
||||
import PropertyRatingList from "@/app/components/ratings/PropertyRatingList";
|
||||
import { getPropertyAverageRating } from "../../utils/ratings";
|
||||
import dynamic from "next/dynamic";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
const MapContainer = dynamic(
|
||||
() => import("react-leaflet").then((m) => m.MapContainer),
|
||||
{ ssr: false },
|
||||
);
|
||||
const TileLayer = dynamic(
|
||||
() => import("react-leaflet").then((m) => m.TileLayer),
|
||||
{ ssr: false },
|
||||
);
|
||||
const Marker = dynamic(() => import("react-leaflet").then((m) => m.Marker), {
|
||||
ssr: false,
|
||||
});
|
||||
const Popup = dynamic(() => import("react-leaflet").then((m) => m.Popup), {
|
||||
ssr: false,
|
||||
});
|
||||
function PropertyDetailMap({ lat, lng, title }) {
|
||||
const mapRef = useRef(null);
|
||||
const mapInstanceRef = useRef(null);
|
||||
const markerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapRef.current || mapInstanceRef.current) return;
|
||||
|
||||
if (mapRef.current._leaflet_id && !mapInstanceRef.current) {
|
||||
delete mapRef.current._leaflet_id;
|
||||
}
|
||||
|
||||
const L = require("leaflet");
|
||||
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
||||
iconUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
||||
shadowUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
||||
});
|
||||
|
||||
const map = L.map(mapRef.current, {
|
||||
center: [lat, lng],
|
||||
zoom: 14,
|
||||
scrollWheelZoom: false,
|
||||
});
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
const marker = L.marker([lat, lng]).addTo(map).bindPopup(title);
|
||||
mapInstanceRef.current = map;
|
||||
markerRef.current = marker;
|
||||
map.invalidateSize();
|
||||
|
||||
return () => {
|
||||
markerRef.current?.remove();
|
||||
markerRef.current = null;
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.remove();
|
||||
mapInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [lat, lng, title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.setView([lat, lng], 14);
|
||||
markerRef.current?.setLatLng([lat, lng]);
|
||||
markerRef.current?.setPopupContent(title);
|
||||
mapInstanceRef.current.invalidateSize();
|
||||
}
|
||||
}, [lat, lng, title]);
|
||||
|
||||
return <div ref={mapRef} className="h-full w-full" />;
|
||||
}
|
||||
|
||||
function formatCurrency(amount) {
|
||||
if (!amount || isNaN(amount)) return "0";
|
||||
@ -1243,19 +1290,11 @@ export default function PropertyDetailsPage() {
|
||||
className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-200"
|
||||
>
|
||||
<div className="h-64">
|
||||
<MapContainer
|
||||
center={[property.location.lat, property.location.lng]}
|
||||
zoom={14}
|
||||
className="h-full w-full"
|
||||
scrollWheelZoom={false}
|
||||
>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<Marker
|
||||
position={[property.location.lat, property.location.lng]}
|
||||
>
|
||||
<Popup>{property.title}</Popup>
|
||||
</Marker>
|
||||
</MapContainer>
|
||||
<PropertyDetailMap
|
||||
lat={property.location.lat}
|
||||
lng={property.location.lng}
|
||||
title={property.title}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3 bg-amber-50 text-center text-sm text-amber-700 flex items-center justify-center gap-2">
|
||||
<Info className="w-4 h-4" />
|
||||
|
||||
@ -1,3 +1,398 @@
|
||||
// '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, CreditCard, Timer, Star,
|
||||
// } from 'lucide-react';
|
||||
// import toast, { Toaster } from 'react-hot-toast';
|
||||
// import AuthService from '../services/AuthService';
|
||||
// import { getRentProperties, getUserReservations, payDeposit } from '../utils/api';
|
||||
// import { addPropertyRating } from '../utils/ratings';
|
||||
|
||||
// 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>
|
||||
// );
|
||||
// }
|
||||
|
||||
// const propAddr = (p, r) => p?.address ?? r?.propertyAddress ?? '';
|
||||
// const propImages = (p, r) => {
|
||||
// if (p?.images && Array.isArray(p.images)) return p.images;
|
||||
// if (r?.property?.images && Array.isArray(r.property.images)) return r.property.images;
|
||||
// return [];
|
||||
// };
|
||||
// const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
|
||||
// const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
|
||||
|
||||
// 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 formatWindowDuration(str) {
|
||||
// if (!str) return '';
|
||||
// const clean = str.replace(/-/g, '');
|
||||
// const dotIdx = clean.indexOf('.');
|
||||
// let totalHours = 0, timePart = clean;
|
||||
// if (dotIdx !== -1) {
|
||||
// const days = parseInt(clean.substring(0, dotIdx), 10) || 0;
|
||||
// totalHours += days * 24;
|
||||
// timePart = clean.substring(dotIdx + 1);
|
||||
// }
|
||||
// const parts = timePart.split(':');
|
||||
// if (parts.length >= 2) {
|
||||
// totalHours += parseInt(parts[0], 10) || 0;
|
||||
// }
|
||||
// if (totalHours > 0) return `${String(totalHours).padStart(2, '0')}:00:00`;
|
||||
// return timePart.substring(0, 8);
|
||||
// }
|
||||
|
||||
// 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 imgs = propImages(p, r);
|
||||
// const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
||||
// const addr = propAddr(p, r);
|
||||
// const beds = propBeds(p, r);
|
||||
// const baths = propBaths(p, r);
|
||||
// const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||
// const canRate = STATUS_MAP[r.status] === 'depositPaid' || STATUS_MAP[r.status] === 'completed';
|
||||
// const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
|
||||
// const deadline = hasTimeWindow
|
||||
// ? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
|
||||
// : null;
|
||||
// const isExpired = deadline ? Date.now() > deadline : false;
|
||||
// const isPaying = payingId === r.id;
|
||||
// const [showRating, setShowRating] = useState(false);
|
||||
// const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
||||
// const [ratingComment, setRatingComment] = useState('');
|
||||
// const [submittingRating, setSubmittingRating] = useState(false);
|
||||
|
||||
// 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>
|
||||
// {isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-3 rounded-xl mb-3">
|
||||
// <div className="flex items-center justify-between mb-1">
|
||||
// <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="text-xs text-blue-600">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</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>
|
||||
// {isOwnerConfirmed && !isExpired && <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>
|
||||
// {canRate && !showRating && <button onClick={() => setShowRating(true)}
|
||||
// className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
|
||||
// <Star className="w-4 h-4"/> قيّم هذا العقار
|
||||
// </button>}
|
||||
// {canRate && showRating && <div className="mt-3 bg-amber-50 p-3 rounded-xl">
|
||||
// <div className="space-y-2 mb-3">
|
||||
// {[
|
||||
// { key: 'clean', label: 'النظافة' },
|
||||
// { key: 'services', label: 'الخدمات' },
|
||||
// { key: 'ownerBehavior', label: 'تعامل المالك' },
|
||||
// { key: 'experience', label: 'التجربة العامة' },
|
||||
// ].map(cat => <div key={cat.key} className="flex items-center justify-between">
|
||||
// <span className="text-sm text-gray-700">{cat.label}</span>
|
||||
// <div className="flex gap-0.5">
|
||||
// {[1,2,3,4,5].map(n => (
|
||||
// <button key={n} onClick={() => setRatings(p => ({...p, [cat.key]: n}))}
|
||||
// className={`p-0.5 rounded-full transition-colors ${n <= ratings[cat.key] ? 'text-amber-500' : 'text-gray-300'}`}>
|
||||
// <Star className={`w-4 h-4 ${n <= ratings[cat.key] ? 'fill-amber-500' : ''}`} />
|
||||
// </button>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>)}
|
||||
// </div>
|
||||
// <textarea value={ratingComment} onChange={e => setRatingComment(e.target.value)}
|
||||
// placeholder="أكتب تعليقك (اختياري)"
|
||||
// className="w-full p-2 text-sm border border-amber-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-amber-500 mb-2" rows={2} />
|
||||
// <div className="flex gap-2">
|
||||
// <button onClick={async () => {
|
||||
// if (!ratings.clean || !ratings.services || !ratings.ownerBehavior || !ratings.experience) return toast.error('قيّم جميع الفئات');
|
||||
// setSubmittingRating(true);
|
||||
// try {
|
||||
// await addPropertyRating({ reservationId: r.id, cleanRating: ratings.clean, servicesRating: ratings.services, ownerBehaviorRating: ratings.ownerBehavior, experienceRating: ratings.experience, comment: ratingComment || null });
|
||||
// toast.success('تم إرسال التقييم');
|
||||
// setShowRating(false);
|
||||
// setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
||||
// setRatingComment('');
|
||||
// } catch (e) { toast.error(e?.message || 'فشل إرسال التقييم'); }
|
||||
// finally { setSubmittingRating(false); }
|
||||
// }} disabled={submittingRating}
|
||||
// className="flex-1 bg-amber-500 text-white py-1.5 rounded-lg text-sm font-medium hover:bg-amber-600 transition-colors disabled:bg-gray-300">
|
||||
// {submittingRating ? 'جاري الإرسال...' : 'إرسال التقييم'}
|
||||
// </button>
|
||||
// <button onClick={() => { setShowRating(false); setRatings({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 }); setRatingComment(''); }}
|
||||
// className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">إلغاء</button>
|
||||
// </div>
|
||||
// </div>}
|
||||
// </div>
|
||||
// </motion.div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
||||
// if (!isOpen || !r) return null;
|
||||
// const p = r._prop;
|
||||
// const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||
// const hasTimeWindow = r.ownerApprovalDate && p?.allowedPaymentPeriod;
|
||||
// const deadline = hasTimeWindow
|
||||
// ? new Date(r.ownerApprovalDate).getTime() + parseTimeSpan(p.allowedPaymentPeriod)
|
||||
// : null;
|
||||
// const isExpired = deadline ? Date.now() > deadline : false;
|
||||
// const isPaying = payingId === 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">تفاصيل الحجز</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, r)||'—'}</p>
|
||||
// {(propBeds(p, r)||propBaths(p, r)) && <div className="flex gap-3 mt-2">
|
||||
// {propBeds(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBeds(p, r)} غرف</span>}
|
||||
// {propBaths(p, r)>0&&<span className="text-sm bg-white px-2 py-1 rounded-lg">{propBaths(p, r)} حمامات</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>
|
||||
// {isOwnerConfirmed && hasTimeWindow && <div className="bg-blue-50 p-4 rounded-xl">
|
||||
// <div className="flex items-center justify-between mb-2">
|
||||
// <span className="text-blue-800 font-medium flex items-center gap-2"><Timer className="w-5 h-5"/> متبقي للدفع:</span>
|
||||
// <CountdownTimer deadline={deadline} />
|
||||
// </div>
|
||||
// <div className="text-xs text-blue-600 mb-3">مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}</div>
|
||||
// {!isExpired && <button onClick={() => { onPay(r); onClose(); }} disabled={isPaying}
|
||||
// className={`w-full py-2 rounded-xl 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-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||
// </button>}
|
||||
// </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('');
|
||||
// const [payingId, setPayingId] = useState(null);
|
||||
|
||||
// useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
||||
|
||||
// const loadReservations = useCallback(async () => {
|
||||
// try {
|
||||
// const [data, rentProps] = await Promise.all([
|
||||
// getUserReservations(),
|
||||
// getRentProperties().catch(() => []),
|
||||
// ]);
|
||||
// const list = Array.isArray(data) ? data : [];
|
||||
// const propsList = Array.isArray(rentProps) ? rentProps : [];
|
||||
// const propMap = {};
|
||||
// propsList.forEach(rp => {
|
||||
// const info = rp?.propertyInformation ?? {};
|
||||
// if (rp?.allowedPaymentPeriod) info.allowedPaymentPeriod = rp.allowedPaymentPeriod;
|
||||
// propMap[rp.propertyInformationId] = info;
|
||||
// propMap[rp.propertyInformation?.id] = info;
|
||||
// });
|
||||
// const enriched = list.map(r => {
|
||||
// if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId];
|
||||
// return r;
|
||||
// });
|
||||
// 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, x).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])) };
|
||||
|
||||
// const handlePay = async (r) => {
|
||||
// setPayingId(r.id);
|
||||
// try {
|
||||
// await payDeposit({
|
||||
// reservationId: r.id,
|
||||
// paymentTypeId: 1,
|
||||
// transactionType: 1,
|
||||
// comment: null,
|
||||
// });
|
||||
// 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>;
|
||||
|
||||
// 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)} onPay={handlePay} payingId={payingId} />
|
||||
// <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} onPay={handlePay} payingId={payingId} />)}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
@ -5,7 +400,7 @@ import { motion } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
|
||||
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star,
|
||||
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star, Flag,
|
||||
} from 'lucide-react';
|
||||
import toast, { Toaster } from 'react-hot-toast';
|
||||
import AuthService from '../services/AuthService';
|
||||
@ -47,6 +442,106 @@ const propImages = (p, r) => {
|
||||
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
|
||||
const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
|
||||
|
||||
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 parseTimeSpan(str) {
|
||||
if (!str) return 0;
|
||||
const clean = str.replace(/-/g, '');
|
||||
@ -99,7 +594,81 @@ function CountdownTimer({ deadline }) {
|
||||
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 }) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportingId }) {
|
||||
const p = r._prop;
|
||||
const imgs = propImages(p, r);
|
||||
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
||||
@ -114,6 +683,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
||||
: null;
|
||||
const isExpired = deadline ? Date.now() > deadline : false;
|
||||
const isPaying = payingId === r.id;
|
||||
const isReporting = reportingId === r.id;
|
||||
const [showRating, setShowRating] = useState(false);
|
||||
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
||||
const [ratingComment, setRatingComment] = useState('');
|
||||
@ -162,6 +732,10 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
||||
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||
</button>}
|
||||
</div>
|
||||
<button onClick={() => onReport(r)} disabled={isReporting}
|
||||
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>
|
||||
{canRate && !showRating && <button onClick={() => setShowRating(true)}
|
||||
className="w-full mt-3 bg-amber-50 text-amber-700 py-2 rounded-xl text-sm font-medium hover:bg-amber-100 transition-colors flex items-center justify-center gap-2">
|
||||
<Star className="w-4 h-4"/> قيّم هذا العقار
|
||||
@ -213,7 +787,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
||||
function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
|
||||
if (!isOpen || !r) return null;
|
||||
const p = r._prop;
|
||||
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||
@ -223,6 +797,7 @@ function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
||||
: null;
|
||||
const isExpired = deadline ? Date.now() > deadline : false;
|
||||
const isPaying = payingId === r.id;
|
||||
const isReporting = reportingId === r.id;
|
||||
|
||||
return (
|
||||
<motion.div initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}}
|
||||
@ -269,6 +844,10 @@ function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
||||
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||
</button>}
|
||||
</div>}
|
||||
<button onClick={() => { onReport(r); onClose(); }} disabled={isReporting}
|
||||
className={`w-full py-2 rounded-xl 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-5 h-5 animate-spin"/> : <Flag className="w-5 h-5"/>} {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
@ -284,6 +863,8 @@ export default function UserReservationsPage() {
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [payingId, setPayingId] = useState(null);
|
||||
const [reportDialog, setReportDialog] = useState({ open: false, reservation: null });
|
||||
const [reportingId, setReportingId] = useState(null);
|
||||
|
||||
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
||||
|
||||
@ -320,7 +901,10 @@ export default function UserReservationsPage() {
|
||||
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, x).toLowerCase().includes(q) || String(x.id).includes(q)); }
|
||||
if (searchTerm) {
|
||||
const q = searchTerm.toLowerCase();
|
||||
r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q));
|
||||
}
|
||||
setFiltered(r);
|
||||
}, [reservations, filterStatus, searchTerm]);
|
||||
|
||||
@ -345,12 +929,50 @@ export default function UserReservationsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
toast.error(err?.message || 'فشل إرسال البلاغ');
|
||||
} finally {
|
||||
setReportingId(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>;
|
||||
|
||||
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)} onPay={handlePay} payingId={payingId} />
|
||||
<DetailsModal
|
||||
r={selected}
|
||||
isOpen={!!selected}
|
||||
onClose={() => setSelected(null)}
|
||||
onPay={handlePay}
|
||||
onReport={openReportDialog}
|
||||
payingId={payingId}
|
||||
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 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>
|
||||
@ -380,7 +1002,17 @@ export default function UserReservationsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{filtered.map(r => <ReservationCard key={r.id} r={r} onViewDetails={setSelected} onPay={handlePay} payingId={payingId} />)}
|
||||
{filtered.map(r => (
|
||||
<ReservationCard
|
||||
key={r.id}
|
||||
r={r}
|
||||
onViewDetails={setSelected}
|
||||
onPay={handlePay}
|
||||
onReport={openReportDialog}
|
||||
payingId={payingId}
|
||||
reportingId={reportingId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user