Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m56s
All checks were successful
Build frontend / build (push) Successful in 1m56s
This commit is contained in:
216
app/components/ratings/RatingForm.js
Normal file
216
app/components/ratings/RatingForm.js
Normal 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;
|
||||||
149
app/components/ratings/RatingList.js
Normal file
149
app/components/ratings/RatingList.js
Normal 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;
|
||||||
90
app/components/ratings/StarRating.js
Normal file
90
app/components/ratings/StarRating.js
Normal 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 'ضعيف';
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
193
app/utils/ratings.js
Normal file
193
app/utils/ratings.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user