Compare commits
15 Commits
db949aaeba
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d3ead55ca | |||
| 97126c5776 | |||
| 1e167c447a | |||
| dd0a9c401d | |||
| 32f6c7af5a | |||
| 7e0d5eaf8d | |||
| beccd8b24f | |||
| 7e9a9d79f2 | |||
| 39f494aecb | |||
| 485baffdc2 | |||
| c46173d7c6 | |||
| 04fa34107b | |||
| 5a7d0ef265 | |||
| 0ba435fd7e | |||
| 9c2a748ae9 |
@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { NavLink, MobileNavLink } from "./components/NavLinks";
|
||||
import { FavoritesProvider } from '@/app/contexts/FavoritesContext';
|
||||
import { NotificationsProvider } from '@/app/contexts/NotificationsContext';
|
||||
import FloatingSidebar from '@/app/components/FloatingSidebar';
|
||||
import {
|
||||
Globe,
|
||||
@ -708,10 +709,12 @@ export default function ClientLayout({ children }) {
|
||||
<main
|
||||
className={`${!isAuthPage && !isProfilePage ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
||||
>
|
||||
<FavoritesProvider>
|
||||
{children}
|
||||
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
|
||||
</FavoritesProvider>
|
||||
<NotificationsProvider>
|
||||
<FavoritesProvider>
|
||||
{children}
|
||||
<FloatingSidebar isRTL={currentLanguage === 'ar'} isAdmin={isAdmin} />
|
||||
</FavoritesProvider>
|
||||
</NotificationsProvider>
|
||||
</main>
|
||||
|
||||
{!isAuthPage && !isProfilePage && (
|
||||
|
||||
@ -5,9 +5,11 @@ import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
|
||||
import { useFavorites } from '@/app/contexts/FavoritesContext';
|
||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||
|
||||
export default function FloatingSidebar({ isRTL, isAdmin }) {
|
||||
const { favorites } = useFavorites();
|
||||
const { unreadCount } = useNotifications();
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
let timeoutId = null;
|
||||
|
||||
@ -150,13 +152,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
|
||||
>
|
||||
<div className="relative">
|
||||
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
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 > 0 && (
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
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"
|
||||
>
|
||||
{unreadCount}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{renderTooltip('notifications', 'الإشعارات')}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
317
app/components/ratings/PropertyRatingForm.js
Normal file
317
app/components/ratings/PropertyRatingForm.js
Normal 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>
|
||||
);
|
||||
}
|
||||
286
app/components/ratings/PropertyRatingList.js
Normal file
286
app/components/ratings/PropertyRatingList.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 { 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 StarRating = ({ rating = 0, onRatingChange, size = 28, readOnly = false }) => {
|
||||
const [hoverRating, setHoverRating] = useState(0);
|
||||
|
||||
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`} />;
|
||||
if (!readOnly && onRatingChange) onRatingChange(value);
|
||||
};
|
||||
|
||||
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
|
||||
className="flex gap-1"
|
||||
onMouseLeave={() => !readOnly && setHoverRating(0)}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((star) => {
|
||||
const filled = (hoverRating || rating) >= star;
|
||||
return (
|
||||
<button
|
||||
key={star}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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 'ضعيف';
|
||||
}
|
||||
export default StarRating;
|
||||
75
app/contexts/NotificationsContext.js
Normal file
75
app/contexts/NotificationsContext.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -4,71 +4,28 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Bell, CheckCircle, XCircle, Calendar, MessageCircle } from 'lucide-react';
|
||||
import AuthService from '@/app/services/AuthService';
|
||||
|
||||
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'
|
||||
}
|
||||
];
|
||||
import { useNotifications } from '@/app/contexts/NotificationsContext';
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const router = useRouter();
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { notifications, unreadCount, isLoading } = useNotifications();
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (AuthService.isAdmin()) {
|
||||
router.push('/');
|
||||
if (!AuthService.isAuthenticated()) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
setNotifications(mockNotifications);
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
}, [router]);
|
||||
|
||||
const markAsRead = (id) => {
|
||||
setNotifications(prev =>
|
||||
prev.map(n => (n.id === id ? { ...n, read: true } : n))
|
||||
);
|
||||
// This will be handled by context if needed
|
||||
};
|
||||
|
||||
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) {
|
||||
return (
|
||||
<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 (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
@ -100,30 +69,31 @@ export default function NotificationsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{notifications.map((notification) => {
|
||||
const Icon = notification.icon;
|
||||
return (
|
||||
<div
|
||||
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 ${notification.bgColor} rounded-full flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon className={`w-6 h-6 ${notification.color}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900">{notification.title}</h3>
|
||||
{notifications.map((notification, index) => (
|
||||
<div
|
||||
key={index}
|
||||
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">
|
||||
<Bell className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900">{notification.title}</h3>
|
||||
{notification.message && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
* @returns {string[]}
|
||||
|
||||
@ -134,7 +134,7 @@ export async function getAvailableDateRanges(propertyId) {
|
||||
}
|
||||
|
||||
export async function getReservations() {
|
||||
return apiFetch('/Reservations/GetReservations');
|
||||
return apiFetch('/Reservations/GetAllReservations');
|
||||
}
|
||||
|
||||
export async function getReservation(id) {
|
||||
@ -366,3 +366,85 @@ export async function addFavoriteProperty(propId) {
|
||||
export async function removeFavoriteProperty(favePropId) {
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,193 +1,292 @@
|
||||
// Rating API endpoints for SweetHome
|
||||
// Handles both customer ratings and property ratings
|
||||
// // 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;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// utils/ratings.js
|
||||
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}`, {
|
||||
const response = 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}`);
|
||||
if (!response.ok && response.status !== 206) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
throw new Error(`API Error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const text = await response.text();
|
||||
if (!text) return null;
|
||||
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (json && typeof json === 'object' && 'data' in json) {
|
||||
return json.data;
|
||||
}
|
||||
return json;
|
||||
return json && typeof json === 'object' && 'data' in json ? json.data : json;
|
||||
} catch {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user