diff --git a/app/owner/reservations/page.js b/app/owner/reservations/page.js
index 43fd68a..15f2c3b 100644
--- a/app/owner/reservations/page.js
+++ b/app/owner/reservations/page.js
@@ -1,104 +1,258 @@
+
// '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, User, RefreshCw, Mail, Phone,
+// Calendar,
+// Clock,
+// CheckCircle,
+// XCircle,
+// Eye,
+// Loader2,
+// Search,
+// MapPin,
+// DollarSign,
+// Home,
+// ArrowLeft,
+// RefreshCw,
// } from 'lucide-react';
// import toast, { Toaster } from 'react-hot-toast';
// import AuthService from '../../services/AuthService';
// import { getRentProperty } from '../../utils/api';
-// const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
+// const 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_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-green-100 text-green-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-blue-100 text-blue-800', icon: CheckCircle },
-// cancelled: { label: 'ملغي', color: 'bg-gray-100 text-gray-800', icon: XCircle },
+// pending: {
+// label: 'قيد الانتظار',
+// color: 'bg-yellow-100 text-yellow-800',
+// icon: Clock,
+// },
+// ownerConfirmed: {
+// label: 'مؤكد',
+// color: 'bg-green-100 text-green-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-blue-100 text-blue-800',
+// icon: CheckCircle,
+// },
+// cancelled: {
+// label: 'ملغي',
+// color: 'bg-gray-100 text-gray-800',
+// icon: XCircle,
+// },
// };
-// const sLabel = c => STATUS_UI[STATUS_MAP[c]]?.label ?? String(c);
-// const sColor = c => STATUS_UI[STATUS_MAP[c]]?.color ?? 'bg-gray-100 text-gray-700';
-// const sIcon = c => STATUS_UI[STATUS_MAP[c]]?.icon ?? Clock;
+// const getStatusKey = (code) => STATUS_MAP[Number(code)] || 'pending';
+
+// const sLabel = (code) => STATUS_UI[getStatusKey(code)]?.label ?? String(code);
+
+// const sColor = (code) =>
+// STATUS_UI[getStatusKey(code)]?.color ?? 'bg-gray-100 text-gray-700';
+
+// const sIcon = (code) => STATUS_UI[getStatusKey(code)]?.icon ?? Clock;
// function StatusBadge({ code }) {
// const Icon = sIcon(code);
-// return {sLabel(code)};
+
+// return (
+//
+// {sLabel(code)}
+//
+// );
// }
// async function enrich(r) {
-// if (!r.propertyId) return r;
+// if (!r?.propertyId) return r;
+
// try {
// const prop = await getRentProperty(r.propertyId);
-// r._prop = prop?.propertyInformation ?? prop ?? null;
-// } catch { /* skip */ }
-// return r;
+
+// return {
+// ...r,
+// _prop: prop?.propertyInformation ?? prop ?? null,
+// };
+// } catch {
+// return {
+// ...r,
+// _prop: null,
+// };
+// }
// }
-// const pAddr = p => p?.address ?? '';
-// const pImgs = p => Array.isArray(p?.images) ? p.images : [];
-// const pBeds = p => p?.numberOfBedRooms ?? 0;
-// const pBaths = p => p?.numberOfBathRooms ?? 0;
-// const API = (token, method, path, body) => fetch(`${API_BASE}${path}`, {
-// method: method || 'GET',
-// headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) },
-// ...(body && { body: JSON.stringify(body) }),
-// });
+// const pAddr = (p) => p?.address ?? '';
+// const pImgs = (p) => (Array.isArray(p?.images) ? p.images : []);
+// const pBeds = (p) => p?.numberOfBedRooms ?? 0;
+// const pBaths = (p) => p?.numberOfBathRooms ?? 0;
-// function OwnerCard({ r, onViewDetails, onConfirm, onReject }) {
+// const API = (token, method, path, body) => {
+// return fetch(`${API_BASE}${path}`, {
+// method: method || 'GET',
+// headers: {
+// 'Content-Type': 'application/json',
+// ...(token && { Authorization: `Bearer ${token}` }),
+// },
+// ...(body && { body: JSON.stringify(body) }),
+// });
+// };
+
+// function buildImageUrl(path) {
+// if (!path) return null;
+
+// if (path.startsWith('http://') || path.startsWith('https://')) {
+// return path;
+// }
+
+// return `${API_BASE}${path}`;
+// }
+
+// function OwnerCard({
+// r,
+// onViewDetails,
+// onConfirm,
+// onReject,
+// actionLoadingId,
+// }) {
// const p = r._prop;
// const imgs = pImgs(p);
-// const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
+// const img = imgs.length > 0 ? buildImageUrl(imgs[0]) : null;
// const addr = pAddr(p);
-// const isPending = r.status === 0; // Pending
+// const isPending = Number(r.status) === 0;
+// const isActionLoading = actionLoadingId === r.id;
// return (
-//
+//
//
-// {img &&
}
+// {img && (
+//
+//

+//
+// )}
+
//
//
-//
-// {addr && {addr}
}
+//
+
+// {addr && (
+//
+//
+// {addr}
+//
+// )}
//
+
//
-//
{r.totalPrice?.toLocaleString()??'—'}
+//
+// {r.totalPrice?.toLocaleString() ?? '—'}
+//
//
السعر الإجمالي
//
//
-// {(pBeds(p)||pBaths(p)) &&
{pBeds(p)>0&&{pBeds(p)} غرف}{pBaths(p)>0&&{pBaths(p)} حمامات}
}
+
+// {(pBeds(p) || pBaths(p)) && (
+//
+// {pBeds(p) > 0 && {pBeds(p)} غرف}
+// {pBaths(p) > 0 && {pBaths(p)} حمامات}
+//
+// )}
+
//
//
-//
من
-//
{new Date(r.startDate).toLocaleDateString('ar')}
+//
+//
من
+//
+// {r.startDate
+// ? new Date(r.startDate).toLocaleDateString('ar')
+// : '—'}
+//
//
+
//
-//
إلى
-//
{new Date(r.endDate).toLocaleDateString('ar')}
+//
+//
إلى
+//
+// {r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
+//
//
//
-//
-//
//
@@ -107,39 +261,120 @@
// function DetailsModal({ r, isOpen, onClose }) {
// if (!isOpen || !r) return null;
+
// const p = r._prop;
+
// return (
-//
-// e.stopPropagation()}>
+//
+// e.stopPropagation()}
+// >
//
//
//
طلب حجز #{r.id}
-//
+
+//
+//
+//
//
//
+
//
-// {p &&
-//
معلومات العقار
-//
العنوان: {pAddr(p)||'—'}
-// {(pBeds(p)||pBaths(p)) &&
-// {pBeds(p)>0&&{pBeds(p)} غرف}
-// {pBaths(p)>0&&{pBaths(p)} حمامات}
-//
}
-//
}
+// {p && (
+//
+//
+//
+// معلومات العقار
+//
+
+//
+// العنوان:{' '}
+// {pAddr(p) || '—'}
+//
+
+// {(pBeds(p) || pBaths(p)) && (
+//
+// {pBeds(p) > 0 && (
+//
+// {pBeds(p)} غرف
+//
+// )}
+
+// {pBaths(p) > 0 && (
+//
+// {pBaths(p)} حمامات
+//
+// )}
+//
+// )}
+//
+// )}
+
//
-//
تفاصيل الحجز
+//
+//
+// تفاصيل الحجز
+//
+
//
-//
تاريخ البداية
{new Date(r.startDate).toLocaleDateString('ar')}
-//
تاريخ النهاية
{new Date(r.endDate).toLocaleDateString('ar')}
-//
-//
تاريخ الإنشاء
{new Date(r.createdAt).toLocaleDateString('ar')}
+//
+//
تاريخ البداية
+//
+// {r.startDate
+// ? new Date(r.startDate).toLocaleDateString('ar')
+// : '—'}
+//
+//
+
+//
+//
تاريخ النهاية
+//
+// {r.endDate ? new Date(r.endDate).toLocaleDateString('ar') : '—'}
+//
+//
+
+//
+
+//
+//
تاريخ الإنشاء
+//
+// {r.createdAt
+// ? new Date(r.createdAt).toLocaleDateString('ar')
+// : '—'}
+//
+//
//
//
+
//
-//
المعلومات المالية
-//
الإجمالي{r.totalPrice?.toLocaleString()??'—'}
+//
+//
+// المعلومات المالية
+//
+
+//
+// الإجمالي
+//
+// {r.totalPrice?.toLocaleString() ?? '—'}
+//
+//
//
//
//
@@ -149,111 +384,256 @@
// export default function OwnerReservationRequestsPage() {
// const router = useRouter();
+
// const [reservations, setReservations] = useState([]);
// const [filtered, setFiltered] = useState([]);
// const [loading, setLoading] = useState(true);
// const [selected, setSelected] = useState(null);
// const [filterStatus, setFilterStatus] = useState('all');
// const [searchTerm, setSearchTerm] = useState('');
-
-// useEffect(() => {
-// if (!AuthService.getUser() || !AuthService.isOwner()) { router.push('/auth/choose-role'); return; }
-// loadReservations();
-// }, [router]);
+// const [actionLoadingId, setActionLoadingId] = useState(null);
// const loadReservations = useCallback(async () => {
// try {
+// setLoading(true);
+
// const token = AuthService.getToken();
-// const res = await fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, {
-// headers: { Authorization: `Bearer ${token}` },
-// });
-// if (!res.ok) throw new Error(`HTTP ${res.status}`);
+
+// const res = await fetch(
+// `${API_BASE}/Reservations/GetOwnerResevationRequests`,
+// {
+// method: 'GET',
+// headers: {
+// Authorization: `Bearer ${token}`,
+// 'Content-Type': 'application/json',
+// },
+// }
+// );
+
+// if (!res.ok) {
+// const errorText = await res.text();
+// throw new Error(errorText || `HTTP ${res.status}`);
+// }
+
// const json = await res.json();
+
// let list = json.data || json || [];
// if (!Array.isArray(list)) list = [];
+
// const enriched = await Promise.all(list.map(enrich));
+
// setReservations(enriched);
// setFiltered(enriched);
// } catch (err) {
// console.error(err);
// toast.error('فشل تحميل طلبات الحجز');
+// setReservations([]);
+// setFiltered([]);
+// } finally {
+// setLoading(false);
// }
-// 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 => pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q));
+// if (!AuthService.getUser() || !AuthService.isOwner()) {
+// router.push('/auth/choose-role');
+// return;
// }
-// setFiltered(r);
+
+// loadReservations();
+// }, [router, loadReservations]);
+
+// useEffect(() => {
+// let result = reservations;
+
+// if (filterStatus !== 'all') {
+// result = result.filter((x) => getStatusKey(x.status) === filterStatus);
+// }
+
+// if (searchTerm.trim()) {
+// const q = searchTerm.trim().toLowerCase();
+
+// result = result.filter(
+// (x) =>
+// pAddr(x._prop).toLowerCase().includes(q) || String(x.id).includes(q)
+// );
+// }
+
+// setFiltered(result);
// }, [reservations, filterStatus, searchTerm]);
// const handleConfirm = async (r) => {
// try {
-// const res = await API(AuthService.getToken(), 'PUT', `/Reservations/owner-confirm/${r.id}`);
-// if (!res.ok) throw new Error(`HTTP ${res.status}`);
+// setActionLoadingId(r.id);
+
+// const res = await API(
+// AuthService.getToken(),
+// 'PUT',
+// `/Reservations/OwnerConfirmReservation/owner-confirm/${r.id}`
+// );
+
+// if (!res.ok) {
+// const errorText = await res.text();
+// throw new Error(errorText || `HTTP ${res.status}`);
+// }
+
// toast.success('تم قبول الحجز بنجاح');
// await loadReservations();
-// } catch (err) { console.error(err); toast.error('فشل قبول الحجز'); }
+// } catch (err) {
+// console.error(err);
+// toast.error('فشل قبول الحجز');
+// } finally {
+// setActionLoadingId(null);
+// }
// };
// const handleReject = async (r) => {
// if (!confirm('هل أنت متأكد من رفض هذا الحجز؟')) return;
+
// try {
-// const res = await API(AuthService.getToken(), 'PUT', `/Reservations/reject/${r.id}`);
-// if (!res.ok) throw new Error(`HTTP ${res.status}`);
+// setActionLoadingId(r.id);
+
+// const res = await API(
+// AuthService.getToken(),
+// 'POST',
+// `/Reservations/ChangeReservationStatus?id=${r.id}&newStatus=5`
+// );
+
+// if (!res.ok) {
+// const errorText = await res.text();
+// throw new Error(errorText || `HTTP ${res.status}`);
+// }
+
// toast.success('تم رفض الحجز');
// await loadReservations();
-// } catch (err) { console.error(err); toast.error('فشل رفض الحجز'); }
+// } catch (err) {
+// console.error(err);
+// toast.error('فشل رفض الحجز');
+// } finally {
+// setActionLoadingId(null);
+// }
// };
-// 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 allStatuses = [
+// ...new Set(reservations.map((r) => getStatusKey(r.status)).filter(Boolean)),
+// ];
-// if (loading) return
;
+// const counts = {
+// all: reservations.length,
+// ...Object.fromEntries(
+// allStatuses.map((s) => [
+// s,
+// reservations.filter((r) => getStatusKey(r.status) === s).length,
+// ])
+// ),
+// };
+
+// if (loading) {
+// return (
+//
+//
+//
+// );
+// }
// return (
//
//
-//
setSelected(null)} />
+
+// setSelected(null)}
+// />
+
//
-//
-// router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"> الرجوع
+//
+// router.back()}
+// className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"
+// >
+//
+// الرجوع
+//
+
//
//
-//
طلبات الحجز
+//
+// طلبات الحجز
+//
//
لديك {reservations.length} طلب
//
-//
+
+//
+//
+//
//
//
+
//
// {Object.entries(counts).map(([s, c]) => (
-//
setFilterStatus(s)}>
+// setFilterStatus(s)}
+// >
// {c}
-// {s==='all'?'الكل':(STATUS_UI[s]?.label||s)}
+//
+// {s === 'all' ? 'الكل' : STATUS_UI[s]?.label || s}
+//
//
// ))}
//
+
//
-//
-// 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"/>
+//
+
+// 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"
+// />
//
+
// {filtered.length === 0 ? (
//
-//
-//
لا توجد طلبات
-//
لم يتم استلام أي طلبات حجز حتى الآن
+//
+//
+// لا توجد طلبات
+//
+//
+// لم يتم استلام أي طلبات حجز حتى الآن
+//
//
// ) : (
//
-// {filtered.map(r => )}
+// {filtered.map((r) => (
+//
+// ))}
//
// )}
//
@@ -262,6 +642,8 @@
// }
+
+
'use client';
import { useState, useEffect, useCallback } from 'react';
@@ -280,6 +662,7 @@ import {
Home,
ArrowLeft,
RefreshCw,
+ Flag,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../../services/AuthService';
@@ -397,12 +780,114 @@ function buildImageUrl(path) {
return `${API_BASE}${path}`;
}
+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 OwnerCard({
r,
onViewDetails,
onConfirm,
onReject,
+ onReport,
actionLoadingId,
+ reportingId,
}) {
const p = r._prop;
const imgs = pImgs(p);
@@ -410,6 +895,7 @@ function OwnerCard({
const addr = pAddr(p);
const isPending = Number(r.status) === 0;
const isActionLoading = actionLoadingId === r.id;
+ const isReporting = reportingId === r.id;
return (
)}
+
+ onReport(r)}
+ 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 ? (
+
+ ) : (
+
+ )}
+ {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
+
);
}
-function DetailsModal({ r, isOpen, onClose }) {
+function DetailsModal({ r, isOpen, onClose, onReport, reportingId }) {
if (!isOpen || !r) return null;
const p = r._prop;
+ const isReporting = reportingId === r.id;
return (
{
+ if (isOpen) setMessage('');
+ }, [isOpen, reservation?.id]);
+
+ if (!isOpen || !reservation) return null;
+
+ return (
+
+ e.stopPropagation()}
+ >
+
+
+
+
الإبلاغ عن الحجز
+
رقم الحجز: #{reservation.id}
+
+
+
+
+
+
+
+
+
+ اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
+
+
+
+
+
+ );
+}
+
export default function OwnerReservationRequestsPage() {
const router = useRouter();
@@ -655,6 +1234,8 @@ export default function OwnerReservationRequestsPage() {
const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [actionLoadingId, setActionLoadingId] = useState(null);
+ const [reportDialog, setReportDialog] = useState({ open: false, reservation: null });
+ const [reportingId, setReportingId] = useState(null);
const loadReservations = useCallback(async () => {
try {
@@ -777,6 +1358,30 @@ export default function OwnerReservationRequestsPage() {
}
};
+ 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) {
+ console.error(err);
+ toast.error(err?.message || 'فشل إرسال البلاغ');
+ } finally {
+ setReportingId(null);
+ }
+ };
+
const allStatuses = [
...new Set(reservations.map((r) => getStatusKey(r.status)).filter(Boolean)),
];
@@ -807,6 +1412,15 @@ export default function OwnerReservationRequestsPage() {
r={selected}
isOpen={!!selected}
onClose={() => setSelected(null)}
+ onReport={openReportDialog}
+ reportingId={reportingId}
+ />
+
@@ -894,7 +1508,9 @@ export default function OwnerReservationRequestsPage() {
onViewDetails={setSelected}
onConfirm={handleConfirm}
onReject={handleReject}
+ onReport={openReportDialog}
actionLoadingId={actionLoadingId}
+ reportingId={reportingId}
/>
))}
diff --git a/app/reservations/page.js b/app/reservations/page.js
index 02e8d1a..6307b8d 100644
--- a/app/reservations/page.js
+++ b/app/reservations/page.js
@@ -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 (
+//
+// {statusLabel(code)}
+//
+// );
+// }
+
+// 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 انتهت المهلة;
+// 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 {pad(h)}:{pad(m)}:{pad(s)};
+// }
+
+// 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 (
+//
+//
+// {img &&
}
+//
+//
+//
+// {addr &&
{addr}
}
+//
+//
+//
{r.totalPrice?.toLocaleString() ?? '—'}
+//
السعر الإجمالي
+//
+//
+// {(beds||baths) &&
{beds>0&&{beds} غرف}{baths>0&&{baths} حمامات}
}
+//
+//
+//
من
+//
{new Date(r.startDate).toLocaleDateString('ar')}
+//
+//
+//
إلى
+//
{new Date(r.endDate).toLocaleDateString('ar')}
+//
+//
+// {isOwnerConfirmed && hasTimeWindow &&
+//
+// متبقي للدفع:
+//
+//
+//
مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}
+//
}
+//
+// 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">
+// التفاصيل
+//
+// {isOwnerConfirmed && !isExpired && 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 ? : } {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
+// }
+//
+// {canRate && !showRating &&
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">
+// قيّم هذا العقار
+// }
+// {canRate && showRating &&
+//
+// {[
+// { key: 'clean', label: 'النظافة' },
+// { key: 'services', label: 'الخدمات' },
+// { key: 'ownerBehavior', label: 'تعامل المالك' },
+// { key: 'experience', label: 'التجربة العامة' },
+// ].map(cat =>
+//
{cat.label}
+//
+// {[1,2,3,4,5].map(n => (
+// setRatings(p => ({...p, [cat.key]: n}))}
+// className={`p-0.5 rounded-full transition-colors ${n <= ratings[cat.key] ? 'text-amber-500' : 'text-gray-300'}`}>
+//
+//
+// ))}
+//
+//
)}
+//
+//
}
+//
+//
+// );
+// }
+
+// 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 (
+//
+// e.stopPropagation()}>
+//
+//
+//
تفاصيل الحجز
+//
+//
+//
رقم الحجز: #{r.id}
+//
+//
+// {p &&
+//
معلومات العقار
+//
العنوان: {propAddr(p, r)||'—'}
+// {(propBeds(p, r)||propBaths(p, r)) &&
+// {propBeds(p, r)>0&&{propBeds(p, r)} غرف}
+// {propBaths(p, r)>0&&{propBaths(p, r)} حمامات}
+//
}
+//
}
+//
+//
تفاصيل الحجز
+//
+//
تاريخ البداية
{new Date(r.startDate).toLocaleDateString('ar')}
+//
تاريخ النهاية
{new Date(r.endDate).toLocaleDateString('ar')}
+//
+//
تاريخ الإنشاء
{new Date(r.createdAt).toLocaleDateString('ar')}
+//
+//
+//
+//
المعلومات المالية
+//
الإجمالي{r.totalPrice?.toLocaleString()??'—'}
+//
+// {isOwnerConfirmed && hasTimeWindow &&
+//
+// متبقي للدفع:
+//
+//
+//
مدة الدفع: {formatWindowDuration(p.allowedPaymentPeriod)}
+// {!isExpired &&
{ 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 ? : } {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
+// }
+//
}
+//
+//
+//
+// );
+// }
+
+// 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
;
+
+// return (
+//
+//
+//
setSelected(null)} onPay={handlePay} payingId={payingId} />
+//
+//
+// router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"> الرجوع
+// حجوزاتي
+// لديك {reservations.length} حجز
+//
+//
+// {Object.entries(counts).map(([s, c]) => (
+//
setFilterStatus(s)}>
+// {c}
+// {s==='all'?'الكل':(STATUS_UI[s]?.label||s)}
+//
+// ))}
+//
+//
+//
+// 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"/>
+//
+// {filtered.length === 0 ? (
+//
+//
+//
لا توجد حجوزات
+//
لم تقم بأي حجز حتى الآن
+//
+// ) : (
+//
+// {filtered.map(r => )}
+//
+// )}
+//
+//
+// );
+// }
+
+
+
+
+
+
'use client';
import { useState, useEffect, useCallback } from 'react';
@@ -5,7 +400,7 @@ import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation';
import {
Calendar, Clock, CheckCircle, XCircle, Eye, Loader2, Search,
- MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star,
+ MapPin, DollarSign, Home, ArrowLeft, CreditCard, Timer, Star, Flag,
} from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
@@ -47,6 +442,106 @@ const propImages = (p, r) => {
const propBeds = (p, r) => p?.numberOfBedRooms ?? r?.property?.numberOfBedRooms ?? 0;
const propBaths = (p, r) => p?.numberOfBathRooms ?? r?.property?.numberOfBathRooms ?? 0;
+const getAuthToken = () => {
+ if (typeof window === 'undefined') return '';
+ return (
+ AuthService.getToken?.() ||
+ AuthService.getAccessToken?.() ||
+ localStorage.getItem('token') ||
+ localStorage.getItem('accessToken') ||
+ localStorage.getItem('authToken') ||
+ ''
+ );
+};
+
+const readStoredUser = () => {
+ if (typeof window === 'undefined') return null;
+ const keys = ['user', 'currentUser', 'authUser', 'profile'];
+ for (const key of keys) {
+ const raw = localStorage.getItem(key);
+ if (!raw) continue;
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return raw;
+ }
+ }
+ return null;
+};
+
+const extractNumericUserId = (value) => {
+ if (!value) return null;
+ if (typeof value === 'number') return Number.isInteger(value) ? value : null;
+ if (typeof value === 'string') {
+ const n = Number(value);
+ return Number.isInteger(n) ? n : null;
+ }
+ if (typeof value === 'object') {
+ const candidates = [
+ value.id,
+ value.userId,
+ value.userID,
+ value.user?.id,
+ value.user?.userId,
+ value.profile?.id,
+ value.profile?.userId,
+ value.data?.id,
+ ];
+ for (const candidate of candidates) {
+ const id = extractNumericUserId(candidate);
+ if (id !== null) return id;
+ }
+ }
+ return null;
+};
+
+async function reportReservation(reservationId, message) {
+ const user = AuthService.getUser?.() ?? readStoredUser();
+ const reporter = extractNumericUserId(user);
+ const rid = Number(reservationId);
+
+ if (!Number.isInteger(rid)) {
+ throw new Error('رقم الحجز غير صالح');
+ }
+ if (!Number.isInteger(reporter)) {
+ throw new Error('تعذر تحديد المستخدم الحالي');
+ }
+
+ const token = getAuthToken();
+ const res = await fetch(`${API_BASE}/ReservationReports/ReportReservation`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ body: JSON.stringify({
+ reservationId: rid,
+ message: message ?? null,
+ reporter,
+ }),
+ });
+
+ if (!res.ok) {
+ let errorMessage = 'فشل إرسال البلاغ';
+ try {
+ const data = await res.json();
+ errorMessage = data?.message || data?.title || errorMessage;
+ } catch (_) {
+ try {
+ const text = await res.text();
+ if (text) errorMessage = text;
+ } catch (_) {}
+ }
+ throw new Error(errorMessage);
+ }
+
+ try {
+ return await res.json();
+ } catch {
+ return null;
+ }
+}
+
function parseTimeSpan(str) {
if (!str) return 0;
const clean = str.replace(/-/g, '');
@@ -99,7 +594,81 @@ function CountdownTimer({ deadline }) {
return {pad(h)}:{pad(m)}:{pad(s)};
}
-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 (
+
+ e.stopPropagation()}
+ >
+
+
+
+
الإبلاغ عن الحجز
+
رقم الحجز: #{reservation.id}
+
+
+
+
+
+
+
+
+
+ اخبر فريق الدعم بما حدث التفاصيل الواضحة تساعدنا على مراجعة هذا الحجز بشكل اسرع
+
+
+
+
+
+ );
+}
+
+function ReservationCard({ r, onViewDetails, onPay, onReport, payingId, reportingId }) {
const p = r._prop;
const imgs = propImages(p, r);
const img = imgs.length > 0 ? `${API_BASE}${imgs[0]}` : null;
@@ -114,6 +683,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
: null;
const isExpired = deadline ? Date.now() > deadline : false;
const isPaying = payingId === r.id;
+ const isReporting = reportingId === r.id;
const [showRating, setShowRating] = useState(false);
const [ratings, setRatings] = useState({ clean: 0, services: 0, ownerBehavior: 0, experience: 0 });
const [ratingComment, setRatingComment] = useState('');
@@ -162,6 +732,10 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
{isPaying ? : } {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
}
+ 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 ? : } {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
+
{canRate && !showRating && 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">
قيّم هذا العقار
@@ -213,7 +787,7 @@ function ReservationCard({ r, onViewDetails, onPay, payingId }) {
);
}
-function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
+function DetailsModal({ r, isOpen, onClose, onPay, onReport, payingId, reportingId }) {
if (!isOpen || !r) return null;
const p = r._prop;
const isOwnerConfirmed = STATUS_MAP[r.status] === 'ownerConfirmed';
@@ -223,6 +797,7 @@ function DetailsModal({ r, isOpen, onClose, onPay, payingId }) {
: null;
const isExpired = deadline ? Date.now() > deadline : false;
const isPaying = payingId === r.id;
+ const isReporting = reportingId === r.id;
return (
: } {isPaying ? 'جاري الدفع...' : 'ادفع الآن'}
}
}
+ { 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 ? : } {isReporting ? 'جاري الإبلاغ...' : 'إبلاغ'}
+
@@ -284,6 +863,8 @@ export default function UserReservationsPage() {
const [filterStatus, setFilterStatus] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [payingId, setPayingId] = useState(null);
+ const [reportDialog, setReportDialog] = useState({ open: false, reservation: null });
+ const [reportingId, setReportingId] = useState(null);
useEffect(() => { if (!AuthService.getUser()) { router.push('/login'); return; } loadReservations(); }, [router]);
@@ -320,7 +901,10 @@ export default function UserReservationsPage() {
useEffect(() => {
let r = reservations;
if (filterStatus !== 'all') r = r.filter(x => STATUS_MAP[x.status] === filterStatus);
- if (searchTerm) { const q = searchTerm.toLowerCase(); r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q)); }
+ if (searchTerm) {
+ const q = searchTerm.toLowerCase();
+ r = r.filter(x => propAddr(x._prop, x).toLowerCase().includes(q) || String(x.id).includes(q));
+ }
setFiltered(r);
}, [reservations, filterStatus, searchTerm]);
@@ -345,12 +929,50 @@ export default function UserReservationsPage() {
}
};
+ const openReportDialog = (r) => {
+ setReportDialog({ open: true, reservation: r });
+ };
+
+ const closeReportDialog = () => {
+ setReportDialog({ open: false, reservation: null });
+ };
+
+ const handleSubmitReport = async (message) => {
+ if (!reportDialog.reservation) return;
+
+ setReportingId(reportDialog.reservation.id);
+ try {
+ await reportReservation(reportDialog.reservation.id, message.trim() || null);
+ toast.success('تم إرسال البلاغ بنجاح');
+ closeReportDialog();
+ } catch (err) {
+ toast.error(err?.message || 'فشل إرسال البلاغ');
+ } finally {
+ setReportingId(null);
+ }
+ };
+
if (loading) return
;
return (
-
setSelected(null)} onPay={handlePay} payingId={payingId} />
+ setSelected(null)}
+ onPay={handlePay}
+ onReport={openReportDialog}
+ payingId={payingId}
+ reportingId={reportingId}
+ />
+
router.back()} className="flex items-center gap-2 text-gray-600 hover:text-amber-600 mb-4"> الرجوع
@@ -380,7 +1002,17 @@ export default function UserReservationsPage() {
) : (
- {filtered.map(r => )}
+ {filtered.map(r => (
+
+ ))}
)}