From 0dd1ab170d38cba9922597f9eafa12364604e526 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 24 Sep 2025 09:02:08 +0330 Subject: [PATCH] update product --- src/config/Pages.ts | 1 + src/index.css | 2 +- src/pages/products/List.tsx | 10 +- src/pages/products/Update.tsx | 209 +++++++++++++++++++ src/pages/products/components/Step1Form.tsx | 11 +- src/pages/products/components/Step2Form.tsx | 11 +- src/pages/products/components/Step3Form.tsx | 16 +- src/pages/products/hooks/useProductData.ts | 23 +- src/pages/products/service/ProductService.ts | 21 ++ src/pages/products/types/Types.ts | 45 ++++ src/router/Main.tsx | 2 + 11 files changed, 337 insertions(+), 14 deletions(-) create mode 100644 src/pages/products/Update.tsx diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 01eeaed..e5ab3be 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -128,5 +128,6 @@ export const Pages = { create: "/products/create", list: "/products/list", variants: "/products/variants/", + update: "/products/update/", }, }; diff --git a/src/index.css b/src/index.css index 98fe4c7..632390c 100644 --- a/src/index.css +++ b/src/index.css @@ -16,7 +16,7 @@ body { overflow-x: hidden !important; background: #eceef6; font-weight: 300; - overflow: hidden; + overflow: hidden !important; } html, body, diff --git a/src/pages/products/List.tsx b/src/pages/products/List.tsx index 2e0c451..b875ac8 100644 --- a/src/pages/products/List.tsx +++ b/src/pages/products/List.tsx @@ -8,7 +8,7 @@ import PaginationUi from '../../components/PaginationUi'; import StatusWithText from '../../components/StatusWithText'; import Td from '../../components/Td'; import TrashWithConfrim from '../../components/TrashWithConfrim'; -import { Edit, Eye } from 'iconsax-react'; +import { Edit } from 'iconsax-react'; import Button from '@/components/Button'; import { Link } from 'react-router-dom'; import { Pages } from '@/config/Pages'; @@ -126,9 +126,11 @@ const List: FC = () => {
- - - + {product.status === ProductStatus.Draft && ( + + + + )} { diff --git a/src/pages/products/Update.tsx b/src/pages/products/Update.tsx new file mode 100644 index 0000000..f25fc42 --- /dev/null +++ b/src/pages/products/Update.tsx @@ -0,0 +1,209 @@ +import { type FC, useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useGetProductById } from './hooks/useProductData'; +import { useUpdateProductDetail, useUpdateProductAttribute, useUpdateProduct } from './hooks/useProductData'; +import { useGetBrands } from '../brand/hooks/useBrandData'; +import { useGetCategories } from '../category/hooks/useCategoryData'; +import { StepIndicator, Step1Form, Step2Form, Step3Form } from './components'; +import { type CreateProductDetailRequestType, type CreateProductAttributeRequestType, type SaveProductRequestType } from './types/Types'; +import { toast } from 'react-toastify'; +import { Pages } from '@/config/Pages'; +import { extractErrorMessage } from '@/helpers/utils'; + +const UpdateProduct: FC = () => { + const { id: productId } = useParams<{ id: string }>(); + const productIdNum = parseInt(productId!, 10); + const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1); + const [productData, setProductData] = useState>({}); + const navigate = useNavigate(); + + // Fetch existing product data + const { data: productDetails, isLoading: productLoading, error: productError } = useGetProductById(productId!); + + // API hooks + const updateProductDetailMutation = useUpdateProductDetail(); + const updateProductAttributeMutation = useUpdateProductAttribute(); + const updateProductMutation = useUpdateProduct(); + const { data: brandsData } = useGetBrands(); + const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories(); + + // Initialize product data when product details are loaded + useEffect(() => { + if (productDetails?.results?.product) { + const product = productDetails.results.product; + setProductData({ + productId: product._id, + category: product.category._id, + brand: product.brand._id, + model: product.model, + title_fa: product.title_fa, + title_en: product.title_en, + seoTitle: product.seoTitle || '', + source: product.source, + isFake: product.isFake, + attributes: product.specifications?.map(spec => ({ + id: 0, // This might need to be adjusted based on API + values: spec.values.map(() => 0) // This might need to be adjusted based on API + })) || [], + description: product.description, + metaDescription: product.metaDescription, + tags: product.tags, + advantages: product.advantages, + disAdvantages: product.disAdvantages, + imagesList: product.imagesUrl?.list || [], + coverImage: product.imagesUrl?.cover || '', + }); + } + }, [productDetails]); + + if (productLoading) { + return ( +
+
در حال بارگذاری اطلاعات محصول...
+
+ ); + } + + if (productError) { + return ( +
+
خطا در بارگذاری اطلاعات محصول
+
+ ); + } + + const handleStep1Submit = (data: CreateProductDetailRequestType) => { + const fullData = { + ...data, + productId: productIdNum + }; + + updateProductDetailMutation.mutate(fullData, { + onSuccess: () => { + setProductData(prev => ({ ...prev, ...data })); + setCurrentStep(2); + }, + onError: (error) => { + toast.error(extractErrorMessage(error)); + } + }); + }; + + const handleStep2Submit = (data: Omit) => { + const fullData = { + ...data, + productId: productIdNum + }; + + updateProductAttributeMutation.mutate(fullData, { + onSuccess: () => { + setProductData(prev => ({ ...prev, ...data })); + setCurrentStep(3); + }, + onError: (error) => { + toast.error(extractErrorMessage(error)); + } + }); + }; + + const handleStep3Submit = (data: Omit) => { + const fullData = { + ...data, + productId: productIdNum + }; + + updateProductMutation.mutate(fullData, { + onSuccess: () => { + toast.success('محصول با موفقیت بروزرسانی شد'); + navigate(Pages.products.list); + }, + onError: (error) => { + toast.error(extractErrorMessage(error)); + } + }); + }; + + const handlePreviousStep = () => { + if (currentStep > 1) { + setCurrentStep((prev) => (prev - 1) as 1 | 2 | 3); + } + }; + + const renderStep1 = () => { + const categories = categoriesData?.results?.data || []; + const brands = brandsData?.results?.brands || []; + + if (categoriesLoading) { + return
در حال بارگذاری دسته‌بندی‌ها...
; + } + + return ( + + ); + }; + + const renderStep2 = () => ( + + ); + + const renderStep3 = () => ( + + ); + + return ( +
+
+

بروزرسانی محصول

+
+ + + +
+
+ +
+ {currentStep === 1 && renderStep1()} + {currentStep === 2 && renderStep2()} + {currentStep === 3 && renderStep3()} +
+
+ ); +}; + +export default UpdateProduct; \ No newline at end of file diff --git a/src/pages/products/components/Step1Form.tsx b/src/pages/products/components/Step1Form.tsx index fbc2f8a..a29dbc0 100644 --- a/src/pages/products/components/Step1Form.tsx +++ b/src/pages/products/components/Step1Form.tsx @@ -1,4 +1,4 @@ -import { type FC, useState, useMemo } from 'react'; +import { type FC, useState, useMemo, useEffect } from 'react'; import Button from '../../../components/Button'; import Input from '../../../components/Input'; import Select from '../../../components/Select'; @@ -12,6 +12,7 @@ interface Step1FormProps { brands: { _id: string; title_fa: string }[]; categories: CategoryTreeNode[]; loading: boolean; + initialData?: Partial; } // تابع برای پیدا کردن همه دسته‌بندی‌های leaf از تمام سطح‌ها @@ -35,7 +36,7 @@ const findAllLeafCategories = (categories: CategoryTreeNode[]): CategoryTreeNode }; -const Step1Form: FC = ({ onSubmit, brands, categories, loading }) => { +const Step1Form: FC = ({ onSubmit, brands, categories, loading, initialData }) => { const [formData, setFormData] = useState({ category: '', brand: '', @@ -47,6 +48,12 @@ const Step1Form: FC = ({ onSubmit, brands, categories, loading } isFake: false }); + useEffect(() => { + if (initialData) { + setFormData(prev => ({ ...prev, ...initialData })); + } + }, [initialData]); + // پیدا کردن همه دسته‌بندی‌های leaf const leafCategories = useMemo(() => { if (!categories || categories.length === 0) { diff --git a/src/pages/products/components/Step2Form.tsx b/src/pages/products/components/Step2Form.tsx index 42fbdc7..c29f708 100644 --- a/src/pages/products/components/Step2Form.tsx +++ b/src/pages/products/components/Step2Form.tsx @@ -1,4 +1,4 @@ -import { type FC, useState } from 'react'; +import { type FC, useState, useEffect } from 'react'; import Button from '../../../components/Button'; import Input from '../../../components/Input'; import Textarea from '../../../components/Textarea'; @@ -66,6 +66,7 @@ interface Step2FormProps { loading: boolean; onPrevious: () => void; categoryId?: string; + initialData?: Partial>; } // کامپوننت مدیریت تگ‌ها @@ -197,7 +198,7 @@ const ListInput: FC = ({ items, onChange, placeholder, label }) ); }; -const Step2Form: FC = ({ onSubmit, loading, onPrevious, categoryId }) => { +const Step2Form: FC = ({ onSubmit, loading, onPrevious, categoryId, initialData }) => { const [formData, setFormData] = useState>({ attributes: [], description: '', @@ -207,6 +208,12 @@ const Step2Form: FC = ({ onSubmit, loading, onPrevious, category disAdvantages: [] }); + useEffect(() => { + if (initialData) { + setFormData(prev => ({ ...prev, ...initialData })); + } + }, [initialData]); + const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || ''); const handleAttributeChange = (attributeId: number, values: number[]) => { diff --git a/src/pages/products/components/Step3Form.tsx b/src/pages/products/components/Step3Form.tsx index a756dde..3efee33 100644 --- a/src/pages/products/components/Step3Form.tsx +++ b/src/pages/products/components/Step3Form.tsx @@ -1,4 +1,4 @@ -import { type FC, useState } from 'react'; +import { type FC, useState, useEffect } from 'react'; import Button from '../../../components/Button'; import UploadBoxDraggble from '../../../components/UploadBoxDraggble'; import UploadBox from '../../../components/UploadBox'; @@ -9,15 +9,23 @@ interface Step3FormProps { onSubmit: (data: Omit) => void; loading: boolean; onPrevious: () => void; + initialData?: Partial>; } -const Step3Form: FC = ({ onSubmit, loading, onPrevious }) => { +const Step3Form: FC = ({ onSubmit, loading, onPrevious, initialData }) => { const [uploadedImages, setUploadedImages] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [coverImage, setCoverImage] = useState(''); const [selectedCoverFile, setSelectedCoverFile] = useState(null); + useEffect(() => { + if (initialData) { + setUploadedImages(initialData.imagesList || []); + setCoverImage(initialData.coverImage || ''); + } + }, [initialData]); + const uploadMultipleMutation = useUploadMultiple(); const handleFileSelect = (files: File[]) => { @@ -149,7 +157,7 @@ const Step3Form: FC = ({ onSubmit, loading, onPrevious }) => {
-
+
diff --git a/src/pages/products/hooks/useProductData.ts b/src/pages/products/hooks/useProductData.ts index 6261657..62bb75b 100644 --- a/src/pages/products/hooks/useProductData.ts +++ b/src/pages/products/hooks/useProductData.ts @@ -4,7 +4,10 @@ import { type CreateProductDetailRequestType, type CreateProductAttributeRequestType, type SaveProductRequestType, - type UpdateVariantRequestType + type UpdateVariantRequestType, + type UpdateProductDetailRequestType, + type UpdateProductAttributeRequestType, + type UpdateProductRequestType } from "../types/Types"; export const useCreateProductDetail = () => { @@ -66,4 +69,22 @@ export const useGetVariantById = (productId: string, variantId: string) => { queryFn: () => api.getVariantById(productId, variantId), enabled: !!productId && !!variantId, }); +}; + +export const useUpdateProductDetail = () => { + return useMutation({ + mutationFn: (variables: UpdateProductDetailRequestType) => api.updateProductDetail(variables), + }); +}; + +export const useUpdateProductAttribute = () => { + return useMutation({ + mutationFn: (variables: UpdateProductAttributeRequestType) => api.updateProductAttribute(variables), + }); +}; + +export const useUpdateProduct = () => { + return useMutation({ + mutationFn: (variables: UpdateProductRequestType) => api.updateProduct(variables), + }); }; \ No newline at end of file diff --git a/src/pages/products/service/ProductService.ts b/src/pages/products/service/ProductService.ts index b326b3a..2c0b45a 100644 --- a/src/pages/products/service/ProductService.ts +++ b/src/pages/products/service/ProductService.ts @@ -12,6 +12,12 @@ import { type GetProductByIdResponseType, type UpdateVariantRequestType, type GetVariantByIdResponseType, + type UpdateProductDetailRequestType, + type UpdateProductDetailResponseType, + type UpdateProductAttributeRequestType, + type UpdateProductAttributeResponseType, + type UpdateProductRequestType, + type UpdateProductResponseType, } from "../types/Types"; export const createProductDetail = async (params: CreateProductDetailRequestType): Promise => { @@ -59,4 +65,19 @@ export const updateVariant = async (params: UpdateVariantRequestType) => { export const getVariantById = async (productId: string, variantId: string): Promise => { const { data } = await axios.get(`/admin/products/${productId}/variants/${variantId}`); return data; +}; + +export const updateProductDetail = async (params: UpdateProductDetailRequestType): Promise => { + const { data } = await axios.patch(`/admin/products/update/detail`, params); + return data; +}; + +export const updateProductAttribute = async (params: UpdateProductAttributeRequestType): Promise => { + const { data } = await axios.patch(`/admin/products/update/attribute`, params); + return data; +}; + +export const updateProduct = async (params: UpdateProductRequestType): Promise => { + const { data } = await axios.patch(`/admin/products/update/save`, params); + 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 ca2f1c8..b95f2fc 100644 --- a/src/pages/products/types/Types.ts +++ b/src/pages/products/types/Types.ts @@ -372,3 +372,48 @@ export type UpdateVariantRequestType = { specialSale_quantity: number; specialSale_endDate: string; }; + +// تایپ‌های مربوط به آپدیت محصول سه مرحله‌ای + +// مرحله اول: آپدیت جزئیات محصول +export type UpdateProductDetailRequestType = { + productId: number; +} & Omit & { + category?: string; + brand?: string; + }; + +export type UpdateProductDetailResponseType = { + status: number; + success: boolean; + results: { + message: string; + product: DraftProductType; + }; +}; + +// مرحله دوم: آپدیت ویژگی‌ها و توضیحات +export type UpdateProductAttributeRequestType = { + productId: number; +} & CreateProductAttributeRequestType; + +export type UpdateProductAttributeResponseType = { + status: number; + success: boolean; + results: { + success: boolean; + }; +}; + +// مرحله سوم: آپدیت تصاویر و ذخیره نهایی +export type UpdateProductRequestType = { + productId: number; +} & SaveProductRequestType; + +export type UpdateProductResponseType = { + status: number; + success: boolean; + results: { + success: boolean; + }; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 15f18b4..a20e3b3 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -13,6 +13,7 @@ import Variant from '../pages/category/Variant' import Attributes from '../pages/category/Attributes' import CreateProduct from '../pages/products/Create' import ListProduct from '../pages/products/List' +import UpdateProduct from '../pages/products/Update' import ProductVariant from '@/pages/products/ProductVariant' const MainRouter: FC = () => { @@ -39,6 +40,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> } />