Compare commits
10 Commits
71b1a71904
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 01ac4f8d6c | |||
| f2724a5cd2 | |||
| bef133ad5b | |||
| a9eb1cc684 | |||
| 13b563e35e | |||
| 5d593d593f | |||
| 51850b85c2 | |||
| 8cacf464d1 | |||
| 91de3d47b7 | |||
| 845ba2436a |
@ -126,6 +126,7 @@ export default function ClientLayout({ children }) {
|
|||||||
|
|
||||||
const isAuthPage = [
|
const isAuthPage = [
|
||||||
"/login",
|
"/login",
|
||||||
|
"/blocked",
|
||||||
"/register",
|
"/register",
|
||||||
"/forgot-password",
|
"/forgot-password",
|
||||||
"/auth/choose-role",
|
"/auth/choose-role",
|
||||||
|
|||||||
166
app/blocked/page.js
Normal file
166
app/blocked/page.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from 'lucide-react';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
|
import { sendGeneralReport } from '../utils/api';
|
||||||
|
|
||||||
|
export default function BlockedPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [form, setForm] = useState({ subject: '', body: '' });
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isSent, setIsSent] = useState(false);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
AuthService.deleteToken();
|
||||||
|
router.replace('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateField = (field, value) => {
|
||||||
|
setForm((current) => ({ ...current, [field]: value }));
|
||||||
|
if (isSent) setIsSent(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!form.subject.trim() || !form.body.trim()) {
|
||||||
|
toast.error('يرجى تعبئة الموضوع والرسالة');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendGeneralReport(form.subject.trim(), form.body.trim());
|
||||||
|
setIsSent(true);
|
||||||
|
setForm({ subject: '', body: '' });
|
||||||
|
toast.success('تم إرسال طلب الدعم بنجاح');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('حدث خطأ أثناء إرسال طلب الدعم. حاول مرة أخرى');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-red-50 via-white to-amber-50 flex items-center justify-center p-4" dir="rtl">
|
||||||
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="w-full max-w-5xl"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100"
|
||||||
|
>
|
||||||
|
<ShieldAlert className="w-12 h-12 text-red-600" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-3">الحساب محظور</h1>
|
||||||
|
<p className="text-gray-600 text-lg max-w-2xl mx-auto">
|
||||||
|
تم تقييد وصولك إلى التطبيق. يمكنك تسجيل الخروج أو مراسلة دعم العملاء للمساعدة في حل المشكلة.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -24 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8 flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
|
||||||
|
<LogOut className="w-7 h-7 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-3">تسجيل الخروج</h2>
|
||||||
|
<p className="text-gray-600 leading-7">
|
||||||
|
إنهاء الجلسة الحالية وإزالة بيانات الدخول من هذا الجهاز.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
تسجيل الخروج
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 24 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center">
|
||||||
|
<MessageSquare className="w-7 h-7 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">مراسلة دعم العملاء</h2>
|
||||||
|
<p className="text-gray-600 mt-1">أرسل تفاصيل المشكلة وسنقوم بمراجعتها.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-gray-700 mb-2">الموضوع</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.subject}
|
||||||
|
onChange={(event) => updateField('subject', event.target.value)}
|
||||||
|
placeholder="اكتب موضوع الرسالة"
|
||||||
|
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-gray-700 mb-2">الرسالة</label>
|
||||||
|
<textarea
|
||||||
|
value={form.body}
|
||||||
|
onChange={(event) => updateField('body', event.target.value)}
|
||||||
|
rows={6}
|
||||||
|
placeholder="اشرح المشكلة بالتفصيل"
|
||||||
|
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full bg-amber-500 hover:bg-amber-600 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
جاري الإرسال...
|
||||||
|
</>
|
||||||
|
) : isSent ? (
|
||||||
|
<>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
تم الإرسال
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
إرسال الرسالة
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
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 { CreditCard, Loader2, Home, Calendar, Check, X, Clock } from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
import AuthService from '@/app/services/AuthService';
|
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'];
|
const STATUS_MAP = ['pending', 'ownerConfirmed', 'depositPaid', 'depositConfirmed', 'completed', 'cancelled'];
|
||||||
|
|
||||||
@ -25,19 +177,58 @@ export default function PaymentsPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [payingId, setPayingId] = useState(null);
|
const [payingId, setPayingId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const getAuthToken = () => {
|
||||||
// Admin check removed
|
if (typeof window === 'undefined') return '';
|
||||||
// if (AuthService.isAdmin()) {
|
return (
|
||||||
// router.push('/');
|
AuthService?.getToken?.() ||
|
||||||
// return;
|
AuthService?.getAccessToken?.() ||
|
||||||
// }
|
localStorage.getItem('token') ||
|
||||||
loadReservations();
|
localStorage.getItem('accessToken') ||
|
||||||
}, [router]);
|
localStorage.getItem('authToken') ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const loadReservations = useCallback(async () => {
|
const loadReservations = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getUserReservations();
|
const token = getAuthToken();
|
||||||
setReservations(Array.isArray(data) ? data : []);
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('فشل تحميل المدفوعات');
|
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) => {
|
const handlePayDeposit = async (reservation) => {
|
||||||
setPayingId(reservation.id);
|
setPayingId(reservation.id);
|
||||||
try {
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -81,8 +288,11 @@ export default function PaymentsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{reservations.length === 0 ? (
|
{reservations.length === 0 ? (
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
className="bg-white rounded-2xl p-12 text-center border-2 border-dashed border-gray-300">
|
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" />
|
<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>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد معاملات مالية</h3>
|
||||||
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
<p className="text-gray-500">ستظهر هنا مدفوعاتك للحجوزات</p>
|
||||||
@ -95,42 +305,73 @@ export default function PaymentsPage() {
|
|||||||
const amount = r.depositAmount || r.totalPrice || 0;
|
const amount = r.depositAmount || r.totalPrice || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div key={r.id || i} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all">
|
key={r.id || i}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="flex items-start gap-3">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0">
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-5 hover:shadow-md transition-all"
|
||||||
<Home className="w-5 h-5 text-amber-600" />
|
>
|
||||||
</div>
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="font-bold text-gray-900">
|
<span className="text-sm font-medium text-gray-400">#{r.reservationId || r.id}</span>
|
||||||
{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}`}>
|
<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.depositPaid ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
||||||
{cfg.label}
|
{cfg.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{canPay(r.status) && (
|
{canPay(r.status) && (
|
||||||
<div className="mt-4 pt-4 border-t border-gray-100 flex justify-end">
|
<div className="pt-2 flex justify-end">
|
||||||
<button onClick={() => handlePayDeposit(r)} disabled={payingId === r.id}
|
<button
|
||||||
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">
|
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 ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
|
||||||
{payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
{payingId === r.id ? 'جاري الدفع...' : 'دفع السلفة'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -1,104 +1,216 @@
|
|||||||
|
// 'use client';
|
||||||
|
|
||||||
|
// import { motion } from 'framer-motion';
|
||||||
|
// import { Shield, Lock, Eye, Database, RefreshCw, Trash2, CheckCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
// const sections = [
|
||||||
|
// {
|
||||||
|
// title: 'المقدمة',
|
||||||
|
// icon: Shield,
|
||||||
|
// content:
|
||||||
|
// 'نحن في SweetHome نلتزم بحماية خصوصية مستخدمينا. توضح سياسة الخصوصية هذه كيفية جمع واستخدام وحماية المعلومات الشخصية التي تقدمها عند استخدام منصتنا. باستخدامك للمنصة، فإنك توافق على الممارسات الموضحة في هذه السياسة.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'المعلومات التي نجمعها',
|
||||||
|
// icon: Database,
|
||||||
|
// content:
|
||||||
|
// 'نجمع المعلومات التي تقدمها مباشرة عند إنشاء حساب، مثل الاسم، البريد الإلكتروني، رقم الهاتف، ومعلومات الدفع. كما نجمع معلومات حول استخدامك للمنصة، مثل العقارات التي تتصفحها، الحجوزات التي تقوم بها، وتقييماتك. قد نجمع أيضاً معلومات تقنية مثل عنوان IP ونوع المتصفح.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'كيف نستخدم معلوماتك',
|
||||||
|
// icon: Eye,
|
||||||
|
// content:
|
||||||
|
// 'نستخدم معلوماتك لتقديم وتحسين خدماتنا، ومعالجة الحجوزات والمدفوعات، والتواصل معك بشأن حساباتك وحجوزاتك، وإرسال التحديثات والعروض الترويجية (بموافقتك)، وتحسين تجربة المستخدم وتطوير ميزات جديدة، والامتثال للالتزامات القانونية.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'حماية البيانات وأمانها',
|
||||||
|
// icon: Lock,
|
||||||
|
// content:
|
||||||
|
// 'نحن نتخذ إجراءات أمنية مناسبة لحماية بياناتك الشخصية من الوصول غير المصرح به أو التعديل أو الإفصاح أو الإتلاف. تشمل هذه الإجراءات التشفير، وجدران الحماية، وضوابط الوصول الصارمة. ومع ذلك، لا يمكن ضمان أمان مطلق لنقل البيانات عبر الإنترنت.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'مشاركة البيانات مع أطراف ثالثة',
|
||||||
|
// icon: RefreshCw,
|
||||||
|
// content:
|
||||||
|
// 'لا نبيع أو نشارك معلوماتك الشخصية مع أطراف ثالثة لأغراض تسويقية دون موافقتك الصريحة. قد نشارك معلوماتك مع مزودي الخدمة الذين يساعدوننا في تشغيل المنصة (مثل معالجة الدفعات)، مع الالتزام باتفاقيات سرية صارمة. قد نكشف عن معلوماتك إذا كان ذلك مطلوباً بموجب القانون.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'حقوقك وخياراتك',
|
||||||
|
// icon: Trash2,
|
||||||
|
// content:
|
||||||
|
// 'لديك الحق في الوصول إلى بياناتك الشخصية وتحديثها أو تصحيحها أو حذفها في أي وقت. يمكنك إدارة تفضيلات الاتصال من إعدادات حسابك. يمكنك طلب حذف حسابك وجميع بياناتك المرتبطة به من خلال التواصل مع فريق الدعم. سنستجيب لطلباتك في أقرب وقت ممكن وفقاً للقوانين المعمول بها.',
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// export default function PrivacyPage() {
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
||||||
|
// <div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
// <motion.div
|
||||||
|
// initial={{ opacity: 0, y: -20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// className="text-center mb-12"
|
||||||
|
// >
|
||||||
|
// <div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
||||||
|
// <Shield className="w-10 h-10 text-amber-600" />
|
||||||
|
// </div>
|
||||||
|
// <h1 className="text-4xl font-bold text-gray-900 mb-4">سياسة الخصوصية</h1>
|
||||||
|
// <p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||||
|
// نلتزم بحماية خصوصيتك وأمان بياناتك الشخصية
|
||||||
|
// </p>
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <div className="space-y-6">
|
||||||
|
// {sections.map((section, index) => {
|
||||||
|
// const Icon = section.icon;
|
||||||
|
// return (
|
||||||
|
// <motion.div
|
||||||
|
// key={index}
|
||||||
|
// initial={{ opacity: 0, y: 20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// transition={{ delay: index * 0.1 }}
|
||||||
|
// className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-start gap-4">
|
||||||
|
// <div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center shrink-0">
|
||||||
|
// <Icon className="w-6 h-6 text-amber-600" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <h2 className="text-xl font-bold text-gray-900 mb-3">{section.title}</h2>
|
||||||
|
// <p className="text-gray-600 leading-relaxed">{section.content}</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <motion.div
|
||||||
|
// initial={{ opacity: 0 }}
|
||||||
|
// animate={{ opacity: 1 }}
|
||||||
|
// transition={{ delay: 0.6 }}
|
||||||
|
// className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
|
||||||
|
// >
|
||||||
|
// <CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
||||||
|
// <div>
|
||||||
|
// <p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
|
||||||
|
// <p className="text-amber-700">
|
||||||
|
// تم آخر تحديث لسياسة الخصوصية في 1 مايو 2026. للمزيد من المعلومات أو الاستفسارات، يرجى التواصل مع فريق الدعم.
|
||||||
|
// </p>
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { useEffect, useState } from 'react';
|
||||||
import { Shield, Lock, Eye, Database, RefreshCw, Trash2, CheckCircle } from 'lucide-react';
|
import { Languages, Loader2, Shield } from 'lucide-react';
|
||||||
|
|
||||||
const sections = [
|
const API_BASE = 'http://45.93.137.91/api';
|
||||||
{
|
|
||||||
title: 'المقدمة',
|
const ENDPOINTS = {
|
||||||
icon: Shield,
|
ar: '/Configuration/GetARPrivacyPolicy',
|
||||||
content:
|
en: '/Configuration/GetENPrivacyPolicy',
|
||||||
'نحن في SweetHome نلتزم بحماية خصوصية مستخدمينا. توضح سياسة الخصوصية هذه كيفية جمع واستخدام وحماية المعلومات الشخصية التي تقدمها عند استخدام منصتنا. باستخدامك للمنصة، فإنك توافق على الممارسات الموضحة في هذه السياسة.',
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'المعلومات التي نجمعها',
|
|
||||||
icon: Database,
|
|
||||||
content:
|
|
||||||
'نجمع المعلومات التي تقدمها مباشرة عند إنشاء حساب، مثل الاسم، البريد الإلكتروني، رقم الهاتف، ومعلومات الدفع. كما نجمع معلومات حول استخدامك للمنصة، مثل العقارات التي تتصفحها، الحجوزات التي تقوم بها، وتقييماتك. قد نجمع أيضاً معلومات تقنية مثل عنوان IP ونوع المتصفح.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'كيف نستخدم معلوماتك',
|
|
||||||
icon: Eye,
|
|
||||||
content:
|
|
||||||
'نستخدم معلوماتك لتقديم وتحسين خدماتنا، ومعالجة الحجوزات والمدفوعات، والتواصل معك بشأن حساباتك وحجوزاتك، وإرسال التحديثات والعروض الترويجية (بموافقتك)، وتحسين تجربة المستخدم وتطوير ميزات جديدة، والامتثال للالتزامات القانونية.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'حماية البيانات وأمانها',
|
|
||||||
icon: Lock,
|
|
||||||
content:
|
|
||||||
'نحن نتخذ إجراءات أمنية مناسبة لحماية بياناتك الشخصية من الوصول غير المصرح به أو التعديل أو الإفصاح أو الإتلاف. تشمل هذه الإجراءات التشفير، وجدران الحماية، وضوابط الوصول الصارمة. ومع ذلك، لا يمكن ضمان أمان مطلق لنقل البيانات عبر الإنترنت.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'مشاركة البيانات مع أطراف ثالثة',
|
|
||||||
icon: RefreshCw,
|
|
||||||
content:
|
|
||||||
'لا نبيع أو نشارك معلوماتك الشخصية مع أطراف ثالثة لأغراض تسويقية دون موافقتك الصريحة. قد نشارك معلوماتك مع مزودي الخدمة الذين يساعدوننا في تشغيل المنصة (مثل معالجة الدفعات)، مع الالتزام باتفاقيات سرية صارمة. قد نكشف عن معلوماتك إذا كان ذلك مطلوباً بموجب القانون.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'حقوقك وخياراتك',
|
|
||||||
icon: Trash2,
|
|
||||||
content:
|
|
||||||
'لديك الحق في الوصول إلى بياناتك الشخصية وتحديثها أو تصحيحها أو حذفها في أي وقت. يمكنك إدارة تفضيلات الاتصال من إعدادات حسابك. يمكنك طلب حذف حسابك وجميع بياناتك المرتبطة به من خلال التواصل مع فريق الدعم. سنستجيب لطلباتك في أقرب وقت ممكن وفقاً للقوانين المعمول بها.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
return (
|
const [language, setLanguage] = useState('ar');
|
||||||
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
|
const [policyText, setPolicyText] = useState('');
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
const [loading, setLoading] = useState(false);
|
||||||
<motion.div
|
const [error, setError] = useState('');
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="text-center mb-12"
|
|
||||||
>
|
|
||||||
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
|
|
||||||
<Shield className="w-10 h-10 text-amber-600" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">سياسة الخصوصية</h1>
|
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
||||||
نلتزم بحماية خصوصيتك وأمان بياناتك الشخصية
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
useEffect(() => {
|
||||||
{sections.map((section, index) => {
|
const controller = new AbortController();
|
||||||
const Icon = section.icon;
|
|
||||||
return (
|
const loadPolicy = async () => {
|
||||||
<motion.div
|
try {
|
||||||
key={index}
|
setLoading(true);
|
||||||
initial={{ opacity: 0, y: 20 }}
|
setError('');
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.1 }}
|
const response = await fetch(`${API_BASE}${ENDPOINTS[language]}`, {
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
method: 'GET',
|
||||||
>
|
cache: 'no-store',
|
||||||
<div className="flex items-start gap-4">
|
signal: controller.signal,
|
||||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center shrink-0">
|
headers: {
|
||||||
<Icon className="w-6 h-6 text-amber-600" />
|
Accept: 'text/plain',
|
||||||
</div>
|
},
|
||||||
<div>
|
});
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-3">{section.title}</h2>
|
|
||||||
<p className="text-gray-600 leading-relaxed">{section.content}</p>
|
if (!response.ok) {
|
||||||
</div>
|
throw new Error(`HTTP ${response.status}`);
|
||||||
</div>
|
}
|
||||||
</motion.div>
|
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
console.log('API RESPONSE:', text);
|
||||||
|
|
||||||
|
setPolicyText(text.trim());
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
setPolicyText('');
|
||||||
|
setError(
|
||||||
|
language === 'ar'
|
||||||
|
? 'تعذر تحميل النص من الخادم.'
|
||||||
|
: 'Failed to load text from the server.'
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPolicy();
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dir={language === 'ar' ? 'rtl' : 'ltr'}
|
||||||
|
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-3xl px-4">
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
|
<Shield className="h-6 w-6 text-amber-600" />
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
{language === 'ar' ? 'سياسة الخصوصية' : 'Privacy Policy'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<button
|
||||||
initial={{ opacity: 0 }}
|
type="button"
|
||||||
animate={{ opacity: 1 }}
|
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
|
||||||
transition={{ delay: 0.6 }}
|
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
|
||||||
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
|
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
|
<Languages className="h-4 w-4" />
|
||||||
<div>
|
{language === 'ar' ? 'English' : 'العربية'}
|
||||||
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
|
</button>
|
||||||
<p className="text-amber-700">
|
</div>
|
||||||
تم آخر تحديث لسياسة الخصوصية في 1 مايو 2026. للمزيد من المعلومات أو الاستفسارات، يرجى التواصل مع فريق الدعم.
|
|
||||||
</p>
|
{loading && (
|
||||||
|
<div className="mb-6 flex items-center gap-3 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
|
<span>{language === 'ar' ? 'جاري التحميل...' : 'Loading...'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-red-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||||
|
<div className="whitespace-pre-wrap leading-8 text-gray-800">
|
||||||
|
{policyText}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import toast, { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -69,23 +69,70 @@ import { useFavorites } from "@/app/contexts/FavoritesContext";
|
|||||||
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums";
|
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums";
|
||||||
import PropertyRatingList from "@/app/components/ratings/PropertyRatingList";
|
import PropertyRatingList from "@/app/components/ratings/PropertyRatingList";
|
||||||
import { getPropertyAverageRating } from "../../utils/ratings";
|
import { getPropertyAverageRating } from "../../utils/ratings";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
const MapContainer = dynamic(
|
function PropertyDetailMap({ lat, lng, title }) {
|
||||||
() => import("react-leaflet").then((m) => m.MapContainer),
|
const mapRef = useRef(null);
|
||||||
{ ssr: false },
|
const mapInstanceRef = useRef(null);
|
||||||
);
|
const markerRef = useRef(null);
|
||||||
const TileLayer = dynamic(
|
|
||||||
() => import("react-leaflet").then((m) => m.TileLayer),
|
useEffect(() => {
|
||||||
{ ssr: false },
|
if (!mapRef.current || mapInstanceRef.current) return;
|
||||||
);
|
|
||||||
const Marker = dynamic(() => import("react-leaflet").then((m) => m.Marker), {
|
if (mapRef.current._leaflet_id && !mapInstanceRef.current) {
|
||||||
ssr: false,
|
delete mapRef.current._leaflet_id;
|
||||||
});
|
}
|
||||||
const Popup = dynamic(() => import("react-leaflet").then((m) => m.Popup), {
|
|
||||||
ssr: false,
|
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) {
|
function formatCurrency(amount) {
|
||||||
if (!amount || isNaN(amount)) return "0";
|
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"
|
className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-200"
|
||||||
>
|
>
|
||||||
<div className="h-64">
|
<div className="h-64">
|
||||||
<MapContainer
|
<PropertyDetailMap
|
||||||
center={[property.location.lat, property.location.lng]}
|
lat={property.location.lat}
|
||||||
zoom={14}
|
lng={property.location.lng}
|
||||||
className="h-full w-full"
|
title={property.title}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-amber-50 text-center text-sm text-amber-700 flex items-center justify-center gap-2">
|
<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" />
|
<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';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
@ -5,7 +400,7 @@ 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, CreditCard, Timer, Star,
|
MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star, Flag,
|
||||||
} 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';
|
||||||
@ -47,6 +442,106 @@ const propImages = (p, r) => {
|
|||||||
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
|
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
|
||||||
const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 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) {
|
function parseTimeSpan(str) {
|
||||||
if (!str) return 0;
|
if (!str) return 0;
|
||||||
const clean = str.replace(/-/g, '');
|
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>;
|
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 p = r._prop;
|
||||||
const imgs = propImages(p, r);
|
const imgs = propImages(p, r);
|
||||||
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
|
||||||
@ -114,6 +683,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
|
|||||||
: null;
|
: null;
|
||||||
const isExpired = deadline ? Date.now() > deadline : false;
|
const isExpired = deadline ? Date.now() > deadline : false;
|
||||||
const isPaying = payingId === r.id;
|
const isPaying = payingId === r.id;
|
||||||
|
const isReporting = reportingId === r.id;
|
||||||
const [showRating, setShowRating] = useState(false);
|
const [showRating, setShowRating] = useState(false);
|
||||||
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
|
||||||
const [ratingComment, setRatingComment] = useState('');
|
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 ? 'جاري الدفع...' : 'ادفع الآن'}
|
{isPaying ? <Loader2 className="w-4 h-4 animate-spin"/> : <CreditCard className="w-4 h-4"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||||
</button>}
|
</button>}
|
||||||
</div>
|
</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)}
|
{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">
|
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"/> قيّم هذا العقار
|
<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;
|
if (!isOpen || !r) return null;
|
||||||
const p = r._prop;
|
const p = r._prop;
|
||||||
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
|
||||||
@ -223,6 +797,7 @@ function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
|
|||||||
: null;
|
: null;
|
||||||
const isExpired = deadline ? Date.now() > deadline : false;
|
const isExpired = deadline ? Date.now() > deadline : false;
|
||||||
const isPaying = payingId === r.id;
|
const isPaying = payingId === r.id;
|
||||||
|
const isReporting = reportingId === 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}}
|
||||||
@ -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 ? 'جاري الدفع...' : 'ادفع الآن'}
|
{isPaying ? <Loader2 className="w-5 h-5 animate-spin"/> : <CreditCard className="w-5 h-5"/>} {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
|
||||||
</button>}
|
</button>}
|
||||||
</div>}
|
</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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -284,6 +863,8 @@ export default function UserReservationsPage() {
|
|||||||
const [filterStatus, setFilterStatus] = useState('all');
|
const [filterStatus, setFilterStatus] = useState('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [payingId, setPayingId] = useState(null);
|
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]);
|
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
|
||||||
|
|
||||||
@ -320,7 +901,10 @@ export default function UserReservationsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let r = reservations;
|
let r = reservations;
|
||||||
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
|
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);
|
setFiltered(r);
|
||||||
}, [reservations, filterStatus, searchTerm]);
|
}, [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>;
|
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)} 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">
|
<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>
|
||||||
@ -380,7 +1002,17 @@ 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} 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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,260 @@
|
|||||||
|
// 'use client';
|
||||||
|
|
||||||
|
// import { useState } from 'react';
|
||||||
|
// import { useRouter } from 'next/navigation';
|
||||||
|
// import Link from 'next/link';
|
||||||
|
// import { motion } from 'framer-motion';
|
||||||
|
// import {
|
||||||
|
// User,
|
||||||
|
// Shield,
|
||||||
|
// Trash2,
|
||||||
|
// LogOut,
|
||||||
|
// ChevronLeft,
|
||||||
|
// Bell,
|
||||||
|
// Lock,
|
||||||
|
// Eye,
|
||||||
|
// FileText,
|
||||||
|
// HelpCircle,
|
||||||
|
// MessageCircle,
|
||||||
|
// Loader2,
|
||||||
|
// AlertTriangle,
|
||||||
|
// X
|
||||||
|
// } from 'lucide-react';
|
||||||
|
// import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
// import AuthService from '../services/AuthService';
|
||||||
|
// import { changePassword, deleteMyAccount } from '../utils/api';
|
||||||
|
|
||||||
|
// export default function SettingsPage() {
|
||||||
|
// const router = useRouter();
|
||||||
|
// const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
// const [deletePassword, setDeletePassword] = useState('');
|
||||||
|
// const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
// const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
|
// const handleSignOut = () => {
|
||||||
|
// AuthService.deleteToken();
|
||||||
|
// toast.success('تم تسجيل الخروج بنجاح');
|
||||||
|
// router.push('/');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleDeleteAccount = async () => {
|
||||||
|
// if (!deletePassword) {
|
||||||
|
// toast.error('الرجاء إدخال كلمة المرور');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// setIsDeleting(true);
|
||||||
|
// try {
|
||||||
|
// await deleteMyAccount(deletePassword);
|
||||||
|
// AuthService.deleteToken();
|
||||||
|
// toast.success('تم حذف الحساب بنجاح');
|
||||||
|
// router.push('/');
|
||||||
|
// } catch (err) {
|
||||||
|
// toast.error(err.message || 'فشل حذف الحساب');
|
||||||
|
// } finally {
|
||||||
|
// setIsDeleting(false);
|
||||||
|
// setShowDeleteDialog(false);
|
||||||
|
// setDeletePassword('');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const sections = [
|
||||||
|
// {
|
||||||
|
// title: 'الحساب',
|
||||||
|
// items: [
|
||||||
|
// { icon: User, label: 'الملف الشخصي', href: '/profile', desc: 'عرض وتعديل معلوماتك الشخصية' },
|
||||||
|
// { icon: Lock, label: 'تغيير كلمة المرور', href: '/change-password', desc: 'تحديث كلمة المرور الخاصة بك' },
|
||||||
|
// { icon: Shield, label: 'التحقق من الحساب', href: '/account-verification', desc: 'تأكيد البريد الإلكتروني ورقم الهاتف' },
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'الإشعارات',
|
||||||
|
// items: [
|
||||||
|
// { icon: Bell, label: 'الإشعارات', href: '/notifications', desc: 'إدارة تفضيلات الإشعارات' },
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'الدعم',
|
||||||
|
// items: [
|
||||||
|
// { icon: HelpCircle, label: 'الأسئلة الشائعة', href: '/faq', desc: 'إجابات للأسئلة المتكررة' },
|
||||||
|
// { icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
||||||
|
// { icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
||||||
|
// { icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// const containerVariants = {
|
||||||
|
// hidden: { opacity: 0 },
|
||||||
|
// visible: {
|
||||||
|
// opacity: 1,
|
||||||
|
// transition: { staggerChildren: 0.08 }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const itemVariants = {
|
||||||
|
// hidden: { opacity: 0, y: 20 },
|
||||||
|
// visible: { opacity: 1, y: 0 }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 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-2xl">
|
||||||
|
// <motion.div
|
||||||
|
// initial={{ opacity: 0, y: -20 }}
|
||||||
|
// animate={{ opacity: 1, y: 0 }}
|
||||||
|
// className="flex items-center gap-3 mb-8"
|
||||||
|
// >
|
||||||
|
// <Link
|
||||||
|
// href="/profile"
|
||||||
|
// className="p-2 rounded-xl hover:bg-gray-200 transition-colors text-gray-600"
|
||||||
|
// >
|
||||||
|
// <ChevronLeft className="w-5 h-5" />
|
||||||
|
// </Link>
|
||||||
|
// <h1 className="text-2xl font-bold text-gray-900">الإعدادات</h1>
|
||||||
|
// </motion.div>
|
||||||
|
|
||||||
|
// <motion.div
|
||||||
|
// variants={containerVariants}
|
||||||
|
// initial="hidden"
|
||||||
|
// animate="visible"
|
||||||
|
// className="space-y-6"
|
||||||
|
// >
|
||||||
|
// {sections.map((section) => (
|
||||||
|
// <motion.div
|
||||||
|
// key={section.title}
|
||||||
|
// variants={itemVariants}
|
||||||
|
// className="bg-white rounded-2xl shadow-sm overflow-hidden"
|
||||||
|
// >
|
||||||
|
// <div className="px-6 py-4 border-b border-gray-100">
|
||||||
|
// <h2 className="text-lg font-semibold text-gray-800">{section.title}</h2>
|
||||||
|
// </div>
|
||||||
|
// <div className="divide-y divide-gray-50">
|
||||||
|
// {section.items.map((item) => {
|
||||||
|
// const Icon = item.icon;
|
||||||
|
// return (
|
||||||
|
// <Link
|
||||||
|
// key={item.label}
|
||||||
|
// href={item.href}
|
||||||
|
// className="flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group"
|
||||||
|
// >
|
||||||
|
// <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors">
|
||||||
|
// <Icon className="w-5 h-5 text-amber-600" />
|
||||||
|
// </div>
|
||||||
|
// <div className="flex-1 min-w-0">
|
||||||
|
// <p className="text-sm font-medium text-gray-900">{item.label}</p>
|
||||||
|
// <p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
||||||
|
// </div>
|
||||||
|
// <ChevronLeft className="w-4 h-4 text-gray-400" />
|
||||||
|
// </Link>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
// ))}
|
||||||
|
|
||||||
|
// <motion.div variants={itemVariants} className="space-y-4">
|
||||||
|
// <div className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
// <div className="px-6 py-4 border-b border-gray-100">
|
||||||
|
// <h2 className="text-lg font-semibold text-gray-800">الأمان</h2>
|
||||||
|
// </div>
|
||||||
|
// <div className="p-6">
|
||||||
|
// <button
|
||||||
|
// onClick={() => setShowDeleteDialog(true)}
|
||||||
|
// className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
|
||||||
|
// >
|
||||||
|
// <Trash2 className="w-5 h-5" />
|
||||||
|
// <span className="text-sm font-medium">حذف الحساب</span>
|
||||||
|
// </button>
|
||||||
|
// <p className="text-xs text-gray-500 mt-2 pr-12">
|
||||||
|
// سيتم حذف جميع بياناتك بشكل دائم ولا يمكن التراجع عن هذا الإجراء
|
||||||
|
// </p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||||
|
// <div className="p-6">
|
||||||
|
// <button
|
||||||
|
// onClick={handleSignOut}
|
||||||
|
// className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
|
// >
|
||||||
|
// <LogOut className="w-5 h-5" />
|
||||||
|
// <span className="text-sm font-medium">تسجيل الخروج</span>
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
// </motion.div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {showDeleteDialog && (
|
||||||
|
// <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||||
|
// <motion.div
|
||||||
|
// initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
// animate={{ opacity: 1, scale: 1 }}
|
||||||
|
// className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-center gap-3 mb-4">
|
||||||
|
// <div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
|
||||||
|
// <AlertTriangle className="w-5 h-5 text-red-600" />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <h3 className="text-lg font-semibold text-gray-900">حذف الحساب</h3>
|
||||||
|
// <p className="text-sm text-gray-500">هذا الإجراء لا يمكن التراجع عنه</p>
|
||||||
|
// </div>
|
||||||
|
// <button
|
||||||
|
// onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
|
||||||
|
// className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
// >
|
||||||
|
// <X className="w-5 h-5 text-gray-400" />
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <p className="text-sm text-gray-600 mb-4">
|
||||||
|
// أدخل كلمة المرور لتأكيد حذف حسابك نهائياً. سيتم حذف جميع بياناتك وملفاتك بشكل دائم.
|
||||||
|
// </p>
|
||||||
|
|
||||||
|
// <input
|
||||||
|
// type="password"
|
||||||
|
// value={deletePassword}
|
||||||
|
// onChange={(e) => setDeletePassword(e.target.value)}
|
||||||
|
// placeholder="كلمة المرور"
|
||||||
|
// className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-red-500 focus:border-transparent outline-none"
|
||||||
|
// />
|
||||||
|
|
||||||
|
// <div className="flex gap-3">
|
||||||
|
// <button
|
||||||
|
// onClick={() => { setShowDeleteDialog(false); setDeletePassword(''); }}
|
||||||
|
// className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
|
||||||
|
// >
|
||||||
|
// إلغاء
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// onClick={handleDeleteAccount}
|
||||||
|
// disabled={isDeleting}
|
||||||
|
// className="flex-1 px-4 py-3 rounded-xl bg-red-600 text-white hover:bg-red-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
// >
|
||||||
|
// {isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||||
|
// {isDeleting ? 'جاري الحذف...' : 'تأكيد الحذف'}
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </motion.div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -31,6 +288,11 @@ export default function SettingsPage() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
||||||
|
const [showReportDialog, setShowReportDialog] = useState(false);
|
||||||
|
const [reportSubject, setReportSubject] = useState('');
|
||||||
|
const [reportBody, setReportBody] = useState('');
|
||||||
|
const [isSendingReport, setIsSendingReport] = useState(false);
|
||||||
|
|
||||||
const handleSignOut = () => {
|
const handleSignOut = () => {
|
||||||
AuthService.deleteToken();
|
AuthService.deleteToken();
|
||||||
toast.success('تم تسجيل الخروج بنجاح');
|
toast.success('تم تسجيل الخروج بنجاح');
|
||||||
@ -49,6 +311,7 @@ export default function SettingsPage() {
|
|||||||
toast.success('تم حذف الحساب بنجاح');
|
toast.success('تم حذف الحساب بنجاح');
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Delete account error:', err);
|
||||||
toast.error(err.message || 'فشل حذف الحساب');
|
toast.error(err.message || 'فشل حذف الحساب');
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
@ -57,6 +320,70 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSendGeneralReport = async () => {
|
||||||
|
if (!reportSubject.trim() || !reportBody.trim()) {
|
||||||
|
toast.error('الرجاء تعبئة عنوان البلاغ ونصه');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportSubject.trim().length > 300) {
|
||||||
|
toast.error('عنوان البلاغ يجب ألا يتجاوز 300 حرف');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token =
|
||||||
|
AuthService.getToken?.() ||
|
||||||
|
(typeof window !== 'undefined'
|
||||||
|
? localStorage.getItem('token') ||
|
||||||
|
localStorage.getItem('accessToken') ||
|
||||||
|
localStorage.getItem('authToken')
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('No token found. Checked AuthService.getToken and localStorage keys: token, accessToken, authToken');
|
||||||
|
toast.error('لم يتم العثور على التوكن');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSendingReport(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('http://45.93.137.91/api/Reports/SendGeneralReport', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
subject: reportSubject.trim(),
|
||||||
|
body: reportBody.trim(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await res.text();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Send report failed:', {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
responseText,
|
||||||
|
});
|
||||||
|
throw new Error(responseText || `فشل إرسال البلاغ (HTTP ${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Send report success:', responseText);
|
||||||
|
toast.success(responseText || 'تم إرسال البلاغ بنجاح');
|
||||||
|
setShowReportDialog(false);
|
||||||
|
setReportSubject('');
|
||||||
|
setReportBody('');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Send report error:', err);
|
||||||
|
toast.error(err.message || 'حدث خطأ أثناء إرسال البلاغ');
|
||||||
|
} finally {
|
||||||
|
setIsSendingReport(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
title: 'الحساب',
|
title: 'الحساب',
|
||||||
@ -79,6 +406,7 @@ export default function SettingsPage() {
|
|||||||
{ icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
{ icon: MessageCircle, label: 'تواصل معنا', href: '/support', desc: 'الحصول على المساعدة والدعم' },
|
||||||
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
{ icon: FileText, label: 'الشروط والأحكام', href: '/terms', desc: 'سياسة الاستخدام والخصوصية' },
|
||||||
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
{ icon: Eye, label: 'سياسة الخصوصية', href: '/privacy', desc: 'كيف نحمي بياناتك' },
|
||||||
|
{ icon: AlertTriangle, label: 'إرسال بلاغ عام', desc: 'إرسال مشكلة أو ملاحظة إلى الإدارة', action: () => setShowReportDialog(true) },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -133,12 +461,9 @@ export default function SettingsPage() {
|
|||||||
<div className="divide-y divide-gray-50">
|
<div className="divide-y divide-gray-50">
|
||||||
{section.items.map((item) => {
|
{section.items.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
return (
|
|
||||||
<Link
|
const content = (
|
||||||
key={item.label}
|
<>
|
||||||
href={item.href}
|
|
||||||
className="flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors">
|
<div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors">
|
||||||
<Icon className="w-5 h-5 text-amber-600" />
|
<Icon className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
@ -147,6 +472,28 @@ export default function SettingsPage() {
|
|||||||
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
<p className="text-xs text-gray-500 truncate">{item.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronLeft className="w-4 h-4 text-gray-400" />
|
<ChevronLeft className="w-4 h-4 text-gray-400" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item.action) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.label}
|
||||||
|
onClick={item.action}
|
||||||
|
className="w-full flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group text-right"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
className="flex items-center gap-4 px-6 py-4 hover:bg-gray-50 transition-colors group"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -242,6 +589,74 @@ export default function SettingsPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showReportDialog && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
|
||||||
|
<MessageCircle className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">إرسال بلاغ عام</h3>
|
||||||
|
<p className="text-sm text-gray-500">سيتم إرسال البلاغ إلى الإدارة</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowReportDialog(false);
|
||||||
|
setReportSubject('');
|
||||||
|
setReportBody('');
|
||||||
|
}}
|
||||||
|
className="mr-auto p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={reportSubject}
|
||||||
|
onChange={(e) => setReportSubject(e.target.value)}
|
||||||
|
placeholder="عنوان البلاغ"
|
||||||
|
maxLength={300}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={reportBody}
|
||||||
|
onChange={(e) => setReportBody(e.target.value)}
|
||||||
|
placeholder="اكتب تفاصيل البلاغ هنا..."
|
||||||
|
rows={5}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl mb-4 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowReportDialog(false);
|
||||||
|
setReportSubject('');
|
||||||
|
setReportBody('');
|
||||||
|
}}
|
||||||
|
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSendGeneralReport}
|
||||||
|
disabled={isSendingReport}
|
||||||
|
className="flex-1 px-4 py-3 rounded-xl bg-amber-600 text-white hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{isSendingReport ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageCircle className="w-4 h-4" />}
|
||||||
|
{isSendingReport ? 'جاري الإرسال...' : 'إرسال البلاغ'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -449,14 +449,48 @@
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
import AuthService from '../services/AuthService';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import AuthService from '../services/AuthService';
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
||||||
|
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || 'http://45.93.137.91/api';
|
||||||
|
|
||||||
|
|
||||||
function isFormData(value) {
|
function isFormData(value) {
|
||||||
return typeof FormData !== 'undefined' && value instanceof FormData;
|
return typeof FormData !== 'undefined' && value instanceof FormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ApiBlockedError extends Error {
|
||||||
|
constructor(message = 'Your account is blocked') {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiBlockedError';
|
||||||
|
this.status = 451;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isApiBlockedError(error) {
|
||||||
|
return error instanceof ApiBlockedError || error?.status === 451;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToBlockedPage() {
|
||||||
|
if (typeof window !== 'undefined' && window.location.pathname !== '/blocked') {
|
||||||
|
window.location.replace('/blocked');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNotBlocked(response) {
|
||||||
|
if (response.status === 451) {
|
||||||
|
redirectToBlockedPage();
|
||||||
|
throw new ApiBlockedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiUrl(base, endpoint) {
|
||||||
|
return `${base.replace(/\/$/, '')}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic API fetch — attaches auth token, unwraps { data } envelope
|
* Generic API fetch — attaches auth token, unwraps { data } envelope
|
||||||
*/
|
*/
|
||||||
@ -475,7 +509,13 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
headers['Content-Type'] = 'application/json';
|
headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}${endpoint}`, {
|
const url = `${API_BASE}${endpoint}`;
|
||||||
|
|
||||||
|
console.log('API Request:', url);
|
||||||
|
console.log('API Method:', options.method || 'GET');
|
||||||
|
console.log('API Body:', hasBody ? options.body : null);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
body:
|
body:
|
||||||
@ -484,8 +524,13 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
: options.body,
|
: options.body,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('API Response Status:', res.status);
|
||||||
|
console.log('API Response OK:', res.ok);
|
||||||
|
assertNotBlocked(res);
|
||||||
|
|
||||||
if (!res.ok && res.status !== 206) {
|
if (!res.ok && res.status !== 206) {
|
||||||
const text = await res.text().catch(() => '');
|
const text = await res.text().catch(() => '');
|
||||||
|
console.error('API Error Response:', text || res.statusText);
|
||||||
throw new Error(`API ${res.status}: ${text || res.statusText}`);
|
throw new Error(`API ${res.status}: ${text || res.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,6 +569,36 @@ async function authFetch(endpoint, body, token = null) {
|
|||||||
body: bodyIsFormData ? body : JSON.stringify(body),
|
body: bodyIsFormData ? body : JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertNotBlocked(res);
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
let data = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = text ? JSON.parse(text) : null;
|
||||||
|
if (data && typeof data === 'object' && 'data' in data) {
|
||||||
|
data = data.data;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
data = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = typeof data === 'object' && data?.message ? data.message : null;
|
||||||
|
|
||||||
|
return { status: res.status, data, ok: res.ok || res.status === 206, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reportFetch(endpoint, body) {
|
||||||
|
const res = await fetch(buildApiUrl(REPORT_API_BASE, endpoint), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotBlocked(res);
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let data = null;
|
let data = null;
|
||||||
|
|
||||||
@ -721,6 +796,8 @@ export async function uploadPicture(file) {
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertNotBlocked(res);
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${text}`);
|
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${text}`);
|
||||||
@ -746,6 +823,8 @@ async function multipartAuthFetch(endpoint, formData) {
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertNotBlocked(res);
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let data = null;
|
let data = null;
|
||||||
|
|
||||||
@ -948,6 +1027,8 @@ export async function registerRealEstateAgent(formData) {
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertNotBlocked(res);
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let data = null;
|
let data = null;
|
||||||
|
|
||||||
@ -1028,8 +1109,15 @@ export async function filterRentProperties(params = {}) {
|
|||||||
|
|
||||||
// ─── Reports ───
|
// ─── Reports ───
|
||||||
|
|
||||||
|
export async function sendGeneralReport(subject, reportBody) {
|
||||||
|
return reportFetch('/Reports/SendGeneralReport', {
|
||||||
|
subject,
|
||||||
|
body: reportBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function submitReport(subject, body) {
|
export async function submitReport(subject, body) {
|
||||||
return apiFetch('/Reports', {
|
return apiFetch('/Reports/SendGeneralReport', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { subject, body },
|
body: { subject, body },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user