list of request create product

This commit is contained in:
hamid zarghami
2025-10-12 10:48:53 +03:30
parent e8c657f573
commit db6157ae5d
10 changed files with 584 additions and 2 deletions
+1
View File
@@ -199,5 +199,6 @@ export const Pages = {
wholesaleRequests: "/sellers/wholesale-requests",
withdrawalRequests: "/sellers/withdrawal-requests",
announcement: "/sellers/announcement",
requestCreateProduct: "/sellers/request-create-product",
},
};
+9
View File
@@ -0,0 +1,9 @@
import { type FC } from 'react'
const CategoryLearning = () => {
return (
<div>CategoryLearning</div>
)
}
export default CategoryLearning
+94
View File
@@ -0,0 +1,94 @@
import { type FC, useState } from 'react'
import { useGetRequestCreateProduct } from './hooks/useSellerData'
import { type ProductRequest } from './types/Types'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import PaginationUi from '../../components/PaginationUi'
import Td from '../../components/Td'
import ProductRequestTableRow from './components/ProductRequestTableRow'
import ProductRequestDetailModal from './components/ProductRequestDetailModal'
const RequestCreateProduct: FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const [selectedRequest, setSelectedRequest] = useState<ProductRequest | null>(null);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const { data, isLoading, error } = useGetRequestCreateProduct(currentPage);
const handleRowClick = (request: ProductRequest) => {
setSelectedRequest(request);
setIsDetailModalOpen(true);
};
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 productRequests = data?.results?.productRequests || [];
const pager = data?.results?.pager;
return (
<div>
<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={'تاریخ ایجاد'} />
<Td text={'خدمات درخواست شده'} />
<Td text={'عملیات'} />
</tr>
</thead>
<tbody>
{productRequests.length === 0 ? (
<tr className='tr'>
<td colSpan={9} className="text-center py-8 text-gray-500">
هیچ درخواست ایجاد محصولی یافت نشد
</td>
</tr>
) : (
productRequests.map((request: ProductRequest) => (
<ProductRequestTableRow
key={request._id}
request={request}
onClick={() => handleRowClick(request)}
/>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
<ProductRequestDetailModal
open={isDetailModalOpen}
close={() => setIsDetailModalOpen(false)}
request={selectedRequest}
/>
</div>
)
}
export default RequestCreateProduct
@@ -0,0 +1,249 @@
import { type FC } from 'react'
import DefaulModal from '@/components/DefaulModal'
import { type ProductRequest } from '../types/Types'
import StatusWithText from '@/components/StatusWithText'
import { Calendar, User as UserIcon, Shop, Tag, Box, MessageSquare, Location, Money } from 'iconsax-react'
interface Props {
open: boolean
close: () => void
request: ProductRequest | null
}
const ProductRequestDetailModal: FC<Props> = ({ open, close, request }) => {
if (!request) return null
const getStatusVariant = (status: string) => {
switch (status) {
case 'Approved': return 'success'
case 'Rejected': return 'error'
case 'Pending': return 'warning'
case 'Draft': return 'warning'
default: return 'warning'
}
}
const getStatusText = (status: string) => {
switch (status) {
case 'Approved': return 'تایید شده'
case 'Rejected': return 'رد شده'
case 'Pending': return 'در انتظار'
case 'Draft': return 'پیش‌نویس'
default: return status
}
}
const formatDate = (dateString: string) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleDateString('fa-IR')
}
return (
<DefaulModal
open={open}
close={close}
isHeader={true}
title_header="جزئیات درخواست ایجاد محصول"
width={800}
>
<div className="space-y-6 mt-4">
{/* وضعیت درخواست */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">وضعیت درخواست</h3>
<StatusWithText
variant={getStatusVariant(request.requestStatus)}
text={getStatusText(request.requestStatus)}
/>
</div>
{/* اطلاعات فروشنده */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<UserIcon size={20} color="#374151" />
اطلاعات فروشنده
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-2">
<UserIcon size={16} color="#6B7280" />
<span className="text-sm text-gray-600">نام:</span>
<span className="text-sm font-medium">{request.seller?.fullName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Shop size={16} color="#6B7280" />
<span className="text-sm text-gray-600">فروشگاه:</span>
<span className="text-sm font-medium">{request.shop?.shopName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Location size={16} color="#6B7280" />
<span className="text-sm text-gray-600">آدرس:</span>
<span className="text-sm font-medium">{request.shop?.address?.province?.name || '-'}، {request.shop?.address?.city?.name || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Tag size={16} color="#6B7280" />
<span className="text-sm text-gray-600">کد فروشگاه:</span>
<span className="text-sm font-medium">{request.shop?.shopCode || '-'}</span>
</div>
</div>
</div>
{/* اطلاعات محصول */}
<div className="bg-blue-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<Box size={20} color="#1E40AF" />
اطلاعات محصول
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-2">
<Box size={16} color="#6B7280" />
<span className="text-sm text-gray-600">نام محصول:</span>
<span className="text-sm font-medium">{request.productName}</span>
</div>
<div className="flex items-center gap-2">
<Tag size={16} color="#6B7280" />
<span className="text-sm text-gray-600">برند:</span>
<span className="text-sm font-medium">{request.brand?.title_fa || '-'}</span>
</div>
<div className="flex items-center gap-2">
<MessageSquare size={16} color="#6B7280" />
<span className="text-sm text-gray-600">دستهبندی:</span>
<span className="text-sm font-medium">{request.category?.title_fa || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Tag size={16} color="#6B7280" />
<span className="text-sm text-gray-600">منبع:</span>
<span className="text-sm font-medium">{request.source || '-'}</span>
</div>
</div>
</div>
{/* توضیحات */}
<div className="bg-green-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3">توضیحات</h4>
<p className="text-sm text-gray-700 leading-relaxed">{String(request.description || 'توضیحاتی وارد نشده')}</p>
</div>
{/* ابعاد و وزن */}
<div className="bg-yellow-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<Box size={20} color="#92400E" />
ابعاد و وزن
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_weight || 0}</div>
<div className="text-xs text-gray-600">وزن (گرم)</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_length || 0}</div>
<div className="text-xs text-gray-600">طول (سانتیمتر)</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_width || 0}</div>
<div className="text-xs text-gray-600">عرض (سانتیمتر)</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_height || 0}</div>
<div className="text-xs text-gray-600">ارتفاع (سانتیمتر)</div>
</div>
</div>
</div>
{/* مزایا و معایب */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-green-50 rounded-lg p-4">
<h4 className="text-md font-medium text-green-900 mb-3">مزایا</h4>
{request.advantages && request.advantages.length > 0 ? (
<ul className="space-y-1">
{request.advantages.map((advantage, index) => (
<li key={index} className="text-sm text-green-800 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-green-600 rounded-full"></span>
{String(advantage)}
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-600">مزایایی وارد نشده</p>
)}
</div>
<div className="bg-red-50 rounded-lg p-4">
<h4 className="text-md font-medium text-red-900 mb-3">معایب</h4>
{request.disAdvantages && request.disAdvantages.length > 0 ? (
<ul className="space-y-1">
{request.disAdvantages.map((disadvantage, index) => (
<li key={index} className="text-sm text-red-800 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-red-600 rounded-full"></span>
{String(disadvantage)}
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-600">معایبی وارد نشده</p>
)}
</div>
</div>
{/* خدمات درخواست شده */}
<div className="bg-purple-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3">خدمات درخواست شده</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={request.requestPhotography}
readOnly
className="rounded border-gray-300"
/>
<span className="text-sm">📸 عکاسی محصول</span>
{request.requestPhotosCount && (
<span className="text-xs text-gray-600">({request.requestPhotosCount} عکس)</span>
)}
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={request.expertReview}
readOnly
className="rounded border-gray-300"
/>
<span className="text-sm">👨💼 بررسی تخصصی</span>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={request.unboxingVideo}
readOnly
className="rounded border-gray-300"
/>
<span className="text-sm">📦 ویدیو باز کردن بسته</span>
</div>
</div>
</div>
{/* اطلاعات پرداخت */}
{request.payment && (
<div className="bg-indigo-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<Money size={20} color="#312E81" />
اطلاعات پرداخت
</h4>
<div className="text-sm text-gray-700">
<pre className="whitespace-pre-wrap">{String(JSON.stringify(request.payment, null, 2))}</pre>
</div>
</div>
)}
{/* تاریخ ایجاد */}
<div className="flex items-center justify-between text-sm text-gray-600 border-t pt-4">
<span>تاریخ ایجاد:</span>
<div className="flex items-center gap-1">
<Calendar size={16} color="#6B7280" />
<span>{formatDate(request._id ? new Date().toISOString() : '')}</span>
</div>
</div>
</div>
</DefaulModal>
)
}
export default ProductRequestDetailModal
@@ -0,0 +1,104 @@
import { type FC } from 'react'
import { type ProductRequest } from '../types/Types'
import StatusWithText from '../../../components/StatusWithText'
import Td from '../../../components/Td'
import { Calendar, User as UserIcon, Shop, Tag, Box, MessageSquare, Eye } from 'iconsax-react'
interface Props {
request: ProductRequest
onClick?: () => void
}
const ProductRequestTableRow: FC<Props> = ({ request, onClick }) => {
const getStatusVariant = (status: string) => {
switch (status) {
case 'Approved': return 'success';
case 'Rejected': return 'error';
case 'Pending': return 'warning';
case 'Draft': return 'warning';
default: return 'warning';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'Approved': return 'تایید شده';
case 'Rejected': return 'رد شده';
case 'Pending': return 'در انتظار';
case 'Draft': return 'پیش‌نویس';
default: return status;
}
};
const formatDate = (dateString: string) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('fa-IR');
};
return (
<tr className='tr'>
<Td text="">
<div className="flex items-center gap-1">
<UserIcon size={16} color="#8C90A3" />
<span>{request.seller?.fullName || '-'}</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-1">
<Shop size={16} color="#8C90A3" />
<span>{request.shop?.shopName || '-'}</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-1">
<Box size={16} color="#8C90A3" />
<span>{request.productName}</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-1">
<Tag size={16} color="#8C90A3" />
<span>{request.brand?.title_fa || '-'}</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-1">
<MessageSquare size={16} color="#8C90A3" />
<span>{request.category?.title_fa || '-'}</span>
</div>
</Td>
<Td text="">
<StatusWithText
variant={getStatusVariant(request.requestStatus)}
text={getStatusText(request.requestStatus)}
/>
</Td>
<Td text="">
<div className="flex items-center gap-1">
<Calendar size={16} color="#8C90A3" />
<span>{formatDate(request._id ? new Date().toISOString() : '')}</span>
</div>
</Td>
<Td text="">
<div className="flex flex-col gap-1 text-xs">
{request.requestPhotography && <span>📸 عکاسی</span>}
{request.expertReview && <span>👨💼 بررسی تخصصی</span>}
{request.unboxingVideo && <span>📦 ویدیو باز کردن بسته</span>}
</div>
</Td>
<Td text="">
<Eye
size={16}
color="#8C90A3"
className="cursor-pointer hover:text-blue-500 transition-colors"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
onClick?.();
}}
/>
</Td>
</tr>
)
}
export default ProductRequestTableRow
+7
View File
@@ -59,3 +59,10 @@ export const useCreateAnnouncementSpecificSeller = () => {
mutationFn: api.createAnnouncementToSpecificSellers,
});
};
export const useGetRequestCreateProduct = (page: number = 1) => {
return useQuery({
queryKey: ["request-create-product", page],
queryFn: () => api.getRequestCreateProduct(page),
});
};
+10
View File
@@ -6,6 +6,7 @@ import type {
CreateAnnouncementSeller,
NotificationsResponse,
CreateAnnouncementSpecificSeller,
GetRequestCreateProductResponse,
} from "../types/Types";
export const getSellers = async (
@@ -77,3 +78,12 @@ export const createAnnouncementToSpecificSellers = async (
);
return data;
};
export const getRequestCreateProduct = async (
page: number = 1
): Promise<GetRequestCreateProductResponse> => {
const { data } = await axios.get(
`/admin/products/requests/add-by-admin?page=${page}`
);
return data;
};
+106
View File
@@ -212,3 +212,109 @@ export interface NotificationsResponse {
notifications: Notification[];
};
}
export interface CreateCategoryLearning {
title: string;
}
// Product Request Types
export interface City {
_id: number;
name: string;
province: number;
createdAt: string;
updatedAt: string;
}
export interface Province {
_id: number;
name: string;
createdAt: string;
updatedAt: string;
}
export interface Address {
_id: string;
address: string;
city: City;
province: Province;
postalCode: string | null;
plaque: string | null;
lat: string;
lon: string;
createdAt: string;
updatedAt: string;
}
export interface ProductRequestShop {
_id: string;
shopName: string;
address: Address;
shopCode: number;
}
export interface Brand {
_id: string;
title_fa: string;
title_en: string;
url: string;
}
export interface Category {
_id: string;
title_fa: string;
title_en: string;
icon: string;
imageUrl: string;
description: string;
variants: string[];
hierarchy: string[];
leaf: boolean;
parent: string | null;
deleted: boolean;
createdAt: string;
updatedAt: string;
url: string;
children: Category[];
}
export interface Dimensions {
package_weight: number;
package_height: number;
package_length: number;
package_width: number;
}
export interface ProductRequestSeller {
_id: string;
fullName: string;
}
export interface ProductRequest {
requestPhotography: boolean;
requestPhotosCount: number | null;
expertReview: boolean;
unboxingVideo: boolean;
payment: unknown | null;
requestStatus: string;
_id: string;
seller: ProductRequestSeller;
shop: ProductRequestShop;
brand: Brand;
category: Category;
source: string;
productName: string;
description: string;
dimensions: Dimensions;
advantages: string[];
disAdvantages: string[];
}
export interface GetRequestCreateProductResponse {
status: number;
success: boolean;
results: {
productRequests: ProductRequest[];
pager: Pager;
};
}
+2
View File
@@ -66,6 +66,7 @@ import SellerList from '@/pages/seller/List'
import WholeSaleRequest from '@/pages/seller/WholeSaleRequest'
import WithdrawalRequestPage from '@/pages/seller/WithdrawalRequest'
import Annoncement from '@/pages/seller/Annoncement'
import RequestCreateProduct from '@/pages/seller/RequestCreateProduct'
const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore()
@@ -165,6 +166,7 @@ const MainRouter: FC = () => {
<Route path={Pages.sellers.wholesaleRequests} element={<WholeSaleRequest />} />
<Route path={Pages.sellers.withdrawalRequests} element={<WithdrawalRequestPage />} />
<Route path={Pages.sellers.announcement} element={<Annoncement />} />
<Route path={Pages.sellers.requestCreateProduct} element={<RequestCreateProduct />} />
</Routes>
</div>
</div>
+2 -2
View File
@@ -518,8 +518,8 @@ const SellersSubMenu: FC = () => {
/>
<SubMenuItem
title={'درخواست های ثبت محصول'}
isActive={isActive('product-requests')}
link="/sellers/product-requests"
isActive={isActive('request-create-product')}
link={Pages.sellers.requestCreateProduct}
/>
<SubMenuItem
title={'قرار داد'}