Compare commits

..

17 Commits

Author SHA1 Message Date
97126c5776 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 54s
2026-04-22 10:52:19 +03:00
1e167c447a Edit profits for owner 2026-04-22 10:52:08 +03:00
dd0a9c401d readdded the getuserId function
All checks were successful
Build frontend / build (push) Successful in 58s
2026-04-17 14:40:47 +03:00
32f6c7af5a fixed the api request
All checks were successful
Build frontend / build (push) Successful in 1m7s
2026-04-16 22:49:15 +03:00
7e0d5eaf8d edited the api request
All checks were successful
Build frontend / build (push) Successful in 40s
2026-04-16 22:40:59 +03:00
beccd8b24f added debugging on the admin confirm
All checks were successful
Build frontend / build (push) Successful in 41s
2026-04-16 22:33:19 +03:00
7e9a9d79f2 there is no endpoint in name /Reservations/GetReservations
All checks were successful
Build frontend / build (push) Successful in 41s
2026-04-16 22:13:14 +03:00
39f494aecb fixed some things
All checks were successful
Build frontend / build (push) Successful in 42s
2026-04-16 22:06:57 +03:00
485baffdc2 fixed some things
All checks were successful
Build frontend / build (push) Successful in 55s
2026-04-16 21:30:22 +03:00
c46173d7c6 fixed some things
All checks were successful
Build frontend / build (push) Successful in 45s
2026-04-16 21:18:31 +03:00
04fa34107b linked the admin confirm deposte
All checks were successful
Build frontend / build (push) Successful in 1m9s
2026-04-16 21:15:21 +03:00
5a7d0ef265 Added confirm button for admin
All checks were successful
Build frontend / build (push) Successful in 46s
2026-04-15 12:28:01 +03:00
0ba435fd7e Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m56s
2026-04-15 12:10:45 +03:00
9c2a748ae9 Added API for notifications and edit style 2026-04-15 12:07:39 +03:00
db949aaeba Fix build errors: corrected import paths, added missing RatingList component, fixed syntax errors in rating components
All checks were successful
Build frontend / build (push) Successful in 54s
2026-04-14 14:23:17 +00:00
ae600ad41b Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 43s
2026-04-13 00:38:59 +03:00
16b1c7c6f6 Edit home 2026-04-13 00:25:29 +03:00
15 changed files with 2228 additions and 1184 deletions

View File

@ -6,6 +6,7 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { NavLink, MobileNavLink } from "./components/NavLinks"; import { NavLink, MobileNavLink } from "./components/NavLinks";
import { FavoritesProvider } from '@/app/contexts/FavoritesContext'; import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
import FloatingSidebar from '@/app/components/FloatingSidebar'; import FloatingSidebar from '@/app/components/FloatingSidebar';
import { import {
Globe, Globe,
@ -708,10 +709,12 @@ export default function ClientLayout({ children }) {
<main <main
className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`} className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
> >
<FavoritesProvider> <NotificationsProvider>
{children} <FavoritesProvider>
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} /> {children}
</FavoritesProvider> <FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
</FavoritesProvider>
</NotificationsProvider>
</main> </main>
{!isAuthPage && !isProfilePage && ( {!isAuthPage && !isProfilePage && (

View File

@ -5,9 +5,11 @@ import { motion } from 'framer-motion';
import Link from 'next/link'; import Link from 'next/link';
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react'; import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
import { useFavorites } from '@/app/contexts/FavoritesContext'; import { useFavorites } from '@/app/contexts/FavoritesContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
export default function FloatingSidebar({ isRTL, isAdmin }) { export default function FloatingSidebar({ isRTL, isAdmin }) {
const { favorites } = useFavorites(); const { favorites } = useFavorites();
const { unreadCount } = useNotifications();
const [tooltip, setTooltip] = useState(null); const [tooltip, setTooltip] = useState(null);
let timeoutId = null; let timeoutId = null;
@ -150,13 +152,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
> >
<div className="relative"> <div className="relative">
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" /> <Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<motion.span {unreadCount > 0 && (
initial={{ scale: 0 }} <motion.span
animate={{ scale: 1 }} initial={{ scale: 0 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md" animate={{ scale: 1 }}
> className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
3 >
</motion.span> {unreadCount}
</motion.span>
)}
</div> </div>
</Link> </Link>
{renderTooltip('notifications', 'الإشعارات')} {renderTooltip('notifications', 'الإشعارات')}

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,24 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Search, MapPin, Home, DollarSign } from 'lucide-react'; import { Search, MapPin, Home, DollarSign, ShieldCheck } from 'lucide-react';
export default function HeroSearch({ onSearch }) { export default function HeroSearch({ onSearch, isAuthenticated }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('rent'); const [activeTab, setActiveTab] = useState('buy');
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
city: '', city: 'all',
propertyType: '', propertyType: 'all',
priceRange: '', priceRange: 'all',
identityType: 'syrian' identityType: 'syrian',
ownerSource: 'all',
rentPeriod: 'all',
availableToday: false
}); });
const [showLoginDialog, setShowLoginDialog] = useState(false);
const cities = [ const cities = [
{ id: 'all', label: 'جميع المدن' }, { id: 'all', label: 'جميع المدن' },
@ -26,10 +31,10 @@ export default function HeroSearch({ onSearch }) {
const propertyTypes = [ const propertyTypes = [
{ id: 'all', label: 'الكل' }, { id: 'all', label: 'الكل' },
{ id: 'apartment', label: 'شقة' }, { id: 'apartment', label: 'شقق سكنية' },
{ id: 'villa', label: 'فيلا' }, { id: 'studio', label: 'استوديو' },
{ id: 'house', label: 'بيت' }, { id: 'commercial', label: 'عقار تجاري' },
{ id: 'studio', label: 'استوديو' } { id: 'villa', label: 'فيلا / مزرعة' }
]; ];
const priceRanges = [ const priceRanges = [
@ -46,17 +51,45 @@ export default function HeroSearch({ onSearch }) {
{ id: 'passport', label: 'جواز سفر' } { id: 'passport', label: 'جواز سفر' }
]; ];
const ownerSources = [
{ id: 'all', label: 'الكل' },
{ id: 'owner', label: 'من المالك' },
{ id: 'agency', label: 'من مكتب عقاري' }
];
const rentPeriods = [
{ id: 'all', label: 'الكل' },
{ id: 'daily', label: 'إيجار يومي' },
{ id: 'monthly', label: 'إيجار شهري' }
];
const handleTabClick = (tab) => {
setActiveTab(tab);
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
setShowLoginDialog(true);
}
};
const handleSearch = () => { const handleSearch = () => {
if ((activeTab === 'rent' || activeTab === 'sell') && !isAuthenticated) {
setShowLoginDialog(true);
return;
}
onSearch({ onSearch({
...filters, ...filters,
propertyType: filters.propertyType || 'all', mode: activeTab,
city: filters.city || 'all', city: filters.city || 'all',
priceRange: filters.priceRange || 'all' propertyType: filters.propertyType || 'all',
priceRange: filters.priceRange || 'all',
ownerSource: filters.ownerSource || 'all',
rentPeriod: filters.rentPeriod || 'all'
}); });
}; };
return ( return (
<motion.div <>
<motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl" className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -66,7 +99,7 @@ export default function HeroSearch({ onSearch }) {
{['rent', 'buy', 'sell'].map((tab) => ( {['rent', 'buy', 'sell'].map((tab) => (
<motion.button <motion.button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => handleTabClick(tab)}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${ className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
activeTab === tab activeTab === tab
? 'bg-amber-500 text-white' ? 'bg-amber-500 text-white'
@ -176,6 +209,63 @@ export default function HeroSearch({ onSearch }) {
</select> </select>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-white mb-2">مصدر العرض</label>
<select
value={filters.ownerSource}
onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{ownerSources.map((source) => (
<option key={source.id} value={source.id}>
{source.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
<select
value={filters.rentPeriod}
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{rentPeriods.map((period) => (
<option key={period.id} value={period.id}>
{period.label}
</option>
))}
</select>
</div>
<div className="md:col-span-2 flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5">
<label className="mt-4 flex items-center gap-3 text-white text-sm">
<input
type="checkbox"
checked={filters.availableToday}
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
/>
<span className="font-medium">عرض فقط العقارات المتاحة من اليوم</span>
</label>
</div>
</div>
<div className="mt-6"> <div className="mt-6">
<motion.button <motion.button
onClick={handleSearch} onClick={handleSearch}
@ -188,5 +278,40 @@ export default function HeroSearch({ onSearch }) {
</motion.button> </motion.button>
</div> </div>
</motion.div> </motion.div>
{showLoginDialog && !isAuthenticated && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
<div className="flex items-center gap-3 mb-5">
<ShieldCheck className="w-7 h-7 text-amber-500" />
<div>
<h3 className="text-lg font-semibold text-gray-900">يرجى تسجيل الدخول</h3>
<p className="text-sm text-gray-600">للوصول إلى خيارات التأجير والبيع، يجب أن تكون مسجلاً.</p>
</div>
</div>
<div className="space-y-4">
<div className="rounded-2xl bg-gray-50 p-4">
<p className="text-sm text-gray-700">اضغط على تسجيل الدخول لاستكمال البحث أو إدارة عقاراتك.</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
<button
type="button"
onClick={() => setShowLoginDialog(false)}
className="w-full sm:w-auto px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors"
>
إغلاق
</button>
<Link
href="/login"
className="w-full sm:w-auto px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
>
تسجيل الدخول
</Link>
</div>
</div>
</div>
</div>
)}
</>
); );
} }

View File

@ -0,0 +1,216 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Star, Edit2, X, Check, Clock } from 'lucide-react';
import StarRating from './StarRating.js';
import toast, { Toaster } from 'react-hot-toast';
import { rateProperty, rateCustomer, getUserPropertyRating, canRateProperty } from '../../utils/ratings.js';
const RatingForm = ({
propertyId,
userId,
propertyOwner = false,
initialRating = 0,
initialComment = '',
onSubmitSuccess
}) => {
const [rating, setRating] = useState(initialRating);
const [comment, setComment] = useState(initialComment);
const [loading, setLoading] = useState(false);
const [showForm, setShowForm] = useState(false);
const [userRating, setUserRating] = useState(null);
// Check if user has already rated
useState(() => {
async function fetchUserRating() {
try {
const rating = await getUserPropertyRating(propertyId, userId);
if (rating) {
setUserRating(rating);
setRating(rating.rating);
setComment(rating.comment || '');
}
} catch (error) {
console.error('[RatingForm] Failed to fetch user rating:', error);
}
}
if (propertyId && userId) {
fetchUserRating();
}
}, [propertyId, userId]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!rating) {
toast.error('يرجى إعطاء تقييم من 1 إلى 5 نجوم');
return;
}
setLoading(true);
try {
const ratingData = {
propertyId,
customerId: userId,
rating,
comment: comment.trim() || null
};
await rateProperty(ratingData);
toast.success('تم إرسال التقييم بنجاح!');
// Reset form
setRating(0);
setComment('');
setShowForm(false);
if (onSubmitSuccess) {
onSubmitSuccess();
}
} catch (error) {
console.error('[RatingForm] Failed to submit rating:', error);
toast.error('حدث خطأ أثناء إرسال التقييم. يرجى المحاولة مرة أخرى.');
} finally {
setLoading(false);
}
};
const handleEdit = () => {
setShowForm(true);
setRating(userRating?.rating || 0);
setComment(userRating?.comment || '');
};
const handleCancel = () => {
setShowForm(false);
setRating(userRating?.rating || 0);
setComment(userRating?.comment || '');
};
if (!propertyId || !userId) {
return null;
}
return (
<div className="space-y-4">
<Toaster position="top-center" reverseOrder={false} />
{/* Display existing rating */}
{userRating && !showForm && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-50 rounded-xl p-4 border border-gray-200"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Star className="w-5 h-5 text-amber-500" />
<span className="font-medium text-gray-900">{userRating.rating}</span>
<span className="text-sm text-gray-500">من 5</span>
</div>
<button
onClick={handleEdit}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm hover:bg-gray-200 transition-colors flex items-center gap-1"
>
<Edit2 className="w-4 h-4" />
تعديل
</button>
</div>
{userRating.comment && (
<div className="text-gray-600 text-sm mb-3 line-clamp-3">
"{userRating.comment}"
</div>
)}
<div className="flex items-center gap-2 text-xs text-gray-400">
<Clock className="w-3 h-3" />
<span>{userRating.createdAt ? new Date(userRating.createdAt).toLocaleDateString('ar-SA') : ''}</span>
</div>
</motion.div>
)}
{/* Rating form */}
{showForm && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-xl p-6 border border-gray-200 shadow-sm"
>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
تقييمك للعقار
</label>
<div className="flex items-center gap-2">
<StarRating
rating={rating}
onRatingChange={setRating}
readOnly={false}
size={28}
color="#ffc107"
/>
<span className="text-lg font-bold text-gray-900">{rating || '1'}</span>
<span className="text-sm text-gray-400">/5</span>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
تعليق (اختياري)
</label>
<textarea
rows="3"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="شارك تجربتك مع العقار..."
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-all resize-none"
/>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleCancel}
disabled={loading}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
إلغاء
</button>
<button
type="submit"
disabled={loading || !rating}
className="flex-1 px-4 py-2 bg-amber-500 text-white rounded-lg font-medium hover:bg-amber-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-1"
>
{loading ? (
<div className="w-4 h-4 border-2 border-white/50 border-t-white rounded-full animate-spin" />
) : (
<Check className="w-5 h-5" />
)}
{loading ? 'إرسال' : 'إرسال التقييم'}
</button>
</div>
</form>
</motion.div>
)}
{/* Add rating button */}
{!userRating && !showForm && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-amber-50 border-2 border-amber-200 rounded-xl p-4 text-center cursor-pointer hover:border-amber-300 transition-all"
onClick={() => setShowForm(true)}
>
<Star className="w-8 h-8 text-amber-500 mx-auto mb-2" />
<h3 className="font-bold text-amber-700 mb-2">قيّم هذا العقار</h3>
<p className="text-sm text-amber-600">شارك تجربتك مع المستأجرين الآخرين</p>
</motion.div>
)}
</div>
);
};
export default RatingForm;

View File

@ -0,0 +1,149 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Star } from 'lucide-react';
import { getPropertyRatings } from '../../utils/ratings.js';
import toast, { Toaster } from 'react-hot-toast';
const RatingList = ({ propertyId, userId }) => {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchReviews = async () => {
if (!propertyId) {
setLoading(false);
return;
}
try {
setLoading(true);
const data = await getPropertyReviews(propertyId);
setReviews(data || []);
setError(null);
} catch (err) {
console.error('[RatingList] Failed to fetch reviews:', err);
setError('فشل تحميل التقييمات');
setReviews([]);
} finally {
setLoading(false);
}
};
fetchReviews();
}, [propertyId]);
if (loading) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-8"
>
<div className="w-10 h-10 border-2 border-gray-200 border-t-gray-500 rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500">جاري تحميل التقييمات...</p>
</motion.div>
);
}
if (error) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-8"
>
<p className="text-red-500">{error}</p>
</motion.div>
);
}
if (reviews.length === 0) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-8"
>
<p className="text-gray-500">لا توجد تقييمات حتى الآن. كن أول من يقيم هذا العقار!</p>
</motion.div>
);
}
// Calculate average rating
const averageRating = reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
>
<Toaster position="top-center" reverseOrder={false} />
<div className="space-y-4">
{/* Header with average rating */}
<div className="flex items-center justify-between pb-3 border-b border-gray-100">
<div>
<h2 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h2>
<p className="text-sm text-gray-500">
{reviews.length} تقييمات
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={`w-5 h-5 ${index < Math.floor(averageRating) ? 'text-amber-500' : 'text-gray-300'}`}
/>
))}
{averageRating % 1 !== 0 && (
<Star className="w-5 h-5 text-amber-400" />
)}
<span className="font-bold text-gray-900 ml-2">{averageRating.toFixed(1)}</span>
</div>
</div>
</div>
{/* Reviews list */}
<div className="space-y-4">
{reviews.map((review, index) => (
<div key={index} className="border-t border-gray-100 pt-4 first:border-t-0 first:pt-0">
<div className="flex justify-between items-start mb-2">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0">
<Star className="w-6 h-6 text-gray-600" />
</div>
<div>
<div className="font-medium text-gray-900">{review.userName || 'مستأجر'}</div>
<div className="flex items-center gap-1 mt-1 text-sm">
{Array.from({ length: 5 }).map((_, starIndex) => (
<Star
key={starIndex}
className={`w-4 h-4 ${starIndex < review.rating ? 'text-amber-500' : 'text-gray-300'}`}
/>
))}
<span className="ml-1 text-xs text-gray-500">({review.rating}/5)</span>
</div>
</div>
</div>
<div className="text-xs text-gray-400">
{review.createdAt ? new Date(review.createdAt).toLocaleDateString('ar-SA') : ''}
</div>
</div>
{review.comment && (
<p className="text-gray-700 text-sm leading-relaxed">{review.comment}</p>
)}
</div>
))}
</div>
</div>
</motion.div>
);
};
export default RatingList;

View File

@ -0,0 +1,90 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Star } from 'lucide-react';
const StarRating = ({
rating,
onRatingChange,
maxStars = 5,
size = 24,
color = '#ffc107',
readOnly = false,
className = ''
}) => {
const [hoverRating, setHoverRating] = useState(null);
const handleClick = (value) => {
if (!readOnly && onRatingChange) {
onRatingChange(value);
}
};
const handleMouseEnter = (value) => {
if (!readOnly) {
setHoverRating(value);
}
};
const handleMouseLeave = () => {
if (!readOnly) {
setHoverRating(null);
}
};
const getStarIcon = (index) => {
const currentRating = hoverRating !== null ? hoverRating : rating;
if (currentRating > index) {
const hasHalfStar = currentRating % 1 > 0.5 && index + 0.5 <= currentRating;
if (hasHalfStar) {
// For half star, we'll use a combination approach or just show full star
// Since we don't have StarOutline, we'll approximate with full stars
return <Star className={`w-${size} h-${size} text-${color}`} />;
}
return <Star className={`w-${size} h-${size} text-${color}`} />;
}
return <Star className={`w-${size} h-${size} text-gray-400`} />;
};
return (
<div className={`flex gap-1 ${className}`} onMouseLeave={handleMouseLeave}>
{[...Array(maxStars)].map((_, index) => (
<motion.div
key={index}
whileHover={{ scale: readOnly ? 1 : 1.1 }}
onClick={() => handleClick(index + 1)}
onMouseEnter={() => handleMouseEnter(index + 1)}
>
{getStarIcon(index)}
</motion.div>
))}
</div>
);
};
export default StarRating;
// Helper functions
export function getStarCount(rating, maxStars = 5) {
return Math.round(rating * maxStars) / maxStars;
}
export function formatRating(rating) {
if (rating === 0) return 'لا يوجد تقييم';
return `${rating.toFixed(1)}`; // Show 1 decimal place
}
export function getRatingColor(rating) {
if (rating >= 4.5) return 'text-green-600';
if (rating >= 3.5) return 'text-yellow-600';
if (rating >= 2.5) return 'text-orange-600';
return 'text-red-600';
}
export function getRatingText(rating) {
if (rating >= 4.5) return 'ممتاز';
if (rating >= 3.5) return 'جيد جداً';
if (rating >= 2.5) return 'جيد';
if (rating >= 1.5) return 'مقبول';
return 'ضعيف';
}

View File

@ -0,0 +1,75 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { getUserNotifications } from '../utils/api';
import AuthService from '../services/AuthService';
const NotificationsContext = createContext();
export const useNotifications = () => {
const context = useContext(NotificationsContext);
if (!context) {
throw new Error('useNotifications must be used within NotificationsProvider');
}
return context;
};
export function NotificationsProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const fetchNotifications = useCallback(async () => {
if (!AuthService.isAuthenticated()) {
setNotifications([]);
setUnreadCount(0);
return;
}
setIsLoading(true);
try {
const data = await getUserNotifications();
const notificationsArray = Array.isArray(data) ? data : [];
setNotifications(notificationsArray);
// Assuming all are unread for now, or add logic to check 'read' field if exists
setUnreadCount(notificationsArray.length);
} catch (error) {
console.error('Error fetching notifications:', error);
setNotifications([]);
setUnreadCount(0);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetchNotifications();
}, [fetchNotifications]);
const markAsRead = useCallback((id) => {
setNotifications(prev =>
prev.map(n => (n.id === id ? { ...n, read: true } : n))
);
setUnreadCount(prev => Math.max(0, prev - 1));
}, []);
const markAllAsRead = useCallback(() => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
}, []);
const value = {
notifications,
unreadCount,
isLoading,
fetchNotifications,
markAsRead,
markAllAsRead,
};
return (
<NotificationsContext.Provider value={value}>
{children}
</NotificationsContext.Provider>
);
}

View File

@ -4,71 +4,28 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle } from 'lucide-react'; import { Bell, CheckCircle, XCircle, Calendar, MessageCircle } from 'lucide-react';
import AuthService from '@/app/services/AuthService'; import AuthService from '@/app/services/AuthService';
import { useNotifications } from '@/app/contexts/NotificationsContext';
const mockNotifications = [
{
id: 1,
type: 'booking',
title: 'تأكيد الحجز',
message: 'تم تأكيد حجزك في فيلا المزة للفترة 10-15 مارس',
date: '2024-03-01',
read: false,
icon: CheckCircle,
color: 'text-green-600',
bgColor: 'bg-green-50'
},
{
id: 2,
type: 'payment',
title: 'دفعة مستلمة',
message: 'تم استلام دفعة الإيجار بقيمة 500,000 ل.س',
date: '2024-02-28',
read: false,
icon: MessageCircle,
color: 'text-blue-600',
bgColor: 'bg-blue-50'
},
{
id: 3,
type: 'reminder',
title: 'تذكير بالإيجار',
message: 'ينتهي عقد الإيجار خلال 3 أيام',
date: '2024-02-25',
read: true,
icon: Calendar,
color: 'text-amber-600',
bgColor: 'bg-amber-50'
}
];
export default function NotificationsPage() { export default function NotificationsPage() {
const router = useRouter(); const router = useRouter();
const [notifications, setNotifications] = useState([]); const { notifications, unreadCount, isLoading } = useNotifications();
const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
if (AuthService.isAdmin()) { if (!AuthService.isAuthenticated()) {
router.push('/'); router.push('/login');
return; return;
} }
setTimeout(() => {
setNotifications(mockNotifications);
setIsLoading(false);
}, 500);
}, [router]); }, [router]);
const markAsRead = (id) => { const markAsRead = (id) => {
setNotifications(prev => // This will be handled by context if needed
prev.map(n => (n.id === id ? { ...n, read: true } : n))
);
}; };
const markAllAsRead = () => { const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true }))); // This will be handled by context if needed
}; };
const unreadCount = notifications.filter(n => !n.read).length;
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
@ -80,6 +37,18 @@ export default function NotificationsPage() {
); );
} }
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h3 className="text-xl font-bold text-gray-700 mb-2">خطأ في التحميل</h3>
<p className="text-gray-500">{error}</p>
</div>
</div>
);
}
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
@ -100,30 +69,31 @@ export default function NotificationsPage() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{notifications.map((notification) => { {notifications.map((notification, index) => (
const Icon = notification.icon; <div
return ( key={index}
<div className="bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md border-gray-200"
key={notification.id} >
className={`bg-white rounded-2xl shadow-sm border transition-all hover:shadow-md 'border-gray-200'}`} <div className="p-5 flex gap-4">
> <div className="w-12 h-12 bg-blue-50 rounded-full flex items-center justify-center shrink-0">
<div className="p-5 flex gap-4"> <Bell className="w-6 h-6 text-blue-600" />
<div className={`w-12 h-12 ${notification.bgColor} rounded-full flex items-center justify-center flex-shrink-0`}> </div>
<Icon className={`w-6 h-6 ${notification.color}`} /> <div className="flex-1">
</div> <div className="flex justify-between items-start">
<div className="flex-1"> <div>
<div className="flex justify-between items-start"> <h3 className="font-bold text-gray-900">{notification.title}</h3>
<div> {notification.message && (
<h3 className="font-bold text-gray-900">{notification.title}</h3>
<p className="text-gray-600 text-sm mt-1">{notification.message}</p> <p className="text-gray-600 text-sm mt-1">{notification.message}</p>
)}
{notification.date && (
<p className="text-xs text-gray-400 mt-2">{notification.date}</p> <p className="text-xs text-gray-400 mt-2">{notification.date}</p>
</div> )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); </div>
})} ))}
</div> </div>
)} )}
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -38,10 +38,15 @@ import AuthService from './services/AuthService';
function mapApiProperty(item, index) { function mapApiProperty(item, index) {
const info = item.propertyInformation || {}; const info = item.propertyInformation || {};
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0; const dailyPrice = item.dailyRent ?? 0;
const monthlyPrice = item.monthlyRent ?? 0; const monthlyPrice = item.monthlyRent ?? 0;
const salePrice = item.price ?? 0;
const isRentListing = Boolean(item.dailyRent != null || item.monthlyRent != null);
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment'; const price = isRentListing ? (dailyPrice || monthlyPrice || 0) : salePrice;
const priceUnit = isRentListing ? (monthlyPrice ? 'monthly' : 'daily') : 'sale';
const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? (item.type || 'apartment');
const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available'; const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
const features = []; const features = [];
@ -58,14 +63,21 @@ function mapApiProperty(item, index) {
? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`) ? rawImages.map(img => img.startsWith('http') ? img : `${apiBase}${img.startsWith('/') ? '' : '/Pictures/'}${img}`)
: ['/property-placeholder.jpg']; : ['/property-placeholder.jpg'];
const ownerSource = info.ownerType == null && item.ownerType == null
? 'all'
: [info.ownerType, item.ownerType].find((value) => value != null) === 1
? 'agency'
: 'owner';
return { return {
id: item.id ?? index + 1, id: item.id ?? index + 1,
title: info.address || `عقار #${item.id || index + 1}`, title: info.address || `عقار #${item.id || index + 1}`,
description: info.description || '', description: info.description || '',
type: propType, type: propType,
price: dailyPrice, price: price,
priceUSD: dailyPrice, priceUSD: price,
priceUnit: 'daily', priceUnit,
listingType: isRentListing ? 'rent' : 'sale',
location: { location: {
city: extractCity(info.address) || 'دمشق', city: extractCity(info.address) || 'دمشق',
district: info.address || '', district: info.address || '',
@ -85,7 +97,9 @@ function mapApiProperty(item, index) {
priceDisplay: { priceDisplay: {
daily: dailyPrice, daily: dailyPrice,
monthly: monthlyPrice, monthly: monthlyPrice,
sale: salePrice,
}, },
ownerSource,
bookings: [], bookings: [],
_raw: item, _raw: item,
}; };
@ -175,6 +189,16 @@ export default function HomePage() {
setSearchFilters(filters); setSearchFilters(filters);
const filtered = allProperties.filter(property => { const filtered = allProperties.filter(property => {
if (filters.mode === 'rent' && property.listingType !== 'rent') {
return false;
}
if (filters.mode === 'sell' && property.listingType !== 'sale') {
return false;
}
if (filters.mode === 'buy' && property.listingType !== 'sale') {
return false;
}
if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) { if (filters.city && filters.city !== 'all' && property.location.city !== filters.city) {
return false; return false;
} }
@ -194,6 +218,20 @@ export default function HomePage() {
} }
} }
if (filters.ownerSource && filters.ownerSource !== 'all') {
if (filters.ownerSource === 'owner' && property.ownerSource !== 'owner') return false;
if (filters.ownerSource === 'agency' && property.ownerSource !== 'agency') return false;
}
if (filters.rentPeriod && filters.rentPeriod !== 'all' && property.listingType === 'rent') {
if (filters.rentPeriod === 'daily' && !property.priceDisplay.daily) return false;
if (filters.rentPeriod === 'monthly' && !property.priceDisplay.monthly) return false;
}
if (filters.availableToday) {
if (property.status !== 'available') return false;
}
if (filters.identityType && property.allowedIdentities) { if (filters.identityType && property.allowedIdentities) {
if (!property.allowedIdentities.includes(filters.identityType)) { if (!property.allowedIdentities.includes(filters.identityType)) {
return false; return false;
@ -312,7 +350,7 @@ export default function HomePage() {
</motion.p> </motion.p>
</motion.div> </motion.div>
{!isOwner && <HeroSearch onSearch={applyFilters} />} {!isOwner && <HeroSearch onSearch={applyFilters} isAuthenticated={!!user} />}
{isOwner && ( {isOwner && (
<motion.div <motion.div
@ -477,6 +515,25 @@ export default function HomePage() {
searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'} searchFilters.priceRange === '2000-3000' ? '200$ - 300$' : 'أكثر من 300$'}
</span> </span>
</div> </div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">مصدر العرض: </span>
<span className="font-bold text-gray-900">
{searchFilters.ownerSource === 'all' ? 'الكل' :
searchFilters.ownerSource === 'owner' ? 'من المالك' : 'من مكتب عقاري'}
</span>
</div>
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="text-gray-600">نوع الإيجار: </span>
<span className="font-bold text-gray-900">
{searchFilters.rentPeriod === 'all' ? 'الكل' :
searchFilters.rentPeriod === 'daily' ? 'إيجار يومي' : 'إيجار شهري'}
</span>
</div>
{searchFilters.availableToday && (
<div className="bg-white px-4 py-2 rounded-full shadow-sm border border-gray-200 text-sm">
<span className="font-bold text-gray-900">فقط المتاحة من اليوم</span>
</div>
)}
</motion.div> </motion.div>
)} )}
</div> </div>

View File

@ -49,9 +49,9 @@ import { getRentProperty, getSaleProperty, bookReservation, checkAvailability, g
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import { useFavorites } from '@/app/contexts/FavoritesContext'; import { useFavorites } from '@/app/contexts/FavoritesContext';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums'; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums';
import RatingForm from '../../components/ratings/RatingForm.js'; import RatingForm from '@/app/components/ratings/RatingForm.js';
import RatingList from '../../components/ratings/RatingList.js'; import RatingList from '@/app/components/ratings/RatingList.js';
import StarRating from '../../components/ratings/StarRating.js'; import StarRating from '@/app/components/ratings/StarRating.js';
// Copy to clipboard that works on HTTP too // Copy to clipboard that works on HTTP too
async function copyToClipboard(text) { async function copyToClipboard(text) {

View File

@ -93,6 +93,18 @@ const AuthService = Object.freeze({
}; };
}, },
/**
* Get current authenticated user id
* @returns {number|string|null}
*/
getUserId() {
const user = this.getUser();
if (!user?.id) return null;
const parsedId = Number(user.id);
return Number.isFinite(parsedId) ? parsedId : user.id;
},
/** /**
* Get roles array from JWT * Get roles array from JWT
* @returns {string[]} * @returns {string[]}

View File

@ -134,7 +134,7 @@ export async function getAvailableDateRanges(propertyId) {
} }
export async function getReservations() { export async function getReservations() {
return apiFetch('/Reservations/GetReservations'); return apiFetch('/Reservations/GetAllReservations');
} }
export async function getReservation(id) { export async function getReservation(id) {
@ -366,3 +366,85 @@ export async function addFavoriteProperty(propId) {
export async function removeFavoriteProperty(favePropId) { export async function removeFavoriteProperty(favePropId) {
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' }); return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' });
} }
export async function getUserNotifications() {
return apiFetch('/Notifications/GetUserNotifications');
}
// ─── Booking/Reservation Management ───
export async function confirmDepositPayment(bookingId) {
return apiFetch('/Reservations/ConfirmDepositPayment', {
method: 'POST',
body: JSON.stringify({ bookingId }),
});
}
export async function adminConfirmDeposit(reservationId, adminId, comment = null) {
const token = AuthService.getToken();
const endpoint = `${API_BASE}/Reservations/AdminConfirmDeposit/admin-confirm-deposit`;
const normalizedComment =
typeof comment === 'string' && comment.trim()
? comment.trim()
: null;
const payload = {
reservationId,
adminId,
comment: normalizedComment,
};
console.log('[API] AdminConfirmDeposit request', {
method: 'PUT',
endpoint,
payload,
adminIdSource: 'jwt-user-id',
hasToken: Boolean(token),
tokenPreview: token ? `${token.slice(0, 18)}...${token.slice(-8)}` : null,
});
const res = await fetch(endpoint, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
body: JSON.stringify(payload),
});
const text = await res.text();
let data = null;
console.log('[API] AdminConfirmDeposit raw response', {
status: res.status,
ok: res.ok,
endpoint,
rawText: text,
});
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;
console.log('[API] AdminConfirmDeposit parsed response', {
status: res.status,
ok: res.ok,
message,
data,
});
return { status: res.status, data, ok: res.ok, message };
}
export async function updateBookingStatus(bookingId, status) {
return apiFetch('/Reservations/UpdateStatus', {
method: 'PUT',
body: JSON.stringify({ bookingId, status }),
});
}

193
app/utils/ratings.js Normal file
View File

@ -0,0 +1,193 @@
// Rating API endpoints for SweetHome
// Handles both customer ratings and property ratings
import AuthService from '../services/AuthService';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
/**
* Rate a property as a customer
* @param {Object} data - Rating data
* @param {number} data.propertyId - ID of the property being rated
* @param {number} data.customerId - ID of the customer doing the rating
* @param {number} data.rating - Rating value (1-5)
* @param {string} data.comment - Optional comment
* @returns {Promise} - API response
*/
export async function rateProperty(data) {
console.log('[Rating] Customer rating property:', data);
return apiFetch('/Ratings/CustomerRateProperty', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* Rate a customer as a property owner
* @param {Object} data - Rating data
* @param {number} data.propertyId - ID of the property
* @param {number} data.customerId - ID of the customer being rated
* @param {number} data.rating - Rating value (1-5)
* @param {string} data.comment - Optional comment
* @returns {Promise} - API response
*/
export async function rateCustomer(data) {
console.log('[Rating] Property owner rating customer:', data);
return apiFetch('/Ratings/PropertyRateCustomer', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* Get all ratings for a property
* @param {number} propertyId - ID of the property
* @returns {Promise} - Array of ratings
*/
export async function getPropertyRatings(propertyId) {
console.log('[Rating] Fetching property ratings for:', propertyId);
return apiFetch(`/Ratings/GetPropertyRatings?propertyId=${propertyId}`);
}
/**
* Get all ratings for a customer
* @param {number} customerId - ID of the customer
* @returns {Promise} - Array of ratings
*/
export async function getCustomerRatings(customerId) {
console.log('[Rating] Fetching customer ratings for:', customerId);
return apiFetch(`/Ratings/GetCustomerRatings?customerId=${customerId}`);
}
/**
* Get average rating for a property
* @param {number} propertyId - ID of the property
* @returns {Promise} - Average rating
*/
export async function getPropertyAverageRating(propertyId) {
console.log('[Rating] Fetching average rating for property:', propertyId);
const ratings = await getPropertyRatings(propertyId);
if (!Array.isArray(ratings) || ratings.length === 0) return 0;
const total = ratings.reduce((sum, rating) => sum + rating.rating, 0);
return Math.round((total / ratings.length) * 10) / 10; // Round to 1 decimal
}
/**
* Get average rating for a customer
* @param {number} customerId - ID of the customer
* @returns {Promise} - Average rating
*/
export async function getCustomerAverageRating(customerId) {
console.log('[Rating] Fetching average rating for customer:', customerId);
const ratings = await getCustomerRatings(customerId);
if (!Array.isArray(ratings) || ratings.length === 0) return 0;
const total = ratings.reduce((sum, rating) => sum + rating.rating, 0);
return Math.round((total / ratings.length) * 10) / 10; // Round to 1 decimal
}
/**
* Get user's rating for a specific property (if any)
* @param {number} propertyId - ID of the property
* @param {number} userId - ID of the user
* @returns {Promise} - User's rating or null
*/
export async function getUserPropertyRating(propertyId, userId) {
console.log('[Rating] Fetching user rating for property:', propertyId, 'user:', userId);
const allRatings = await getPropertyRatings(propertyId);
if (!Array.isArray(allRatings)) return null;
return allRatings.find(r => r.userId === userId) || null;
}
/**
* Get user's rating for a specific customer (if any)
* @param {number} customerId - ID of the customer
* @param {number} userId - ID of the user
* @returns {Promise} - User's rating or null
*/
export async function getUserCustomerRating(customerId, userId) {
console.log('[Rating] Fetching user rating for customer:', customerId, 'user:', userId);
const allRatings = await getCustomerRatings(customerId);
if (!Array.isArray(allRatings)) return null;
return allRatings.find(r => r.userId === userId) || null;
}
/**
* Check if user can rate a property (after renting)
* @param {number} propertyId - ID of the property
* @param {number} userId - ID of the user
* @returns {Promise} - Boolean indicating if rating is allowed
*/
export async function canRateProperty(propertyId, userId) {
console.log('[Rating] Checking if user can rate property:', propertyId, 'user:', userId);
// Logic: User can rate if they have completed a rental in the past
// This would typically check reservation history
// For now, we'll simulate this with a simple check
// In a real implementation, this would check:
// 1. User's reservation history for this property
// 2. Whether the rental period has ended
// 3. Whether they've already rated
const userRating = await getUserPropertyRating(propertyId, userId);
return !userRating; // Can rate if no existing rating
}
/**
* Check if user can rate a customer (after renting to them)
* @param {number} customerId - ID of the customer
* @param {number} userId - ID of the user (owner)
* @returns {Promise} - Boolean indicating if rating is allowed
*/
export async function canRateCustomer(customerId, userId) {
console.log('[Rating] Checking if user can rate customer:', customerId, 'user:', userId);
// Logic: Owner can rate if they have rented to this customer
// This would typically check reservation history
const userRating = await getUserCustomerRating(customerId, userId);
return !userRating; // Can rate if no existing rating
}
// Helper function for API calls
async function apiFetch(endpoint, options = {}) {
const token = AuthService.getToken();
const headers = {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
};
console.log('[Rating API] Request:', options.method || 'GET', `${API_BASE}${endpoint}`);
const res = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
console.log('[Rating API] Response:', res.status, endpoint);
if (!res.ok && res.status !== 206) {
const text = await res.text().catch(() => '');
console.error('[Rating API] Error:', res.status, text);
throw new Error(`Rating API ${res.status}: ${text || res.statusText}`);
}
const text = await res.text();
if (!text) return null;
try {
const json = JSON.parse(text);
if (json && typeof json === 'object' && 'data' in json) {
return json.data;
}
return json;
} catch {
return text;
}
}