From 06df41a69dc81b7f8e6c7bc2b7323218425e9955 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 26 Oct 2025 14:46:18 +0330 Subject: [PATCH] all comments and aprove them --- src/App.tsx | 2 +- src/config/PageTitles.ts | 1 + src/config/Pages.ts | 6 + src/pages/products/Reviews.tsx | 174 ++++++++++++++++++ src/pages/products/components/ReviewModal.tsx | 153 +++++++++++++++ src/pages/products/components/index.ts | 1 + src/pages/products/hooks/useProductData.ts | 13 ++ src/pages/products/service/ProductService.ts | 12 ++ src/pages/products/types/Types.ts | 54 ++++++ src/router/Main.tsx | 3 + src/shared/components/ProductsSubMenu.tsx | 12 ++ 11 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 src/pages/products/Reviews.tsx create mode 100644 src/pages/products/components/ReviewModal.tsx diff --git a/src/App.tsx b/src/App.tsx index 33f1980..a98b4d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { type FC, useEffect, useState } from 'react' import { BrowserRouter } from 'react-router-dom' // import 'swiper/swiper-bundle.css'; -// import 'rc-rate/assets/index.css'; +import 'rc-rate/assets/index.css'; // import "react-multi-date-picker/styles/layouts/mobile.css" import './assets/fonts/irancell/style.css' import i18next from 'i18next' diff --git a/src/config/PageTitles.ts b/src/config/PageTitles.ts index 25c034c..ab87612 100644 --- a/src/config/PageTitles.ts +++ b/src/config/PageTitles.ts @@ -15,6 +15,7 @@ export const PageTitles = { [Pages.products.warranty.list]: "لیست گارانتی‌ها", [Pages.products.warranty.create]: "افزودن گارانتی جدید", [Pages.products.warranty.update]: "ویرایش گارانتی", + [Pages.products.reviews.list]: "نظرات محصولات", // Categories [Pages.category.list]: "لیست دسته‌بندی‌ها", diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 3693b93..e6cddc1 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -168,6 +168,12 @@ export const Pages = { create: "/products/warranty/create", update: "/products/warranty/update/", }, + reviews: { + list: "/products/reviews", + }, + questions: { + list: "/products/questions", + }, }, orders: { native: "/orders/native", diff --git a/src/pages/products/Reviews.tsx b/src/pages/products/Reviews.tsx new file mode 100644 index 0000000..a83c649 --- /dev/null +++ b/src/pages/products/Reviews.tsx @@ -0,0 +1,174 @@ +import { type FC, useState } from 'react' +import { useGetProductReviews, useApproveProductReview } from './hooks/useProductData' +import { type ProductReviewType } from './types/Types' +import PageLoading from '../../components/PageLoading' +import Error from '../../components/Error' +import PageTitle from '../../components/PageTitle' +import Td from '../../components/Td' +import PaginationUi from '../../components/PaginationUi' +import StatusWithText from '../../components/StatusWithText' +import { ReviewModal } from './components' +import Rate from 'rc-rate' +import { Eye } from 'iconsax-react' + +const Reviews: FC = () => { + const [currentPage, setCurrentPage] = useState(1); + const [selectedReview, setSelectedReview] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const { data, isLoading, error, refetch } = useGetProductReviews(currentPage); + const approveMutation = useApproveProductReview(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ +
+ ); + } + + const reviews = data?.results?.comments || []; + const pager = data?.results?.pager; + + const getStatusVariant = (status: string) => { + switch (status) { + case 'accepted': return 'success'; + case 'rejected': return 'error'; + case 'pending': return 'warning'; + default: return 'warning'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'accepted': return 'تایید شده'; + case 'rejected': return 'رد شده'; + case 'pending': return 'در انتظار'; + default: return status; + } + }; + + const handleViewReview = (review: ProductReviewType) => { + setSelectedReview(review); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setSelectedReview(null); + }; + + const handleApproveReview = async (reviewId: string) => { + try { + await approveMutation.mutateAsync(reviewId); + await refetch(); + handleCloseModal(); + } catch (error) { + console.error('خطا در تایید کامنت:', error); + } + }; + + return ( +
+ +
+ + + + + + + {reviews.length === 0 ? ( + + + + ) : ( + reviews.map((review: ProductReviewType) => ( + + + + + + + + )) + )} + +
+ + + + + + +
+ هیچ نظری یافت نشد +
+
+ {review.user?.[0]?.fullName || 'کاربر ناشناس'} +
+
+
+
+ {review.product?.title_fa} +
+
+
+ {review.product?.title_fa} +
+
+ {review.product?.title_en} +
+
+
+
+ + + + + + +
+ handleViewReview(review)} + /> +
+
+
+ + { + setCurrentPage(page); + }} + /> + + +
+ ) +} + +export default Reviews \ No newline at end of file diff --git a/src/pages/products/components/ReviewModal.tsx b/src/pages/products/components/ReviewModal.tsx new file mode 100644 index 0000000..76b735d --- /dev/null +++ b/src/pages/products/components/ReviewModal.tsx @@ -0,0 +1,153 @@ +import { type FC } from 'react' +import { type ProductReviewType } from '../types/Types' +import DefaulModal from '../../../components/DefaulModal' +import StatusWithText from '../../../components/StatusWithText' +import Rate from 'rc-rate' +import { Button } from '@/components/ui/button' +import { TickCircle } from 'iconsax-react' + +interface ReviewModalProps { + isOpen: boolean + onClose: () => void + review: ProductReviewType | null + onApprove: (reviewId: string) => void + isApproving?: boolean +} + +const ReviewModal: FC = ({ + isOpen, + onClose, + review, + onApprove, + isApproving = false +}) => { + const getStatusVariant = (status: string) => { + switch (status) { + case 'accepted': return 'success'; + case 'rejected': return 'error'; + case 'pending': return 'warning'; + default: return 'warning'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'accepted': return 'تایید شده'; + case 'rejected': return 'رد شده'; + case 'pending': return 'در انتظار'; + default: return status; + } + }; + + const handleApprove = () => { + if (review) { + onApprove(review._id); + } + }; + + return ( + + {review && ( +
+ {/* اطلاعات کاربر و محصول */} +
+
+
+ {review.product?.title_fa} +
+
+
+ {review.product?.title_fa} +
+
+ توسط {review.user?.[0]?.fullName || 'کاربر ناشناس'} +
+
+
+
+ + {/* عنوان کامنت */} +
+

{review.title}

+
+ + {/* امتیاز */} +
+ امتیاز: + +
+ + {/* محتوای کامنت */} +
+

متن کامنت:

+

{review.content}

+
+ + {/* مزایا */} + {review.advantage && review.advantage.length > 0 && ( +
+

مزایا:

+
    + {review.advantage.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ )} + + {/* معایب */} + {review.disAdvantage && review.disAdvantage.length > 0 && ( +
+

معایب:

+
    + {review.disAdvantage.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ )} + + {/* وضعیت */} +
+ وضعیت: + +
+ + {/* تاریخ */} +
+ تاریخ ارسال: {review.createdAt} +
+ + {/* دکمه تایید */} + {review.status !== 'accepted' && ( +
+ +
+ )} +
+ )} +
+ ) +} + +export default ReviewModal diff --git a/src/pages/products/components/index.ts b/src/pages/products/components/index.ts index b49dfc4..ad159e9 100644 --- a/src/pages/products/components/index.ts +++ b/src/pages/products/components/index.ts @@ -2,3 +2,4 @@ export { default as StepIndicator } from "./StepIndicator"; export { default as Step1Form } from "./Step1Form"; export { default as Step2Form } from "./Step2Form"; export { default as Step3Form } from "./Step3Form"; +export { default as ReviewModal } from "./ReviewModal"; diff --git a/src/pages/products/hooks/useProductData.ts b/src/pages/products/hooks/useProductData.ts index 8d89bba..3adcec8 100644 --- a/src/pages/products/hooks/useProductData.ts +++ b/src/pages/products/hooks/useProductData.ts @@ -103,4 +103,17 @@ export const useDeleteProduct = () => { return useMutation({ mutationFn: (id: number) => api.deleteProduct(id), }); +}; + +export const useGetProductReviews = (page: number = 1, limit: number = 10) => { + return useQuery({ + queryKey: ['product-reviews', page, limit], + queryFn: () => api.getProductReviews(page, limit), + }); +}; + +export const useApproveProductReview = () => { + return useMutation({ + mutationFn: api.approveProductReview, + }); }; \ No newline at end of file diff --git a/src/pages/products/service/ProductService.ts b/src/pages/products/service/ProductService.ts index 999ab22..3b89b81 100644 --- a/src/pages/products/service/ProductService.ts +++ b/src/pages/products/service/ProductService.ts @@ -8,6 +8,7 @@ import { type SaveProductResponseType, type GetProductsResponseType, type GetProductVariantsResponseType, + type GetProductReviewsResponseType, type CreateProductVariantRequestType, type CreateProductVariantResponseType, type GetProductByIdResponseType, @@ -98,4 +99,15 @@ export const updateProduct = async (params: UpdateProductRequestType): Promise { const { data } = await axios.delete(`/admin/products/${id}/delete`); return data; +}; + +export const getProductReviews = async (page: number = 1, limit: number = 10): Promise => { + const params: Record = { page, limit }; + const { data } = await axios.get(`/admin/products/comments`, { params }); + return data; +}; + +export const approveProductReview = async (id: string) => { + const { data } = await axios.post(`/admin/products/comments/${id}/approve`); + return data; }; \ No newline at end of file diff --git a/src/pages/products/types/Types.ts b/src/pages/products/types/Types.ts index 3753548..15dc202 100644 --- a/src/pages/products/types/Types.ts +++ b/src/pages/products/types/Types.ts @@ -402,6 +402,60 @@ export type UpdateVariantResponseType = { }; }; +// تایپ‌های مربوط به reviews/comments محصول +export type ProductReviewUserType = { + fullName: string; +}; + +export type ProductReviewProductType = { + _id: number; + title_fa: string; + title_en: string; + seoTitle: string; + seoDescription: string | null; + source: string; + description: string; + metaDescription: string; + tags: string[]; + advantages: string[]; + disAdvantages: string[]; + imagesUrl: ProductImagesType; + isFake: string; + voice: string | null; + specifications: ProductSpecificationType[]; + brand: { + _id: string; + }; + category: { + _id: string; + }; + variants: { + _id: string; + }[]; +}; + +export type ProductReviewType = { + _id: string; + title: string; + advantage: string[]; + disAdvantage: string[]; + content: string; + status: string; + rate: number; + user: ProductReviewUserType[]; + product: ProductReviewProductType; + createdAt: string; +}; + +export type GetProductReviewsResponseType = { + status: number; + success: boolean; + results: { + pager: PagerType; + comments: ProductReviewType[]; + }; +}; + // تایپ‌های مربوط به آپدیت محصول سه مرحله‌ای // مرحله اول: آپدیت جزئیات محصول diff --git a/src/router/Main.tsx b/src/router/Main.tsx index c9e6eeb..bca73bf 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -77,6 +77,7 @@ import Category from '@/pages/ticket/Category' import CreateCategory from '@/pages/ticket/Create' import UpdateCategory from '@/pages/ticket/Update' import TicketMessages from '@/pages/ticket/TicketMessages' +import Reviews from '@/pages/products/Reviews' const MainRouter: FC = () => { const { hasSubMenu } = useSharedStore() @@ -187,6 +188,8 @@ const MainRouter: FC = () => { } /> } /> } /> + + } /> diff --git a/src/shared/components/ProductsSubMenu.tsx b/src/shared/components/ProductsSubMenu.tsx index 0aec11f..7753b22 100644 --- a/src/shared/components/ProductsSubMenu.tsx +++ b/src/shared/components/ProductsSubMenu.tsx @@ -44,6 +44,18 @@ const ProductsSubMenu: FC = () => { isActive={isActive('warranty')} link={Pages.products.warranty.list} /> + + + + )