all comments and aprove them

This commit is contained in:
hamid zarghami
2025-10-26 14:46:18 +03:30
parent 2343bc8d6f
commit 06df41a69d
11 changed files with 430 additions and 1 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import { type FC, useEffect, useState } from 'react' import { type FC, useEffect, useState } from 'react'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
// import 'swiper/swiper-bundle.css'; // 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 "react-multi-date-picker/styles/layouts/mobile.css"
import './assets/fonts/irancell/style.css' import './assets/fonts/irancell/style.css'
import i18next from 'i18next' import i18next from 'i18next'
+1
View File
@@ -15,6 +15,7 @@ export const PageTitles = {
[Pages.products.warranty.list]: "لیست گارانتی‌ها", [Pages.products.warranty.list]: "لیست گارانتی‌ها",
[Pages.products.warranty.create]: "افزودن گارانتی جدید", [Pages.products.warranty.create]: "افزودن گارانتی جدید",
[Pages.products.warranty.update]: "ویرایش گارانتی", [Pages.products.warranty.update]: "ویرایش گارانتی",
[Pages.products.reviews.list]: "نظرات محصولات",
// Categories // Categories
[Pages.category.list]: "لیست دسته‌بندی‌ها", [Pages.category.list]: "لیست دسته‌بندی‌ها",
+6
View File
@@ -168,6 +168,12 @@ export const Pages = {
create: "/products/warranty/create", create: "/products/warranty/create",
update: "/products/warranty/update/", update: "/products/warranty/update/",
}, },
reviews: {
list: "/products/reviews",
},
questions: {
list: "/products/questions",
},
}, },
orders: { orders: {
native: "/orders/native", native: "/orders/native",
+174
View File
@@ -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<ProductReviewType | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const { data, isLoading, error, refetch } = useGetProductReviews(currentPage);
const approveMutation = useApproveProductReview();
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
);
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری نظرات" />
</div>
);
}
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 (
<div>
<PageTitle />
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text={'نام کاربر'} />
<Td text={'محصول'} />
<Td text={'عنوان نظر'} />
<Td text={'امتیاز'} />
<Td text={'وضعیت'} />
<Td text={'تاریخ'} />
<Td text={'عملیات'} />
</tr>
</thead>
<tbody>
{reviews.length === 0 ? (
<tr className='tr'>
<td colSpan={6} className="text-center py-8 text-gray-500">
هیچ نظری یافت نشد
</td>
</tr>
) : (
reviews.map((review: ProductReviewType) => (
<tr key={review._id} className='tr'>
<Td text="">
<div className="font-medium text-gray-900">
{review.user?.[0]?.fullName || 'کاربر ناشناس'}
</div>
</Td>
<Td text="">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden">
<img
src={review.product?.imagesUrl?.cover || '/placeholder-image.png'}
alt={review.product?.title_fa}
className="w-full h-full object-cover"
/>
</div>
<div>
<div className="font-medium text-gray-900">
{review.product?.title_fa}
</div>
<div className="text-gray-500 text-xs">
{review.product?.title_en}
</div>
</div>
</div>
</Td>
<Td text={review.title} />
<Td text="">
<Rate value={review.rate} disabled />
</Td>
<Td text="">
<StatusWithText
variant={getStatusVariant(review.status)}
text={getStatusText(review.status)}
/>
</Td>
<Td text={review.createdAt} />
<Td text="">
<div className="flex items-center gap-2">
<Eye
size={16}
color="#8C90A3"
className="cursor-pointer"
onClick={() => handleViewReview(review)}
/>
</div>
</Td>
</tr>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
<ReviewModal
isOpen={isModalOpen}
onClose={handleCloseModal}
review={selectedReview}
onApprove={handleApproveReview}
isApproving={approveMutation.isPending}
/>
</div>
)
}
export default Reviews
@@ -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<ReviewModalProps> = ({
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 (
<DefaulModal
open={isOpen}
close={onClose}
isHeader={true}
title_header="مشاهده کامنت"
width={600}
>
{review && (
<div className="space-y-4 mt-5 w-[400px]">
{/* اطلاعات کاربر و محصول */}
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg overflow-hidden">
<img
src={review.product?.imagesUrl?.cover || '/placeholder-image.png'}
alt={review.product?.title_fa}
className="w-full h-full object-cover"
/>
</div>
<div>
<div className="font-medium text-gray-900 text-sm">
{review.product?.title_fa}
</div>
<div className="text-gray-500 text-xs">
توسط {review.user?.[0]?.fullName || 'کاربر ناشناس'}
</div>
</div>
</div>
</div>
{/* عنوان کامنت */}
<div>
<h3 className="font-semibold text-gray-900 mb-2">{review.title}</h3>
</div>
{/* امتیاز */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">امتیاز:</span>
<Rate value={review.rate} disabled />
</div>
{/* محتوای کامنت */}
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">متن کامنت:</h4>
<p className="text-gray-700 leading-relaxed">{review.content}</p>
</div>
{/* مزایا */}
{review.advantage && review.advantage.length > 0 && (
<div className="bg-green-50 p-4 rounded-lg">
<h4 className="font-medium text-green-900 mb-2">مزایا:</h4>
<ul className="list-disc list-inside text-green-800 space-y-1">
{review.advantage.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)}
{/* معایب */}
{review.disAdvantage && review.disAdvantage.length > 0 && (
<div className="bg-red-50 p-4 rounded-lg">
<h4 className="font-medium text-red-900 mb-2">معایب:</h4>
<ul className="list-disc list-inside text-red-800 space-y-1">
{review.disAdvantage.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)}
{/* وضعیت */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">وضعیت:</span>
<StatusWithText
variant={getStatusVariant(review.status)}
text={getStatusText(review.status)}
/>
</div>
{/* تاریخ */}
<div className="text-sm text-gray-500">
تاریخ ارسال: {review.createdAt}
</div>
{/* دکمه تایید */}
{review.status !== 'accepted' && (
<div className="flex justify-end pt-4 border-t">
<Button
onClick={handleApprove}
disabled={isApproving}
className="bg-green-600 hover:bg-green-700 disabled:opacity-50"
>
<TickCircle color="#fff" size={16} className="" />
{isApproving ? 'در حال تایید...' : 'تایید کامنت'}
</Button>
</div>
)}
</div>
)}
</DefaulModal>
)
}
export default ReviewModal
+1
View File
@@ -2,3 +2,4 @@ export { default as StepIndicator } from "./StepIndicator";
export { default as Step1Form } from "./Step1Form"; export { default as Step1Form } from "./Step1Form";
export { default as Step2Form } from "./Step2Form"; export { default as Step2Form } from "./Step2Form";
export { default as Step3Form } from "./Step3Form"; export { default as Step3Form } from "./Step3Form";
export { default as ReviewModal } from "./ReviewModal";
@@ -103,4 +103,17 @@ export const useDeleteProduct = () => {
return useMutation({ return useMutation({
mutationFn: (id: number) => api.deleteProduct(id), 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,
});
}; };
@@ -8,6 +8,7 @@ import {
type SaveProductResponseType, type SaveProductResponseType,
type GetProductsResponseType, type GetProductsResponseType,
type GetProductVariantsResponseType, type GetProductVariantsResponseType,
type GetProductReviewsResponseType,
type CreateProductVariantRequestType, type CreateProductVariantRequestType,
type CreateProductVariantResponseType, type CreateProductVariantResponseType,
type GetProductByIdResponseType, type GetProductByIdResponseType,
@@ -98,4 +99,15 @@ export const updateProduct = async (params: UpdateProductRequestType): Promise<U
export const deleteProduct = async (id: number) => { export const deleteProduct = async (id: number) => {
const { data } = await axios.delete(`/admin/products/${id}/delete`); const { data } = await axios.delete(`/admin/products/${id}/delete`);
return data; return data;
};
export const getProductReviews = async (page: number = 1, limit: number = 10): Promise<GetProductReviewsResponseType> => {
const params: Record<string, string | number> = { 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;
}; };
+54
View File
@@ -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[];
};
};
// تایپ‌های مربوط به آپدیت محصول سه مرحله‌ای // تایپ‌های مربوط به آپدیت محصول سه مرحله‌ای
// مرحله اول: آپدیت جزئیات محصول // مرحله اول: آپدیت جزئیات محصول
+3
View File
@@ -77,6 +77,7 @@ import Category from '@/pages/ticket/Category'
import CreateCategory from '@/pages/ticket/Create' import CreateCategory from '@/pages/ticket/Create'
import UpdateCategory from '@/pages/ticket/Update' import UpdateCategory from '@/pages/ticket/Update'
import TicketMessages from '@/pages/ticket/TicketMessages' import TicketMessages from '@/pages/ticket/TicketMessages'
import Reviews from '@/pages/products/Reviews'
const MainRouter: FC = () => { const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore() const { hasSubMenu } = useSharedStore()
@@ -187,6 +188,8 @@ const MainRouter: FC = () => {
<Route path={Pages.sellers.learningCreate} element={<CreateLearning />} /> <Route path={Pages.sellers.learningCreate} element={<CreateLearning />} />
<Route path={`${Pages.sellers.learningUpdate}:id`} element={<UpdateLearning />} /> <Route path={`${Pages.sellers.learningUpdate}:id`} element={<UpdateLearning />} />
<Route path={Pages.sellers.contract} element={<ContractPage />} /> <Route path={Pages.sellers.contract} element={<ContractPage />} />
<Route path={Pages.products.reviews.list} element={<Reviews />} />
</Routes> </Routes>
</div> </div>
</div> </div>
+12
View File
@@ -44,6 +44,18 @@ const ProductsSubMenu: FC = () => {
isActive={isActive('warranty')} isActive={isActive('warranty')}
link={Pages.products.warranty.list} link={Pages.products.warranty.list}
/> />
<SubMenuItem
title={'نظرات محصولات'}
isActive={isActive('reviews')}
link={Pages.products.reviews.list}
/>
<SubMenuItem
title={'پرسش های محصولات'}
isActive={isActive('questions')}
link={Pages.products.questions.list}
/>
</div> </div>
</div> </div>
) )