update product
This commit is contained in:
@@ -128,5 +128,6 @@ export const Pages = {
|
||||
create: "/products/create",
|
||||
list: "/products/list",
|
||||
variants: "/products/variants/",
|
||||
update: "/products/update/",
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ body {
|
||||
overflow-x: hidden !important;
|
||||
background: #eceef6;
|
||||
font-weight: 300;
|
||||
overflow: hidden;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
|
||||
@@ -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 = () => {
|
||||
<Td text={String(product.default_variant?.stock || 0)} />
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye color='#8C90A3' size={19} />
|
||||
|
||||
<Edit color='#8C90A3' size={16} />
|
||||
{product.status === ProductStatus.Draft && (
|
||||
<Link to={`${Pages.products.update}${product._id}`}>
|
||||
<Edit color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<TrashWithConfrim
|
||||
onDelete={() => {
|
||||
|
||||
@@ -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<Partial<CreateProductDetailRequestType & CreateProductAttributeRequestType & SaveProductRequestType>>({});
|
||||
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 (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<div>در حال بارگذاری اطلاعات محصول...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (productError) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<div className="text-red-500">خطا در بارگذاری اطلاعات محصول</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<CreateProductAttributeRequestType, 'productId'>) => {
|
||||
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<SaveProductRequestType, 'productId'>) => {
|
||||
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 <div>در حال بارگذاری دستهبندیها...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Step1Form
|
||||
onSubmit={handleStep1Submit}
|
||||
brands={brands}
|
||||
categories={categories}
|
||||
loading={updateProductDetailMutation.isPending}
|
||||
initialData={{
|
||||
category: productData.category,
|
||||
brand: productData.brand,
|
||||
model: productData.model,
|
||||
title_fa: productData.title_fa,
|
||||
title_en: productData.title_en,
|
||||
seoTitle: productData.seoTitle,
|
||||
source: productData.source,
|
||||
isFake: productData.isFake,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStep2 = () => (
|
||||
<Step2Form
|
||||
onSubmit={handleStep2Submit}
|
||||
loading={updateProductAttributeMutation.isPending}
|
||||
onPrevious={handlePreviousStep}
|
||||
categoryId={productData.category}
|
||||
initialData={{
|
||||
attributes: productData.attributes,
|
||||
description: productData.description,
|
||||
metaDescription: productData.metaDescription,
|
||||
tags: productData.tags,
|
||||
advantages: productData.advantages,
|
||||
disAdvantages: productData.disAdvantages,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderStep3 = () => (
|
||||
<Step3Form
|
||||
onSubmit={handleStep3Submit}
|
||||
loading={updateProductMutation.isPending}
|
||||
onPrevious={handlePreviousStep}
|
||||
initialData={{
|
||||
imagesList: productData.imagesList,
|
||||
coverImage: productData.coverImage,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">بروزرسانی محصول</h1>
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<StepIndicator step={1} currentStep={currentStep} title="جزئیات محصول" />
|
||||
<StepIndicator step={2} currentStep={currentStep} title="ویژگیها و توضیحات" />
|
||||
<StepIndicator step={3} currentStep={currentStep} title="تصاویر و ذخیره" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
|
||||
{currentStep === 1 && renderStep1()}
|
||||
{currentStep === 2 && renderStep2()}
|
||||
{currentStep === 3 && renderStep3()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateProduct;
|
||||
@@ -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<CreateProductDetailRequestType>;
|
||||
}
|
||||
|
||||
// تابع برای پیدا کردن همه دستهبندیهای leaf از تمام سطحها
|
||||
@@ -35,7 +36,7 @@ const findAllLeafCategories = (categories: CategoryTreeNode[]): CategoryTreeNode
|
||||
};
|
||||
|
||||
|
||||
const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading }) => {
|
||||
const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading, initialData }) => {
|
||||
const [formData, setFormData] = useState<CreateProductDetailRequestType>({
|
||||
category: '',
|
||||
brand: '',
|
||||
@@ -47,6 +48,12 @@ const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading }
|
||||
isFake: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setFormData(prev => ({ ...prev, ...initialData }));
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
// پیدا کردن همه دستهبندیهای leaf
|
||||
const leafCategories = useMemo(() => {
|
||||
if (!categories || categories.length === 0) {
|
||||
|
||||
@@ -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<Omit<CreateProductAttributeRequestType, 'productId'>>;
|
||||
}
|
||||
|
||||
// کامپوننت مدیریت تگها
|
||||
@@ -197,7 +198,7 @@ const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label })
|
||||
);
|
||||
};
|
||||
|
||||
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId }) => {
|
||||
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId, initialData }) => {
|
||||
const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>({
|
||||
attributes: [],
|
||||
description: '',
|
||||
@@ -207,6 +208,12 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
|
||||
disAdvantages: []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setFormData(prev => ({ ...prev, ...initialData }));
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || '');
|
||||
|
||||
const handleAttributeChange = (attributeId: number, values: number[]) => {
|
||||
|
||||
@@ -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<SaveProductRequestType, 'productId'>) => void;
|
||||
loading: boolean;
|
||||
onPrevious: () => void;
|
||||
initialData?: Partial<Omit<SaveProductRequestType, 'productId'>>;
|
||||
}
|
||||
|
||||
const Step3Form: FC<Step3FormProps> = ({ onSubmit, loading, onPrevious }) => {
|
||||
const Step3Form: FC<Step3FormProps> = ({ onSubmit, loading, onPrevious, initialData }) => {
|
||||
|
||||
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [coverImage, setCoverImage] = useState<string>('');
|
||||
const [selectedCoverFile, setSelectedCoverFile] = useState<File | null>(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<Step3FormProps> = ({ onSubmit, loading, onPrevious }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-4 justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -162,7 +170,7 @@ const Step3Form: FC<Step3FormProps> = ({ onSubmit, loading, onPrevious }) => {
|
||||
isLoading={loading || uploadMultipleMutation.isPending}
|
||||
disabled={selectedFiles.length === 0 && uploadedImages.length === 0 && !selectedCoverFile && !coverImage}
|
||||
>
|
||||
ایجاد محصول
|
||||
بروزرسانی محصول
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
};
|
||||
@@ -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<CreateProductDetailResponseType> => {
|
||||
@@ -59,4 +65,19 @@ export const updateVariant = async (params: UpdateVariantRequestType) => {
|
||||
export const getVariantById = async (productId: string, variantId: string): Promise<GetVariantByIdResponseType> => {
|
||||
const { data } = await axios.get(`/admin/products/${productId}/variants/${variantId}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateProductDetail = async (params: UpdateProductDetailRequestType): Promise<UpdateProductDetailResponseType> => {
|
||||
const { data } = await axios.patch(`/admin/products/update/detail`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateProductAttribute = async (params: UpdateProductAttributeRequestType): Promise<UpdateProductAttributeResponseType> => {
|
||||
const { data } = await axios.patch(`/admin/products/update/attribute`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateProduct = async (params: UpdateProductRequestType): Promise<UpdateProductResponseType> => {
|
||||
const { data } = await axios.patch(`/admin/products/update/save`, params);
|
||||
return data;
|
||||
};
|
||||
@@ -372,3 +372,48 @@ export type UpdateVariantRequestType = {
|
||||
specialSale_quantity: number;
|
||||
specialSale_endDate: string;
|
||||
};
|
||||
|
||||
// تایپهای مربوط به آپدیت محصول سه مرحلهای
|
||||
|
||||
// مرحله اول: آپدیت جزئیات محصول
|
||||
export type UpdateProductDetailRequestType = {
|
||||
productId: number;
|
||||
} & Omit<CreateProductDetailRequestType, "category" | "brand"> & {
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
|
||||
<Route path={Pages.products.create} element={<CreateProduct />} />
|
||||
<Route path={Pages.products.list} element={<ListProduct />} />
|
||||
<Route path={`${Pages.products.update}:id`} element={<UpdateProduct />} />
|
||||
<Route path={`${Pages.products.variants}:id`} element={<ProductVariant />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user