detail of review

This commit is contained in:
hamid zarghami
2025-12-09 12:13:11 +03:30
parent f995d03b30
commit 4606a8b479
6 changed files with 360 additions and 2 deletions
+11 -1
View File
@@ -863,6 +863,16 @@
"review_list": "لیست نظرات",
"review_name": "نام نظر",
"review_email": "ایمیل نظر",
"review_comment": "نظر"
"review_comment": "نظر",
"great_taste": "طعم عالی",
"fast_delivery": "تحویل سریع",
"good_quality": "کیفیت خوب",
"good_portion_size": "اندازه مناسب پرس",
"friendly_service": "خدمات دوستانه",
"small_portion": "پرس کوچک",
"slow_delivery": "تحویل کند",
"poor_quality": "کیفیت پایین",
"overpriced": "قیمت بالا",
"unfriendly_service": "خدمات نامناسب"
}
}
+288
View File
@@ -0,0 +1,288 @@
import { type FC } from 'react'
import { useGetReviewById } from './hooks/useReviewData'
import { useParams } from 'react-router-dom'
import PageLoading from '@/components/PageLoading'
import Input from '@/components/Input'
import Status from '@/components/Status'
import { Star1, User, ShoppingCart, DocumentText, Like1, Dislike } from 'iconsax-react'
import { formatOptionalDate, formatPrice, formatFaNumber } from '@/helpers/func'
import { useTranslation } from 'react-i18next'
const DetailReview: FC = () => {
const { t } = useTranslation('global')
const { id } = useParams()
const { data: reviewData, isLoading } = useGetReviewById(id!)
if (isLoading) {
return (
<div className='mt-5 flex justify-center items-center min-h-[400px]'>
<PageLoading />
</div>
)
}
const review = reviewData?.data
if (!review) {
return (
<div className='mt-5'>
<div className='flex justify-between items-center'>
<h1 className='text-lg font-light'>جزئیات نظر</h1>
</div>
<div className='mt-8 text-center text-gray-500'>نظر یافت نشد</div>
</div>
)
}
const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
const variantMap: Record<string, 'success' | 'error' | 'warning' | 'info' | 'pending'> = {
'pending': 'pending',
'approved': 'success',
'rejected': 'error',
}
return variantMap[status] || 'pending'
}
const statusLabels: Record<string, string> = {
'pending': 'در انتظار',
'approved': 'تایید شده',
'rejected': 'رد شده',
}
const userName = [review.user.firstName, review.user.lastName].filter(Boolean).join(' ') || '-'
return (
<div className='mt-5'>
<div className='flex justify-between items-center mb-8'>
<h1 className='text-lg font-light'>جزئیات نظر</h1>
<Status
variant={getStatusVariant(review.status)}
label={statusLabels[review.status] || review.status}
/>
</div>
<div className='flex gap-7'>
{/* Main Content */}
<div className='flex-1 bg-white rounded-4xl p-8 space-y-6'>
{/* Rating Section */}
<div className='flex items-center gap-3 pb-6 border-b border-border'>
<div className='flex items-center gap-2'>
<Star1 size={24} color="#FFB800" variant="Bold" />
<span className='text-2xl font-semibold'>{review.rating}</span>
</div>
<span className='text-sm text-gray-500'>از ۵</span>
</div>
{/* Comment Section */}
<div>
<div className='flex items-center gap-2 mb-4'>
<DocumentText size={20} color="#8C90A3" />
<span className='text-sm font-medium'>نظر</span>
</div>
<div className='bg-gray-50 rounded-xl p-4 border border-border'>
<p className='text-sm leading-6 text-gray-700 whitespace-pre-wrap'>
{review.comment || 'نظری ثبت نشده است'}
</p>
</div>
</div>
{/* Positive Points */}
{review.positivePoints && review.positivePoints.length > 0 && (
<div>
<div className='flex items-center gap-2 mb-4'>
<Like1 size={20} color="#22C55E" />
<span className='text-sm font-medium'>نقاط مثبت</span>
</div>
<div className='flex flex-wrap gap-3'>
{review.positivePoints.map((point, index) => (
<span
key={index}
className='px-4 py-2 border border-green-500 text-green-600 rounded-xl text-sm bg-white'
>
{t(`reviews.${point}`)}
</span>
))}
</div>
</div>
)}
{/* Negative Points */}
{review.negativePoints && review.negativePoints.length > 0 && (
<div>
<div className='flex items-center gap-2 mb-4'>
<Dislike size={20} color="#EF4444" />
<span className='text-sm font-medium'>نقاط منفی</span>
</div>
<div className='flex flex-wrap gap-3'>
{review.negativePoints.map((point, index) => (
<span
key={index}
className='px-4 py-2 border border-red-500 text-red-600 rounded-xl text-sm bg-white'
>
{t(`reviews.${point}`)}
</span>
))}
</div>
</div>
)}
{/* Food Information */}
<div className='pt-6 border-t border-border'>
<div className='flex items-center gap-2 mb-4'>
<ShoppingCart size={20} color="#8C90A3" />
<span className='text-sm font-medium'>اطلاعات غذا</span>
</div>
<div className='grid grid-cols-2 gap-4'>
<Input
label='عنوان غذا'
value={review.food.title}
readOnly
/>
<Input
label='قیمت'
value={formatPrice(review.food.price)}
readOnly
/>
<Input
label='امتیاز غذا'
value={formatFaNumber(review.food.rate)}
readOnly
/>
<Input
label='دسته‌بندی'
value={review.food.category}
readOnly
/>
</div>
{review.food.images && review.food.images.length > 0 && (
<div className='mt-4'>
<span className='text-sm mb-2 block'>تصاویر غذا</span>
<div className='flex gap-3 flex-wrap'>
{review.food.images.map((image, index) => (
<img
key={index}
src={image}
alt={`غذا ${index + 1}`}
className='w-24 h-24 object-cover rounded-xl border border-border'
/>
))}
</div>
</div>
)}
</div>
</div>
{/* Sidebar */}
<div className='w-[330px] h-fit bg-white rounded-4xl p-8 flex flex-col gap-6'>
{/* User Information */}
<div>
<div className='flex items-center gap-2 mb-4'>
<User size={20} color="#8C90A3" />
<span className='text-sm font-medium'>اطلاعات کاربر</span>
</div>
<div className='space-y-4'>
<Input
label='نام و نام خانوادگی'
value={userName}
readOnly
/>
<Input
label='شماره تماس'
value={review.user.phone}
readOnly
/>
<Input
label='تاریخ تولد'
value={formatOptionalDate(review.user.birthDate)}
readOnly
/>
<Input
label='کیف پول'
value={formatPrice(review.user.wallet)}
readOnly
/>
<Input
label='امتیاز'
value={formatFaNumber(review.user.points)}
readOnly
/>
</div>
</div>
{/* Order Information */}
<div className='pt-6 border-t border-border'>
<div className='flex items-center gap-2 mb-4'>
<ShoppingCart size={20} color="#8C90A3" />
<span className='text-sm font-medium'>اطلاعات سفارش</span>
</div>
<div className='space-y-4'>
<Input
label='شماره سفارش'
value={formatFaNumber(review.order.orderNumber)}
readOnly
/>
<Input
label='تاریخ سفارش'
value={formatOptionalDate(review.order.createdAt, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
readOnly
/>
<Input
label='مبلغ کل'
value={formatPrice(review.order.total)}
readOnly
/>
<Input
label='وضعیت سفارش'
value={review.order.status}
readOnly
/>
<Input
label='وضعیت پرداخت'
value={review.order.paymentStatus}
readOnly
/>
</div>
</div>
{/* Review Metadata */}
<div className='pt-6 border-t border-border'>
<div className='space-y-4'>
<Input
label='تاریخ ثبت نظر'
value={formatOptionalDate(review.createdAt, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
readOnly
/>
{review.updatedAt && (
<Input
label='تاریخ آخرین بروزرسانی'
value={formatOptionalDate(review.updatedAt, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
readOnly
/>
)}
</div>
</div>
</div>
</div>
</div>
)
}
export default DetailReview
+7
View File
@@ -7,3 +7,10 @@ export const useGetReviews = (params?: Record<string, string | number>) => {
queryFn: () => api.getReviews(params),
});
};
export const useGetReviewById = (id: string) => {
return useQuery({
queryKey: ["review", id],
queryFn: () => api.getReviewById(id),
});
};
+10 -1
View File
@@ -1,5 +1,5 @@
import axios from "@/config/axios";
import type { GetReviewsResponse } from "../types/Types";
import type { GetReviewsResponse, GetReviewByIdResponse } from "../types/Types";
export const getReviews = async (
params?: Record<string, string | number>
@@ -9,3 +9,12 @@ export const getReviews = async (
});
return data;
};
export const getReviewById = async (
id: string
): Promise<GetReviewByIdResponse> => {
const { data } = await axios.get<GetReviewByIdResponse>(
`/admin/reviews/${id}`
);
return data;
};
+42
View File
@@ -49,6 +49,32 @@ export interface ReviewUser {
phone: string;
}
export interface ReviewOrder {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
user: string;
restaurant: string;
deliveryMethod: string;
address: string;
paymentMethod: string;
orderNumber: number;
couponDiscount: number;
couponDetail: unknown | null;
itemsDiscount: number;
totalDiscount: number;
subTotal: number;
tax: number;
deliveryFee: number;
total: number;
totalItems: number;
description: string | null;
tableNumber: string | null;
status: string;
paymentStatus: string;
}
export interface Review extends RowDataType {
id: string;
createdAt: string;
@@ -64,4 +90,20 @@ export interface Review extends RowDataType {
status: string;
}
export interface ReviewDetail {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
order: ReviewOrder;
food: ReviewFood;
user: ReviewUser;
comment: string;
rating: number;
positivePoints: string[];
negativePoints: string[];
status: string;
}
export type GetReviewsResponse = IResponse<Review[]>;
export type GetReviewByIdResponse = IResponse<ReviewDetail>;
+2
View File
@@ -37,6 +37,7 @@ import CouponsList from '@/pages/coupon/List'
import CreateCoupon from '@/pages/coupon/Create'
import UpdateCoupon from '@/pages/coupon/Update'
import ReviewsList from '@/pages/review/List'
import DetailReview from '@/pages/review/Detail'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
@@ -90,6 +91,7 @@ const MainRouter: FC = () => {
<Route path={Pages.coupons.update + ':id'} element={<UpdateCoupon />} />
<Route path={Pages.reviews.list} element={<ReviewsList />} />
<Route path={Pages.reviews.detail + ':id'} element={<DetailReview />} />
</Routes>
</div>
</div>