detail of review
This commit is contained in:
+11
-1
@@ -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": "خدمات نامناسب"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user