Complete Questions list page implementation with modal and pagination

This commit is contained in:
hamid zarghami
2025-10-26 14:57:17 +03:30
parent 06df41a69d
commit 3d5eebc91e
7 changed files with 390 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
import { type FC, useState } from 'react'
import { useGetProductQuestions } from './hooks/useProductData'
import { type ProductQuestionType } 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 { QuestionModal } from './components'
import { Eye } from 'iconsax-react'
const Questions: FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const [selectedQuestion, setSelectedQuestion] = useState<ProductQuestionType | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const { data, isLoading, error } = useGetProductQuestions(currentPage);
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 questions = data?.results?.questions || [];
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 handleViewQuestion = (question: ProductQuestionType) => {
setSelectedQuestion(question);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setSelectedQuestion(null);
};
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={'عملیات'} />
</tr>
</thead>
<tbody>
{questions.length === 0 ? (
<tr className='tr'>
<td colSpan={6} className="text-center py-8 text-gray-500">
هیچ سوالی یافت نشد
</td>
</tr>
) : (
questions.map((question: ProductQuestionType) => (
<tr key={question._id} className='tr'>
<Td text="">
<div className="font-medium text-gray-900">
{question.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={question.product?.imagesUrl?.cover || '/placeholder-image.png'}
alt={question.product?.title_fa}
className="w-full h-full object-cover"
/>
</div>
<div>
<div className="font-medium text-gray-900">
{question.product?.title_fa}
</div>
<div className="text-gray-500 text-xs">
{question.product?.title_en}
</div>
</div>
</div>
</Td>
<Td text="">
<div className="max-w-xs truncate">
{question.content}
</div>
</Td>
<Td text="">
<StatusWithText
variant={getStatusVariant(question.status)}
text={getStatusText(question.status)}
/>
</Td>
<Td text={question.createdAt} />
<Td text="">
<div className="flex items-center gap-2">
<Eye
size={16}
color="#8C90A3"
className="cursor-pointer"
onClick={() => handleViewQuestion(question)}
/>
</div>
</Td>
</tr>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
<QuestionModal
isOpen={isModalOpen}
onClose={handleCloseModal}
question={selectedQuestion}
/>
</div>
)
}
export default Questions
@@ -0,0 +1,147 @@
import { type FC } from 'react'
import { type ProductQuestionType } from '../types/Types'
import DefaulModal from '../../../components/DefaulModal'
import StatusWithText from '../../../components/StatusWithText'
import { Button } from '@/components/ui/button'
import { TickCircle } from 'iconsax-react'
interface QuestionModalProps {
isOpen: boolean
onClose: () => void
question: ProductQuestionType | null
onApprove?: (questionId: string) => void
isApproving?: boolean
}
const QuestionModal: FC<QuestionModalProps> = ({
isOpen,
onClose,
question,
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 (question && onApprove) {
onApprove(question._id);
}
};
return (
<DefaulModal
open={isOpen}
close={onClose}
isHeader={true}
title_header="مشاهده سوال"
width={600}
>
{question && (
<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={question.product?.imagesUrl?.cover || '/placeholder-image.png'}
alt={question.product?.title_fa}
className="w-full h-full object-cover"
/>
</div>
<div>
<div className="font-medium text-gray-900 text-sm">
{question.product?.title_fa}
</div>
<div className="text-gray-500 text-xs">
توسط {question.user?.[0]?.fullName || 'کاربر ناشناس'}
</div>
</div>
</div>
</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">{question.content}</p>
</div>
{/* پاسخ‌ها */}
{question.answers && question.answers.length > 0 && (
<div className="bg-blue-50 p-4 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">پاسخها:</h4>
<div className="space-y-3">
{question.answers.map((answer, index) => (
<div key={answer._id || index} className="bg-white p-3 rounded border-l-4 border-blue-500">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-900">
{answer.user?.fullName || 'مدیر'}
</span>
{answer.status && (
<StatusWithText
variant={getStatusVariant(answer.status)}
text={getStatusText(answer.status)}
/>
)}
</div>
<p className="text-gray-700">{answer.content}</p>
{answer.createdAt && (
<div className="text-xs text-gray-500 mt-2">
{answer.createdAt}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* وضعیت */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">وضعیت:</span>
<StatusWithText
variant={getStatusVariant(question.status)}
text={getStatusText(question.status)}
/>
</div>
{/* تاریخ */}
<div className="text-sm text-gray-500">
تاریخ ارسال: {question.createdAt}
</div>
{/* دکمه تایید */}
{question.status !== 'accepted' && onApprove && (
<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 QuestionModal
+1
View File
@@ -3,3 +3,4 @@ export { default as Step1Form } from "./Step1Form";
export { default as Step2Form } from "./Step2Form";
export { default as Step3Form } from "./Step3Form";
export { default as ReviewModal } from "./ReviewModal";
export { default as QuestionModal } from "./QuestionModal";
@@ -117,3 +117,10 @@ export const useApproveProductReview = () => {
mutationFn: api.approveProductReview,
});
};
export const useGetProductQuestions = (page: number = 1, limit: number = 10) => {
return useQuery({
queryKey: ['product-questions', page, limit],
queryFn: () => api.getProductQuestions(page, limit),
});
};
@@ -9,6 +9,7 @@ import {
type GetProductsResponseType,
type GetProductVariantsResponseType,
type GetProductReviewsResponseType,
type ProductQuestionsResponseType,
type CreateProductVariantRequestType,
type CreateProductVariantResponseType,
type GetProductByIdResponseType,
@@ -111,3 +112,9 @@ export const approveProductReview = async (id: string) => {
const { data } = await axios.post(`/admin/products/comments/${id}/approve`);
return data;
};
export const getProductQuestions = async (page: number = 1, limit: number = 10): Promise<ProductQuestionsResponseType> => {
const params: Record<string, string | number> = { page, limit };
const { data } = await axios.get(`admin/products/questions`, { params });
return data;
};
+66
View File
@@ -456,6 +456,72 @@ export type GetProductReviewsResponseType = {
};
};
// تایپ‌های مربوط به سوالات محصول
export type ProductQuestionUserType = {
fullName: string;
};
export type ProductQuestionBrandType = {
_id: string;
};
export type ProductQuestionCategoryType = {
_id: string;
};
export type ProductQuestionVariantType = {
_id: string;
};
export type ProductQuestionProductType = {
_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: ProductQuestionBrandType;
category: ProductQuestionCategoryType;
variants: ProductQuestionVariantType[];
};
export type ProductQuestionAnswerType = {
_id?: string;
content: string;
status?: string;
user?: ProductQuestionUserType;
createdAt?: string;
};
export type ProductQuestionType = {
_id: string;
content: string;
status: string;
product: ProductQuestionProductType;
user: ProductQuestionUserType[];
answers: ProductQuestionAnswerType[];
createdAt: string;
};
export type ProductQuestionsResponseType = {
status: number;
success: boolean;
results: {
pager: PagerType;
questions: ProductQuestionType[];
count: number;
};
};
// تایپ‌های مربوط به آپدیت محصول سه مرحله‌ای
// مرحله اول: آپدیت جزئیات محصول
+2
View File
@@ -78,6 +78,7 @@ import CreateCategory from '@/pages/ticket/Create'
import UpdateCategory from '@/pages/ticket/Update'
import TicketMessages from '@/pages/ticket/TicketMessages'
import Reviews from '@/pages/products/Reviews'
import Questions from '@/pages/products/Questions'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
@@ -190,6 +191,7 @@ const MainRouter: FC = () => {
<Route path={Pages.sellers.contract} element={<ContractPage />} />
<Route path={Pages.products.reviews.list} element={<Reviews />} />
<Route path={Pages.products.questions.list} element={<Questions />} />
</Routes>
</div>
</div>