detail of review
This commit is contained in:
+11
-1
@@ -863,6 +863,16 @@
|
|||||||
"review_list": "لیست نظرات",
|
"review_list": "لیست نظرات",
|
||||||
"review_name": "نام نظر",
|
"review_name": "نام نظر",
|
||||||
"review_email": "ایمیل نظر",
|
"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),
|
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 axios from "@/config/axios";
|
||||||
import type { GetReviewsResponse } from "../types/Types";
|
import type { GetReviewsResponse, GetReviewByIdResponse } from "../types/Types";
|
||||||
|
|
||||||
export const getReviews = async (
|
export const getReviews = async (
|
||||||
params?: Record<string, string | number>
|
params?: Record<string, string | number>
|
||||||
@@ -9,3 +9,12 @@ export const getReviews = async (
|
|||||||
});
|
});
|
||||||
return data;
|
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;
|
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 {
|
export interface Review extends RowDataType {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -64,4 +90,20 @@ export interface Review extends RowDataType {
|
|||||||
status: string;
|
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 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 CreateCoupon from '@/pages/coupon/Create'
|
||||||
import UpdateCoupon from '@/pages/coupon/Update'
|
import UpdateCoupon from '@/pages/coupon/Update'
|
||||||
import ReviewsList from '@/pages/review/List'
|
import ReviewsList from '@/pages/review/List'
|
||||||
|
import DetailReview from '@/pages/review/Detail'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -90,6 +91,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.coupons.update + ':id'} element={<UpdateCoupon />} />
|
<Route path={Pages.coupons.update + ':id'} element={<UpdateCoupon />} />
|
||||||
|
|
||||||
<Route path={Pages.reviews.list} element={<ReviewsList />} />
|
<Route path={Pages.reviews.list} element={<ReviewsList />} />
|
||||||
|
<Route path={Pages.reviews.detail + ':id'} element={<DetailReview />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user