navigate back
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-06-29 10:43:01 +03:30
parent d12b36d8dc
commit 901a1b5dd2
3 changed files with 246 additions and 270 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
VITE_TOKEN_NAME = 'dkala_a_t' VITE_TOKEN_NAME = 'dkala_a_t'
VITE_REFRESH_TOKEN_NAME = 'dkala-a-rt' VITE_REFRESH_TOKEN_NAME = 'dkala-a-rt'
VITE_BASE_URL = 'https://dkala-api.danakcorp.com' # VITE_BASE_URL = 'https://dkala-api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.99.131:4000' VITE_BASE_URL = 'http://192.168.99.131:4000'
VITE_SOCKET_URL = 'https://dkala-api.danakcorp.com' VITE_SOCKET_URL = 'https://dkala-api.danakcorp.com'
+67 -84
View File
@@ -1,38 +1,36 @@
import { type FC, useState } from 'react' import Button from "@/components/Button";
import { useFormik } from 'formik' import type { ErrorType } from "@/helpers/types";
import * as Yup from 'yup' import { useFormik } from "formik";
import { TickCircle } from 'iconsax-react' import { TickCircle } from "iconsax-react";
import { toast } from 'react-toastify' import { type FC, useState } from "react";
import Button from '@/components/Button' import { useNavigate } from "react-router-dom";
import CreateProductSidebar from './components/CreateProductSidebar' import { toast } from "react-toastify";
import BasicInfoSection from './components/BasicInfoSection' import * as Yup from "yup";
import type { CreateProductType } from './types/Types' import { useMultipleUpload } from "../uploader/hooks/useUploaderData";
import { useCreateProduct, useGetCategories } from './hooks/useProductData' import BasicInfoSection from "./components/BasicInfoSection";
import { useMultipleUpload } from '../uploader/hooks/useUploaderData' import CreateProductSidebar from "./components/CreateProductSidebar";
import { Pages } from '@/config/Pages' import { PaymentType, PricingType, PurchaseUnit } from "./enum/Enum";
import type { ErrorType } from '@/helpers/types' import { useCreateProduct, useGetCategories } from "./hooks/useProductData";
import { PaymentType, PricingType, PurchaseUnit } from './enum/Enum' import type { CreateProductType } from "./types/Types";
const CreateProduct: FC = () => { const CreateProduct: FC = () => {
const navigate = useNavigate();
const { data } = useGetCategories(); const { data } = useGetCategories();
const [isActive, setIsActive] = useState<boolean>(true) const [isActive, setIsActive] = useState<boolean>(true);
const [isSpecial, setIsSpecial] = useState<boolean>(false) const [isSpecial, setIsSpecial] = useState<boolean>(false);
const [imageFiles, setImageFiles] = useState<File[]>([]) const [imageFiles, setImageFiles] = useState<File[]>([]);
const { mutate: createProduct, isPending } = useCreateProduct() const { mutate: createProduct, isPending } = useCreateProduct();
const { mutate: multipleUpload, isPending: isUploading } = useMultipleUpload() const { mutate: multipleUpload, isPending: isUploading } = useMultipleUpload();
const categories = data?.data ?? [] const categories = data?.data ?? [];
const formik = useFormik<CreateProductType>({ const formik = useFormik<CreateProductType>({
initialValues: { initialValues: {
categoryId: '', categoryId: "",
title: '', title: "",
attribute: '', attribute: "",
variants: [], variants: [],
desc: '', desc: "",
price: 0, price: 0,
isActive: true, isActive: true,
images: [], images: [],
@@ -49,36 +47,33 @@ const CreateProduct: FC = () => {
needAdminAcceptance: false, needAdminAcceptance: false,
}, },
validationSchema: Yup.object().shape({ validationSchema: Yup.object().shape({
title: Yup.string().required('نام کالا الزامی است'), title: Yup.string().required("نام کالا الزامی است"),
categoryId: Yup.string().required('دسته‌بندی الزامی است'), categoryId: Yup.string().required("دسته‌بندی الزامی است"),
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'), price: Yup.number().required("قیمت الزامی است").min(0, "قیمت باید مثبت باشد"),
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'), discount: Yup.number().min(0, "تخفیف باید مثبت باشد"),
attribute: Yup.string(), attribute: Yup.string(),
pricingType: Yup.string().required(), pricingType: Yup.string().required(),
paymentType: Yup.string().required(), paymentType: Yup.string().required(),
minPurchaseQuantity: Yup.number().when('pricingType', { minPurchaseQuantity: Yup.number().when("pricingType", {
is: PricingType.VARIABLE, is: PricingType.VARIABLE,
then: (schema) => schema.min(0, 'حداقل مقدار باید مثبت باشد'), then: (schema) => schema.min(0, "حداقل مقدار باید مثبت باشد"),
}), }),
maxPurchaseQuantity: Yup.number().min(0, 'حداکثر مقدار باید مثبت باشد'), maxPurchaseQuantity: Yup.number().min(0, "حداکثر مقدار باید مثبت باشد"),
purchasePitch: Yup.number().when('pricingType', { purchasePitch: Yup.number().when("pricingType", {
is: PricingType.VARIABLE, is: PricingType.VARIABLE,
then: (schema) => schema.min(0, 'گام تغییر باید مثبت باشد'), then: (schema) => schema.min(0, "گام تغییر باید مثبت باشد"),
}), }),
variants: Yup.array().of( variants: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
id: Yup.string(), id: Yup.string(),
value: Yup.string(), value: Yup.string(),
price: Yup.number().min(0) price: Yup.number().min(0),
}) }),
) ),
}), }),
onSubmit: (values) => { onSubmit: (values) => {
const submitProduct = (imageUrls: string[] = []) => { const submitProduct = (imageUrls: string[] = []) => {
const variants = const variants = values.variants.length > 0 ? values.variants : [{ value: "", price: values.price, ...(values.stock !== undefined && values.stock !== null && { stock: values.stock }) }];
values.variants.length > 0
? values.variants
: [{ value: '', price: values.price, ...(values.stock !== undefined && values.stock !== null && { stock: values.stock }) }]
const productData: CreateProductType = { const productData: CreateProductType = {
...values, ...values,
variants, variants,
@@ -91,74 +86,62 @@ const CreateProduct: FC = () => {
minPurchaseQuantity: values.minPurchaseQuantity ?? 1, minPurchaseQuantity: values.minPurchaseQuantity ?? 1,
purchasePitch: undefined, purchasePitch: undefined,
}), }),
} };
createProduct(productData, { createProduct(productData, {
onSuccess: () => { onSuccess: () => {
toast.success('کالا با موفقیت ایجاد شد') toast.success("کالا با موفقیت ایجاد شد");
window.location.href = Pages.products.list navigate(-1);
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast.error(error?.response?.data?.error?.message[0] || 'خطا در ایجاد کالا') toast.error(error?.response?.data?.error?.message[0] || "خطا در ایجاد کالا");
} },
}) });
} };
if (imageFiles.length > 0) { if (imageFiles.length > 0) {
multipleUpload(imageFiles, { multipleUpload(imageFiles, {
onSuccess: (response) => { onSuccess: (response) => {
const imageUrls = response?.data?.map(item => item.url) || [] const imageUrls = response?.data?.map((item) => item.url) || [];
submitProduct(imageUrls) submitProduct(imageUrls);
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast.error(error?.response?.data?.error?.message[0] || 'خطا در آپلود تصاویر') toast.error(error?.response?.data?.error?.message[0] || "خطا در آپلود تصاویر");
} },
}) });
} else { } else {
submitProduct() submitProduct();
} }
} },
}) });
return ( return (
<div className='w-full mt-4'> <div className="w-full mt-4">
<div className='flex flex-col sm:flex-row w-full justify-between items-start sm:items-center gap-4'> <div className="flex flex-col sm:flex-row w-full justify-between items-start sm:items-center gap-4">
<div className='text-lg font-light'> <div className="text-lg font-light">کالای جدید</div>
کالای جدید
</div>
<div> <div>
<Button <Button className="px-5 w-full sm:w-auto" onClick={() => formik.handleSubmit()} isloading={isPending || isUploading}>
className='px-5 w-full sm:w-auto' <div className="flex gap-2 items-center">
onClick={() => formik.handleSubmit()} <TickCircle className="size-5" color="white" />
isloading={isPending || isUploading}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
<div>ثبت کالا</div> <div>ثبت کالا</div>
</div> </div>
</Button> </Button>
</div> </div>
</div> </div>
<div className='flex flex-col lg:flex-row gap-6 mt-6'> <div className="flex flex-col lg:flex-row gap-6 mt-6">
<div className='flex-1'> <div className="flex-1">
<div className='bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl'> <div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
<BasicInfoSection formik={formik} categories={categories} /> <BasicInfoSection formik={formik} categories={categories} />
</div> </div>
</div> </div>
<div className='w-full lg:w-auto'> <div className="w-full lg:w-auto">
<CreateProductSidebar <CreateProductSidebar isActive={isActive} isSpecial={isSpecial} onChangeActive={setIsActive} onChangeSpecial={setIsSpecial} onChangeImages={setImageFiles} />
isActive={isActive}
isSpecial={isSpecial}
onChangeActive={setIsActive}
onChangeSpecial={setIsSpecial}
onChangeImages={setImageFiles}
/>
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default CreateProduct export default CreateProduct;
+103 -110
View File
@@ -1,44 +1,43 @@
import { type FC, useState, useEffect } from 'react' import Button from "@/components/Button";
import { useFormik } from 'formik' import { extractErrorMessage } from "@/config/func";
import * as Yup from 'yup' import type { ErrorType } from "@/helpers/types";
import { TickCircle } from 'iconsax-react' import { useQueryClient } from "@tanstack/react-query";
import { toast } from 'react-toastify' import { useFormik } from "formik";
import { useNavigate, useParams } from 'react-router-dom' import { TickCircle } from "iconsax-react";
import Button from '@/components/Button' import { type FC, useEffect, useState } from "react";
import CreateProductSidebar from './components/CreateProductSidebar' import { useNavigate, useParams } from "react-router-dom";
import BasicInfoSection from './components/BasicInfoSection' import { toast } from "react-toastify";
import type { CreateProductType } from './types/Types' import * as Yup from "yup";
import { useQueryClient } from '@tanstack/react-query' import { useMultipleUpload } from "../uploader/hooks/useUploaderData";
import { useUpdateProduct, useGetProductDetails, useGetCategories } from './hooks/useProductData' import BasicInfoSection from "./components/BasicInfoSection";
import { useMultipleUpload } from '../uploader/hooks/useUploaderData' import CreateProductSidebar from "./components/CreateProductSidebar";
import { Pages } from '@/config/Pages' import { PaymentType, PricingType, PurchaseUnit } from "./enum/Enum";
import type { ErrorType } from '@/helpers/types' import { useGetCategories, useGetProductDetails, useUpdateProduct } from "./hooks/useProductData";
import { extractErrorMessage } from '@/config/func' import type { CreateProductType } from "./types/Types";
import { PaymentType, PricingType, PurchaseUnit } from './enum/Enum'
const UpdateProduct: FC = () => { const UpdateProduct: FC = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient() const queryClient = useQueryClient();
const { data: productData, isLoading } = useGetProductDetails(id || '') const { data: productData, isLoading } = useGetProductDetails(id || "");
const { data: categoriesData } = useGetCategories() const { data: categoriesData } = useGetCategories();
const [isActive, setIsActive] = useState<boolean>(true) const [isActive, setIsActive] = useState<boolean>(true);
const [isSpecial, setIsSpecial] = useState<boolean>(false) const [isSpecial, setIsSpecial] = useState<boolean>(false);
const [imageFiles, setImageFiles] = useState<File[]>([]) const [imageFiles, setImageFiles] = useState<File[]>([]);
const [existingImages, setExistingImages] = useState<string[]>([]) const [existingImages, setExistingImages] = useState<string[]>([]);
const { mutate: updateProduct, isPending } = useUpdateProduct() const { mutate: updateProduct, isPending } = useUpdateProduct();
const { mutate: multipleUpload, isPending: isUploading } = useMultipleUpload() const { mutate: multipleUpload, isPending: isUploading } = useMultipleUpload();
const navigate = useNavigate() const navigate = useNavigate();
const categories = categoriesData?.data ?? [] const categories = categoriesData?.data ?? [];
const product = productData?.data const product = productData?.data;
const formik = useFormik<CreateProductType>({ const formik = useFormik<CreateProductType>({
initialValues: { initialValues: {
categoryId: '', categoryId: "",
title: '', title: "",
attribute: '', attribute: "",
variants: [], variants: [],
desc: '', desc: "",
price: 0, price: 0,
isActive: true, isActive: true,
images: [], images: [],
@@ -55,47 +54,47 @@ const UpdateProduct: FC = () => {
needAdminAcceptance: false, needAdminAcceptance: false,
}, },
validationSchema: Yup.object().shape({ validationSchema: Yup.object().shape({
title: Yup.string().required('نام کالا الزامی است'), title: Yup.string().required("نام کالا الزامی است"),
categoryId: Yup.string().required('دسته‌بندی الزامی است'), categoryId: Yup.string().required("دسته‌بندی الزامی است"),
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'), price: Yup.number().required("قیمت الزامی است").min(0, "قیمت باید مثبت باشد"),
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'), discount: Yup.number().min(0, "تخفیف باید مثبت باشد"),
attribute: Yup.string(), attribute: Yup.string(),
pricingType: Yup.string().required(), pricingType: Yup.string().required(),
paymentType: Yup.string().required(), paymentType: Yup.string().required(),
minPurchaseQuantity: Yup.number().when('pricingType', { minPurchaseQuantity: Yup.number().when("pricingType", {
is: PricingType.VARIABLE, is: PricingType.VARIABLE,
then: (schema) => schema.min(0, 'حداقل مقدار باید مثبت باشد'), then: (schema) => schema.min(0, "حداقل مقدار باید مثبت باشد"),
}), }),
maxPurchaseQuantity: Yup.number().min(0, 'حداکثر مقدار باید مثبت باشد'), maxPurchaseQuantity: Yup.number().min(0, "حداکثر مقدار باید مثبت باشد"),
purchasePitch: Yup.number().when('pricingType', { purchasePitch: Yup.number().when("pricingType", {
is: PricingType.VARIABLE, is: PricingType.VARIABLE,
then: (schema) => schema.min(0, 'گام تغییر باید مثبت باشد'), then: (schema) => schema.min(0, "گام تغییر باید مثبت باشد"),
}), }),
variants: Yup.array().of( variants: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
id: Yup.string(), id: Yup.string(),
value: Yup.string(), value: Yup.string(),
price: Yup.number().min(0) price: Yup.number().min(0),
}) }),
) ),
}), }),
onSubmit: (values) => { onSubmit: (values) => {
if (!id) { if (!id) {
toast.error('شناسه کالا یافت نشد') toast.error("شناسه کالا یافت نشد");
return return;
} }
const submitProduct = (newImageUrls: string[] = []) => { const submitProduct = (newImageUrls: string[] = []) => {
const allImages = [...values.images, ...newImageUrls] const allImages = [...values.images, ...newImageUrls];
const variants = const variants =
values.variants.length > 0 values.variants.length > 0
? values.variants.map((v) => ({ ? values.variants.map((v) => ({
...(v.id && { id: v.id }), ...(v.id && { id: v.id }),
value: v.value ?? '', value: v.value ?? "",
price: v.price ?? 0, price: v.price ?? 0,
...(v.stock !== undefined && v.stock !== null && { stock: v.stock }) ...(v.stock !== undefined && v.stock !== null && { stock: v.stock }),
})) }))
: [{ value: '', price: values.price, ...(values.stock !== undefined && values.stock !== null && { stock: values.stock }) }] : [{ value: "", price: values.price, ...(values.stock !== undefined && values.stock !== null && { stock: values.stock }) }];
const productData: CreateProductType = { const productData: CreateProductType = {
...values, ...values,
variants, variants,
@@ -108,47 +107,50 @@ const UpdateProduct: FC = () => {
minPurchaseQuantity: values.minPurchaseQuantity ?? 1, minPurchaseQuantity: values.minPurchaseQuantity ?? 1,
purchasePitch: undefined, purchasePitch: undefined,
}), }),
} };
updateProduct({ id, params: productData }, { updateProduct(
{ id, params: productData },
{
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['product', id] }) queryClient.invalidateQueries({ queryKey: ["product", id] });
queryClient.invalidateQueries({ queryKey: ['products'] }) queryClient.invalidateQueries({ queryKey: ["products"] });
toast.success('کالا با موفقیت به‌روزرسانی شد') toast.success("کالا با موفقیت به‌روزرسانی شد");
navigate(Pages.products.list) navigate(-1);
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error)) toast.error(extractErrorMessage(error));
} },
}) },
} );
};
if (imageFiles.length > 0) { if (imageFiles.length > 0) {
multipleUpload(imageFiles, { multipleUpload(imageFiles, {
onSuccess: (response) => { onSuccess: (response) => {
const imageUrls = response?.data?.map(item => item.url) || [] const imageUrls = response?.data?.map((item) => item.url) || [];
submitProduct(imageUrls) submitProduct(imageUrls);
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error, 'خطا در آپلود تصاویر')) toast.error(extractErrorMessage(error, "خطا در آپلود تصاویر"));
} },
}) });
} else { } else {
submitProduct() submitProduct();
} }
} },
}) });
useEffect(() => { useEffect(() => {
if (product) { if (product) {
const variants = product.variants || [] const variants = product.variants || [];
const priceFromVariant = variants.length > 0 ? variants[0].price : product.price const priceFromVariant = variants.length > 0 ? variants[0].price : product.price;
formik.setValues({ formik.setValues({
categoryId: product.category?.id || '', categoryId: product.category?.id || "",
title: product.title || '', title: product.title || "",
attribute: product.attribute || '', attribute: product.attribute || "",
variants, variants,
desc: product.desc || '', desc: product.desc || "",
price: priceFromVariant ?? product.price ?? 0, price: priceFromVariant ?? product.price ?? 0,
isActive: product.isActive || false, isActive: product.isActive || false,
images: product.images || [], images: product.images || [],
@@ -163,70 +165,61 @@ const UpdateProduct: FC = () => {
paymentType: product.paymentType ?? (product.needAdminAcceptance ? PaymentType.AFTER_PREPARATION : PaymentType.INSTANT), paymentType: product.paymentType ?? (product.needAdminAcceptance ? PaymentType.AFTER_PREPARATION : PaymentType.INSTANT),
purchaseUnit: product.purchaseUnit ?? PurchaseUnit.NUMBER, purchaseUnit: product.purchaseUnit ?? PurchaseUnit.NUMBER,
needAdminAcceptance: product.needAdminAcceptance ?? false, needAdminAcceptance: product.needAdminAcceptance ?? false,
}) });
setIsActive(product.isActive || false) setIsActive(product.isActive || false);
setIsSpecial(product.isSpecialOffer || false) setIsSpecial(product.isSpecialOffer || false);
setExistingImages(product.images || []) setExistingImages(product.images || []);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [product]) }, [product]);
useEffect(() => { useEffect(() => {
formik.setFieldValue('images', existingImages) formik.setFieldValue("images", existingImages);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingImages]) }, [existingImages]);
if (isLoading) { if (isLoading) {
return ( return (
<div className='w-full mt-4 flex justify-center items-center'> <div className="w-full mt-4 flex justify-center items-center">
<div>در حال بارگذاری...</div> <div>در حال بارگذاری...</div>
</div> </div>
) );
} }
if (!product) { if (!product) {
return ( return (
<div className='w-full mt-4 flex justify-center items-center'> <div className="w-full mt-4 flex justify-center items-center">
<div>کالا یافت نشد</div> <div>کالا یافت نشد</div>
</div> </div>
) );
} }
return ( return (
<div className='w-full mt-4'> <div className="w-full mt-4">
<div className='flex flex-col sm:flex-row w-full justify-between items-start sm:items-center gap-4'> <div className="flex flex-col sm:flex-row w-full justify-between items-start sm:items-center gap-4">
<div className='text-lg font-light'> <div className="text-lg font-light">ویرایش کالا</div>
ویرایش کالا
</div>
<div> <div>
<Button <Button className="px-5 w-full sm:w-auto" onClick={() => formik.handleSubmit()} isloading={isPending || isUploading}>
className='px-5 w-full sm:w-auto' <div className="flex gap-2 items-center">
onClick={() => formik.handleSubmit()} <TickCircle className="size-5" color="white" />
isloading={isPending || isUploading}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
<div>ذخیره تغییرات</div> <div>ذخیره تغییرات</div>
</div> </div>
</Button> </Button>
</div> </div>
</div> </div>
<div className='flex flex-col lg:flex-row gap-6 mt-6'> <div className="flex flex-col lg:flex-row gap-6 mt-6">
<div className='flex-1'> <div className="flex-1">
<div className='bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl'> <div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
<BasicInfoSection <BasicInfoSection
formik={formik} formik={formik}
categories={categories} categories={categories}
initialHasVariants={ initialHasVariants={(product?.variants?.length ?? 0) > 1 || (product?.variants?.length === 1 && (product.variants[0].value ?? "") !== "")}
(product?.variants?.length ?? 0) > 1 ||
((product?.variants?.length === 1) && (product.variants[0].value ?? '') !== '')
}
/> />
</div> </div>
</div> </div>
<div className='w-full lg:w-auto'> <div className="w-full lg:w-auto">
<CreateProductSidebar <CreateProductSidebar
isActive={isActive} isActive={isActive}
isSpecial={isSpecial} isSpecial={isSpecial}
@@ -239,7 +232,7 @@ const UpdateProduct: FC = () => {
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default UpdateProduct export default UpdateProduct;