Compare commits

..

13 Commits

Author SHA1 Message Date
5d3ead55ca Added API for rating
All checks were successful
Build frontend / build (push) Successful in 54s
2026-04-26 13:46:30 +03:00
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
11 changed files with 3462 additions and 2633 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,317 @@
// 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;
'use client';
import { useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import toast from 'react-hot-toast';
import StarRating from './StarRating';
import { addPropertyRating } from '../../utils/ratings';
const RatingField = ({ label, value, onChange }) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
<StarRating rating={value} onRatingChange={onChange} size={28} />
{value === 0 && <p className="text-xs text-red-500">مطلوب</p>}
</div>
);
export default function PropertyRatingForm({ reservationId, onSuccess, onCancel }) {
const [cleanRating, setCleanRating] = useState(0);
const [servicesRating, setServicesRating] = useState(0);
const [ownerBehaviorRating, setOwnerBehaviorRating] = useState(0);
const [experienceRating, setExperienceRating] = useState(0);
const [comment, setComment] = useState('');
const [loading, setLoading] = useState(false);
const validate = () => {
if (cleanRating === 0) return 'نظافة العقار';
if (servicesRating === 0) return 'جودة الخدمات';
if (ownerBehaviorRating === 0) return 'سلوك المالك';
if (experienceRating === 0) return 'التجربة العامة';
return null;
};
const handleSubmit = async (e) => {
e.preventDefault();
const missing = validate();
if (missing) {
toast.error(`يرجى تقييم: ${missing}`);
return;
}
setLoading(true);
try {
await addPropertyRating({
reservationId,
cleanRating,
servicesRating,
ownerBehaviorRating,
experienceRating,
comment: comment.trim() || null,
});
toast.success('تم إرسال التقييم بنجاح!');
onSuccess?.();
} catch (err) {
console.error(err);
toast.error('حدث خطأ، حاول مرة أخرى');
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900">تقييم العقار</h3>
{onCancel && (
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
<X size={20} />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<RatingField label="نظافة العقار" value={cleanRating} onChange={setCleanRating} />
<RatingField label="جودة الخدمات" value={servicesRating} onChange={setServicesRating} />
<RatingField label="سلوك المالك" value={ownerBehaviorRating} onChange={setOwnerBehaviorRating} />
<RatingField label="التجربة العامة" value={experienceRating} onChange={setExperienceRating} />
<div>
<label className="block text-sm font-medium text-gray-700">تعليق (اختياري)</label>
<textarea
rows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
placeholder="شارك تجربتك..."
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 rounded-xl transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? 'جاري الإرسال...' : 'إرسال التقييم'}
</button>
</form>
</div>
);
}

View File

@ -0,0 +1,286 @@
// '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;
'use client';
import { useState, useEffect } from 'react';
import { Star, User, Calendar, ChevronDown, Loader2 } from 'lucide-react';
import { getPropertyRatings, getPropertyAverageRating } from '../../utils/ratings';
const RatingItem = ({ rating }) => {
const overall = (
rating.cleanRating + rating.servicesRating +
rating.ownerBehaviorRating + rating.experienceRating
) / 4;
return (
<div className="border-b border-gray-100 py-4 last:border-0">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-amber-600" />
</div>
<span className="font-medium text-gray-800">{rating.customerName || 'مستأجر'}</span>
</div>
<div className="flex items-center gap-1 bg-amber-50 px-2 py-1 rounded-full">
<Star className="w-3 h-3 text-amber-500 fill-amber-500" />
<span className="text-sm font-bold">{overall.toFixed(1)}</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-600 mb-2">
<div>النظافة: {rating.cleanRating}/5</div>
<div>الخدمات: {rating.servicesRating}/5</div>
<div>سلوك المالك: {rating.ownerBehaviorRating}/5</div>
<div>التجربة: {rating.experienceRating}/5</div>
</div>
{rating.comment && (
<p className="text-gray-700 text-sm mt-2 pr-4 border-r-2 border-amber-200">"{rating.comment}"</p>
)}
<div className="flex items-center gap-1 text-xs text-gray-400 mt-2">
<Calendar className="w-3 h-3" />
<span>{new Date(rating.createdAt).toLocaleDateString('ar-SA')}</span>
</div>
</div>
);
};
export default function PropertyRatingList({ propertyId }) {
const [ratings, setRatings] = useState([]);
const [average, setAverage] = useState(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const fetchRatings = async (reset = false) => {
const currentPage = reset ? 1 : page;
try {
if (reset) setLoading(true);
else setLoadingMore(true);
const result = await getPropertyRatings(propertyId, currentPage, 10);
const items = result?.items || result?.data?.items || result || [];
const totalPages = result?.totalPages || result?.data?.totalPages || 1;
setRatings(prev => reset ? items : [...prev, ...items]);
setHasMore(currentPage < totalPages);
if (reset) setPage(1);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
setLoadingMore(false);
}
};
const fetchAverage = async () => {
try {
const avg = await getPropertyAverageRating(propertyId);
setAverage(avg);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
if (propertyId) {
fetchRatings(true);
fetchAverage();
}
}, [propertyId]);
const loadMore = () => {
const nextPage = page + 1;
setPage(nextPage);
fetchRatings(false);
};
if (loading && ratings.length === 0) {
return (
<div className="text-center py-8">
<Loader2 className="w-8 h-8 animate-spin text-amber-500 mx-auto" />
<p className="text-gray-500 mt-2">جاري تحميل التقييمات...</p>
</div>
);
}
return (
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<div className="flex justify-between items-center mb-4 pb-3 border-b border-gray-100">
<h3 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h3>
{average !== null && (
<div className="flex items-center gap-1 bg-amber-50 px-3 py-1 rounded-full">
<Star className="w-5 h-5 text-amber-500 fill-amber-500" />
<span className="font-bold text-lg">{average.toFixed(1)}</span>
<span className="text-gray-500 text-sm">/5</span>
</div>
)}
</div>
{ratings.length === 0 ? (
<p className="text-center text-gray-500 py-6">لا توجد تقييمات حتى الآن. كن أول من يقيم هذا العقار.</p>
) : (
<>
{ratings.map((r, idx) => <RatingItem key={idx} rating={r} />)}
{hasMore && (
<button
onClick={loadMore}
disabled={loadingMore}
className="w-full mt-4 py-2 text-amber-600 hover:text-amber-700 font-medium text-sm flex items-center justify-center gap-1"
>
{loadingMore ? <Loader2 className="w-4 h-4 animate-spin" /> : <ChevronDown className="w-4 h-4" />}
{loadingMore ? 'جاري التحميل...' : 'عرض المزيد'}
</button>
)}
</>
)}
</div>
);
}

View File

@ -1,216 +0,0 @@
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

@ -1,149 +0,0 @@
'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

@ -1,90 +1,134 @@
// 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 'ضعيف';
// }
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion';
import { Star } from 'lucide-react'; import { Star } from 'lucide-react';
const StarRating = ({ const StarRating = ({ rating = 0, onRatingChange, size = 28, readOnly = false }) => {
rating, const [hoverRating, setHoverRating] = useState(0);
onRatingChange,
maxStars = 5,
size = 24,
color = '#ffc107',
readOnly = false,
className = ''
}) => {
const [hoverRating, setHoverRating] = useState(null);
const handleClick = (value) => { const handleClick = (value) => {
if (!readOnly && onRatingChange) { if (!readOnly && onRatingChange) onRatingChange(value);
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 ( return (
<div className={`flex gap-1 ${className}`} onMouseLeave={handleMouseLeave}> <div
{[...Array(maxStars)].map((_, index) => ( className="flex gap-1"
<motion.div onMouseLeave={() => !readOnly && setHoverRating(0)}
key={index} >
whileHover={{ scale: readOnly ? 1 : 1.1 }} {[1, 2, 3, 4, 5].map((star) => {
onClick={() => handleClick(index + 1)} const filled = (hoverRating || rating) >= star;
onMouseEnter={() => handleMouseEnter(index + 1)} return (
> <button
{getStarIcon(index)} key={star}
</motion.div> type="button"
))} onClick={() => handleClick(star)}
onMouseEnter={() => !readOnly && setHoverRating(star)}
className="focus:outline-none transition-transform hover:scale-110"
disabled={readOnly}
>
<Star
size={size}
className={`${filled ? 'fill-amber-500 text-amber-500' : 'text-gray-300 fill-transparent'}`}
/>
</button>
);
})}
</div> </div>
); );
}; };
export default StarRating; 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 'ضعيف';
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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) {
@ -370,3 +370,81 @@ export async function removeFavoriteProperty(favePropId) {
export async function getUserNotifications() { export async function getUserNotifications() {
return apiFetch('/Notifications/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 }),
});
}

View File

@ -1,193 +1,292 @@
// Rating API endpoints for SweetHome // // Rating API endpoints for SweetHome
// Handles both customer ratings and property ratings // // 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;
// }
// }
// utils/ratings.js
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';
/**
* 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 = {}) { async function apiFetch(endpoint, options = {}) {
const token = AuthService.getToken(); const token = AuthService.getToken();
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }), ...(token && { Authorization: `Bearer ${token}` }),
...options.headers, ...options.headers,
}; };
console.log('[Rating API] Request:', options.method || 'GET', `${API_BASE}${endpoint}`); const response = await fetch(`${API_BASE}${endpoint}`, {
const res = await fetch(`${API_BASE}${endpoint}`, {
...options, ...options,
headers, headers,
}); });
console.log('[Rating API] Response:', res.status, endpoint); if (!response.ok && response.status !== 206) {
const errorText = await response.text().catch(() => '');
if (!res.ok && res.status !== 206) { throw new Error(`API Error ${response.status}: ${errorText}`);
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(); const text = await response.text();
if (!text) return null; if (!text) return null;
try { try {
const json = JSON.parse(text); const json = JSON.parse(text);
if (json && typeof json === 'object' && 'data' in json) { return json && typeof json === 'object' && 'data' in json ? json.data : json;
return json.data;
}
return json;
} catch { } catch {
return text; return text;
} }
} }
/**
* POST /Rating/AddPropertyRating
* @param {Object} data - { reservationId, cleanRating, servicesRating, ownerBehaviorRating, experienceRating, comment? }
*/
export async function addPropertyRating(data) {
return apiFetch('/Rating/AddPropertyRating', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* POST /Rating/AddCustomerRating
* @param {Object} data - { reservationId, furnitureIntegrityRating, termsComplianceRating, renterBehaviorRating, comment? }
*/
export async function addCustomerRating(data) {
return apiFetch('/Rating/AddCustomerRating', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* GET /Rating/GetPropertyRatings
* @param {number} propertyId
* @param {number} page - default 1
* @param {number} pageSize - default 10
* @returns {Promise<{ items: Array, totalPages: number, currentPage: number }>}
*/
export async function getPropertyRatings(propertyId, page = 1, pageSize = 10) {
const query = new URLSearchParams({
propertyId: String(propertyId),
page: String(page),
pageSize: String(pageSize),
}).toString();
return apiFetch(`/Rating/GetPropertyRatings?${query}`);
}
/**
* GET /Rating/GetCustomerRatings
* @param {number} renterId
* @param {number} page
* @param {number} pageSize
*/
export async function getCustomerRatings(renterId, page = 1, pageSize = 10) {
const query = new URLSearchParams({
renterId: String(renterId),
page: String(page),
pageSize: String(pageSize),
}).toString();
return apiFetch(`/Rating/GetCustomerRatings?${query}`);
}
/**
* GET /Rating/GetPropertyAverage
* @param {number} propertyId
* @returns {Promise<number>} average rating (0 if none)
*/
export async function getPropertyAverageRating(propertyId) {
const result = await apiFetch(`/Rating/GetPropertyAverage?propertyId=${propertyId}`);
if (typeof result === 'number') return result;
if (result && typeof result.average === 'number') return result.average;
return 0;
}