update product

This commit is contained in:
hamid zarghami
2025-09-24 09:02:08 +03:30
parent a71d521c7c
commit 0dd1ab170d
11 changed files with 337 additions and 14 deletions
+1
View File
@@ -128,5 +128,6 @@ export const Pages = {
create: "/products/create", create: "/products/create",
list: "/products/list", list: "/products/list",
variants: "/products/variants/", variants: "/products/variants/",
update: "/products/update/",
}, },
}; };
+1 -1
View File
@@ -16,7 +16,7 @@ body {
overflow-x: hidden !important; overflow-x: hidden !important;
background: #eceef6; background: #eceef6;
font-weight: 300; font-weight: 300;
overflow: hidden; overflow: hidden !important;
} }
html, html,
body, body,
+6 -4
View File
@@ -8,7 +8,7 @@ import PaginationUi from '../../components/PaginationUi';
import StatusWithText from '../../components/StatusWithText'; import StatusWithText from '../../components/StatusWithText';
import Td from '../../components/Td'; import Td from '../../components/Td';
import TrashWithConfrim from '../../components/TrashWithConfrim'; import TrashWithConfrim from '../../components/TrashWithConfrim';
import { Edit, Eye } from 'iconsax-react'; import { Edit } from 'iconsax-react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Pages } from '@/config/Pages'; import { Pages } from '@/config/Pages';
@@ -126,9 +126,11 @@ const List: FC = () => {
<Td text={String(product.default_variant?.stock || 0)} /> <Td text={String(product.default_variant?.stock || 0)} />
<Td text=""> <Td text="">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Eye color='#8C90A3' size={19} /> {product.status === ProductStatus.Draft && (
<Link to={`${Pages.products.update}${product._id}`}>
<Edit color='#8C90A3' size={16} /> <Edit color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500" />
</Link>
)}
<TrashWithConfrim <TrashWithConfrim
onDelete={() => { onDelete={() => {
+209
View File
@@ -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;
+9 -2
View File
@@ -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 Button from '../../../components/Button';
import Input from '../../../components/Input'; import Input from '../../../components/Input';
import Select from '../../../components/Select'; import Select from '../../../components/Select';
@@ -12,6 +12,7 @@ interface Step1FormProps {
brands: { _id: string; title_fa: string }[]; brands: { _id: string; title_fa: string }[];
categories: CategoryTreeNode[]; categories: CategoryTreeNode[];
loading: boolean; loading: boolean;
initialData?: Partial<CreateProductDetailRequestType>;
} }
// تابع برای پیدا کردن همه دسته‌بندی‌های leaf از تمام سطح‌ها // تابع برای پیدا کردن همه دسته‌بندی‌های 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>({ const [formData, setFormData] = useState<CreateProductDetailRequestType>({
category: '', category: '',
brand: '', brand: '',
@@ -47,6 +48,12 @@ const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading }
isFake: false isFake: false
}); });
useEffect(() => {
if (initialData) {
setFormData(prev => ({ ...prev, ...initialData }));
}
}, [initialData]);
// پیدا کردن همه دسته‌بندی‌های leaf // پیدا کردن همه دسته‌بندی‌های leaf
const leafCategories = useMemo(() => { const leafCategories = useMemo(() => {
if (!categories || categories.length === 0) { if (!categories || categories.length === 0) {
+9 -2
View File
@@ -1,4 +1,4 @@
import { type FC, useState } from 'react'; import { type FC, useState, useEffect } from 'react';
import Button from '../../../components/Button'; import Button from '../../../components/Button';
import Input from '../../../components/Input'; import Input from '../../../components/Input';
import Textarea from '../../../components/Textarea'; import Textarea from '../../../components/Textarea';
@@ -66,6 +66,7 @@ interface Step2FormProps {
loading: boolean; loading: boolean;
onPrevious: () => void; onPrevious: () => void;
categoryId?: string; 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'>>({ const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>({
attributes: [], attributes: [],
description: '', description: '',
@@ -207,6 +208,12 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
disAdvantages: [] disAdvantages: []
}); });
useEffect(() => {
if (initialData) {
setFormData(prev => ({ ...prev, ...initialData }));
}
}, [initialData]);
const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || ''); const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || '');
const handleAttributeChange = (attributeId: number, values: number[]) => { const handleAttributeChange = (attributeId: number, values: number[]) => {
+12 -4
View File
@@ -1,4 +1,4 @@
import { type FC, useState } from 'react'; import { type FC, useState, useEffect } from 'react';
import Button from '../../../components/Button'; import Button from '../../../components/Button';
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'; import UploadBoxDraggble from '../../../components/UploadBoxDraggble';
import UploadBox from '../../../components/UploadBox'; import UploadBox from '../../../components/UploadBox';
@@ -9,15 +9,23 @@ interface Step3FormProps {
onSubmit: (data: Omit<SaveProductRequestType, 'productId'>) => void; onSubmit: (data: Omit<SaveProductRequestType, 'productId'>) => void;
loading: boolean; loading: boolean;
onPrevious: () => void; 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 [uploadedImages, setUploadedImages] = useState<string[]>([]);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [coverImage, setCoverImage] = useState<string>(''); const [coverImage, setCoverImage] = useState<string>('');
const [selectedCoverFile, setSelectedCoverFile] = useState<File | null>(null); const [selectedCoverFile, setSelectedCoverFile] = useState<File | null>(null);
useEffect(() => {
if (initialData) {
setUploadedImages(initialData.imagesList || []);
setCoverImage(initialData.coverImage || '');
}
}, [initialData]);
const uploadMultipleMutation = useUploadMultiple(); const uploadMultipleMutation = useUploadMultiple();
const handleFileSelect = (files: File[]) => { const handleFileSelect = (files: File[]) => {
@@ -149,7 +157,7 @@ const Step3Form: FC<Step3FormProps> = ({ onSubmit, loading, onPrevious }) => {
</div> </div>
</div> </div>
<div className="flex justify-between"> <div className="flex gap-4 justify-between">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -162,7 +170,7 @@ const Step3Form: FC<Step3FormProps> = ({ onSubmit, loading, onPrevious }) => {
isLoading={loading || uploadMultipleMutation.isPending} isLoading={loading || uploadMultipleMutation.isPending}
disabled={selectedFiles.length === 0 && uploadedImages.length === 0 && !selectedCoverFile && !coverImage} disabled={selectedFiles.length === 0 && uploadedImages.length === 0 && !selectedCoverFile && !coverImage}
> >
ایجاد محصول بروزرسانی محصول
</Button> </Button>
</div> </div>
</form> </form>
+22 -1
View File
@@ -4,7 +4,10 @@ import {
type CreateProductDetailRequestType, type CreateProductDetailRequestType,
type CreateProductAttributeRequestType, type CreateProductAttributeRequestType,
type SaveProductRequestType, type SaveProductRequestType,
type UpdateVariantRequestType type UpdateVariantRequestType,
type UpdateProductDetailRequestType,
type UpdateProductAttributeRequestType,
type UpdateProductRequestType
} from "../types/Types"; } from "../types/Types";
export const useCreateProductDetail = () => { export const useCreateProductDetail = () => {
@@ -66,4 +69,22 @@ export const useGetVariantById = (productId: string, variantId: string) => {
queryFn: () => api.getVariantById(productId, variantId), queryFn: () => api.getVariantById(productId, variantId),
enabled: !!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 GetProductByIdResponseType,
type UpdateVariantRequestType, type UpdateVariantRequestType,
type GetVariantByIdResponseType, type GetVariantByIdResponseType,
type UpdateProductDetailRequestType,
type UpdateProductDetailResponseType,
type UpdateProductAttributeRequestType,
type UpdateProductAttributeResponseType,
type UpdateProductRequestType,
type UpdateProductResponseType,
} from "../types/Types"; } from "../types/Types";
export const createProductDetail = async (params: CreateProductDetailRequestType): Promise<CreateProductDetailResponseType> => { 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> => { export const getVariantById = async (productId: string, variantId: string): Promise<GetVariantByIdResponseType> => {
const { data } = await axios.get(`/admin/products/${productId}/variants/${variantId}`); const { data } = await axios.get(`/admin/products/${productId}/variants/${variantId}`);
return data; 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;
}; };
+45
View File
@@ -372,3 +372,48 @@ export type UpdateVariantRequestType = {
specialSale_quantity: number; specialSale_quantity: number;
specialSale_endDate: string; 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;
};
};
+2
View File
@@ -13,6 +13,7 @@ import Variant from '../pages/category/Variant'
import Attributes from '../pages/category/Attributes' import Attributes from '../pages/category/Attributes'
import CreateProduct from '../pages/products/Create' import CreateProduct from '../pages/products/Create'
import ListProduct from '../pages/products/List' import ListProduct from '../pages/products/List'
import UpdateProduct from '../pages/products/Update'
import ProductVariant from '@/pages/products/ProductVariant' import ProductVariant from '@/pages/products/ProductVariant'
const MainRouter: FC = () => { const MainRouter: FC = () => {
@@ -39,6 +40,7 @@ const MainRouter: FC = () => {
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} /> <Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
<Route path={Pages.products.create} element={<CreateProduct />} /> <Route path={Pages.products.create} element={<CreateProduct />} />
<Route path={Pages.products.list} element={<ListProduct />} /> <Route path={Pages.products.list} element={<ListProduct />} />
<Route path={`${Pages.products.update}:id`} element={<UpdateProduct />} />
<Route path={`${Pages.products.variants}:id`} element={<ProductVariant />} /> <Route path={`${Pages.products.variants}:id`} element={<ProductVariant />} />
</Routes> </Routes>
</div> </div>