26 Commits

Author SHA1 Message Date
morteza 7f56e58dee optimize docker
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-22 19:36:22 +03:30
morteza 3f9a2072ea update api url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-22 16:35:10 +03:30
hamid zarghami 778223a550 logo and base url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-02-24 14:39:58 +03:30
Mehdi Mehrmanesh 814784b9a8 add gitlab-ci file 2026-02-21 13:40:29 +03:30
hamid zarghami 234256f89c name and color 2026-02-21 10:12:43 +03:30
hamid zarghami 5126fd5b7c delete variant
deploy to danak / build_and_deploy (push) Has been cancelled
2026-02-21 09:54:10 +03:30
hamid zarghami f5c1f27b9c change api url 2025-12-20 14:19:43 +03:30
hamid zarghami 1295725deb fix update product 2025-12-15 16:20:00 +03:30
hamid zarghami 5d134ba05d theme value 2025-12-13 16:44:46 +03:30
hamid zarghami 7e281ec794 fix title 2025-12-09 15:21:28 +03:30
hamid zarghami ded2c83662 fix brand edit 2025-12-06 15:55:02 +03:30
hamid zarghami 533137a5af fix asign shipment to shop 2025-12-02 16:54:18 +03:30
hamid zarghami a1443911fa fix error bug 2025-12-02 15:20:43 +03:30
hamid zarghami a492251e96 order detail 2025-11-30 09:17:49 +03:30
hamid zarghami e8a10b5780 grid 2025-11-29 16:10:11 +03:30
hamid zarghami 9646f76001 change variant crud 2025-11-29 12:09:42 +03:30
hamid zarghami 7cf27823ee change category by theme 2025-11-29 11:32:14 +03:30
hamid zarghami b999a764a6 update theme values + delete theme value 2025-11-29 11:08:31 +03:30
hamid zarghami 04ba8f3203 Create theme value 2025-11-29 10:42:00 +03:30
hamid zarghami 2d31a2eacf list of values 2025-11-29 10:18:52 +03:30
hamid zarghami 322e2f6173 delete category theme 2025-11-29 10:05:44 +03:30
hamid zarghami a4aaf21803 update category theme 2025-11-29 10:00:15 +03:30
hamid zarghami 4e8068071f list of category themes 2025-11-29 09:38:27 +03:30
hamid zarghami 259fc8af97 create category themee 2025-11-29 09:28:10 +03:30
hamid zarghami 15c6d29bb1 deply.yml 2025-11-26 09:59:19 +03:30
hamid zarghami 0a3e3bd316 remove advantages + disadvantages 2025-11-26 09:58:28 +03:30
38 changed files with 1305 additions and 1311 deletions
+12
View File
@@ -0,0 +1,12 @@
node_modules
dist
dist-ssr
.git
.github
*.md
.env
.env.*
.vscode
.idea
.DS_Store
*.log
+5 -3
View File
@@ -1,4 +1,6 @@
VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com' VITE_BASE_URL = 'https://api.shinan.ir'
# VITE_BASE_URL = 'http://192.168.99.235:4000'
# VITE_BASE_URL = 'https://api.shinan.ir' # VITE_BASE_URL = 'https://api.shinan.ir'
VITE_TOKEN_NAME = 'sh_admin_token' VITE_TOKEN_NAME = 'shinan_admin_token'
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token' VITE_REFRESH_TOKEN_NAME = 'shinan_admin_refresh_token'
+3 -5
View File
@@ -3,7 +3,7 @@ name: deploy to danak
on: on:
push: push:
branches: branches:
- master - shinan
jobs: jobs:
build_and_deploy: build_and_deploy:
@@ -11,10 +11,8 @@ jobs:
env: env:
DANAK_SERVER: "https://captain.dev.danakcorp.com" DANAK_SERVER: "https://captain.dev.danakcorp.com"
APP_TOKEN: 0c540850b77568a873eaf92399a54cc042fa1054a0336b7909ae8f42effe5971 APP_TOKEN: 0fbba2ce0112b82dbdb6e8d2d5f6b07db97e3074d2bb7cca6b81f8287e680bb9
APP_NAME: shop-admin APP_NAME: shinan-admin
# APP_TOKEN: 111ac60c637716478be8ab8b888fd20baba808a10b0fc0c18bf788766883ec9e
# APP_NAME: e-commerce-front
GITHUB_TOKEN: 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 GITHUB_TOKEN: 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
steps: steps:
+7 -13
View File
@@ -1,23 +1,17 @@
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
# Install tzdata to support timezone settings
RUN apk add --no-cache tzdata
# Set the timezone to Asia/Tehran
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
WORKDIR /build WORKDIR /build
COPY package*.json ./
RUN npm install COPY package.json package-lock.json ./
COPY . ./ RUN npm ci
COPY . .
RUN npm run build RUN npm run build
FROM nginx:stable-alpine AS production-stage FROM nginx:stable-alpine AS production
COPY --from=builder /build/dist /usr/share/nginx/html COPY --from=builder /build/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
RUN cat /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80
+2 -2
View File
@@ -3,9 +3,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>پنل مدیریت | فجر مرکزی</title> <title>پنل مدیریت | شینان</title>
</head> </head>
<body> <body>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 549 KiB

+21 -1063
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 550 KiB

+56 -9
View File
@@ -24,22 +24,63 @@ export const ShipmentManagementModal = ({
const removeShipmentFromAdminMutation = useRemoveShipmentFromAdmin(); const removeShipmentFromAdminMutation = useRemoveShipmentFromAdmin();
const shipments = shipmentData?.results?.shipper || []; const shipments = shipmentData?.results?.shipper || [];
const adminShipments: ShipmentMethod[] = adminShopData?.results?.data?.shipmentMethod || []; const adminShipments: ShipmentMethod[] = adminShopData?.results?.data?.shipmentMethod || (adminShopData?.results as { shop?: { shipmentMethod?: ShipmentMethod[] } })?.shop?.shipmentMethod || [];
// لیست روش‌های ارسال که هنوز به ادمین اضافه نشده‌اند // لیست روش‌های ارسال که هنوز به ادمین اضافه نشده‌اند
const availableShipments = shipments.filter((shipment: Shipper) => const availableShipments = shipments.filter((shipment: Shipper) => {
!adminShipments.some((adminShipment: ShipmentMethod) => adminShipment._id === shipment._id) // اگر adminShipments خالی باشد، همه shipments را نشان بده
if (adminShipments.length === 0) {
return true;
}
// اگر shipment._id وجود داشته باشد، از _id برای مقایسه استفاده کن
if (shipment._id != null) {
return !adminShipments.some((adminShipment: ShipmentMethod) =>
adminShipment._id != null && adminShipment._id === shipment._id
); );
}
// اگر _id وجود نداشته باشد، از name برای مقایسه استفاده کن
return !adminShipments.some((adminShipment: ShipmentMethod) =>
adminShipment.name === shipment.name
);
});
const handleAddShipmentToAdmin = async () => { const handleAddShipmentToAdmin = async () => {
if (!selectedShipment) return; if (!selectedShipment) return;
await addShipmentToAdminMutation.mutateAsync(Number(selectedShipment), { // اگر value با "index-" شروع شود، یعنی _id وجود ندارد
if (selectedShipment.startsWith('index-')) {
const index = parseInt(selectedShipment.replace('index-', ''));
const shipment = availableShipments[index];
if (!shipment || shipment._id == null) {
toast.error('شناسه روش ارسال یافت نشد. لطفاً صفحه را رفرش کنید.');
return;
}
await addShipmentToAdminMutation.mutateAsync(Number(shipment._id), {
onError: (error) => { onError: (error) => {
toast.error(extractErrorMessage(error)); toast.error(extractErrorMessage(error));
}, },
onSuccess: () => { onSuccess: () => {
refetchAdminShop(); refetchAdminShop();
setSelectedShipment('');
toast.success('روش ارسال با موفقیت اضافه شد');
}
});
return;
}
const shipmentId = Number(selectedShipment);
if (isNaN(shipmentId)) {
toast.error('شناسه روش ارسال نامعتبر است');
return;
}
await addShipmentToAdminMutation.mutateAsync(shipmentId, {
onError: (error) => {
toast.error(extractErrorMessage(error));
},
onSuccess: () => {
refetchAdminShop();
setSelectedShipment('');
toast.success('روش ارسال با موفقیت اضافه شد'); toast.success('روش ارسال با موفقیت اضافه شد');
} }
}); });
@@ -87,10 +128,14 @@ export const ShipmentManagementModal = ({
placeholder="انتخاب روش ارسال" placeholder="انتخاب روش ارسال"
value={selectedShipment} value={selectedShipment}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setSelectedShipment(e.target.value)} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setSelectedShipment(e.target.value)}
items={availableShipments.map((shipment: Shipper) => ({ items={availableShipments.map((shipment: Shipper, index: number) => {
value: shipment._id.toString(), // اگر _id وجود داشته باشد، از آن استفاده کن، در غیر این صورت از index
const value = shipment._id != null ? String(shipment._id) : `index-${index}`;
return {
value,
label: shipment.name label: shipment.name
}))} };
})}
/> />
<Button <Button
label="اضافه کردن" label="اضافه کردن"
@@ -109,7 +154,9 @@ export const ShipmentManagementModal = ({
<p className="text-gray-500 text-center py-8">هیچ روش ارسالی فعالی وجود ندارد</p> <p className="text-gray-500 text-center py-8">هیچ روش ارسالی فعالی وجود ندارد</p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{adminShipments.map((shipment: ShipmentMethod) => ( {adminShipments
.filter((shipment: ShipmentMethod) => shipment._id != null)
.map((shipment: ShipmentMethod) => (
<div key={shipment._id} className="flex items-center justify-between p-4 bg-gray-50 rounded-xl"> <div key={shipment._id} className="flex items-center justify-between p-4 bg-gray-50 rounded-xl">
<div> <div>
<h4 className="font-medium">{shipment.name}</h4> <h4 className="font-medium">{shipment.name}</h4>
@@ -123,7 +170,7 @@ export const ShipmentManagementModal = ({
variant="danger" variant="danger"
size="sm" size="sm"
label="حذف" label="حذف"
onClick={() => handleRemoveShipmentFromAdmin(shipment._id.toString())} onClick={() => handleRemoveShipmentFromAdmin(String(shipment._id))}
isLoading={removeShipmentFromAdminMutation.isPending} isLoading={removeShipmentFromAdminMutation.isPending}
className="w-fit" className="w-fit"
/> />
+6
View File
@@ -154,6 +154,12 @@ export const Pages = {
attributes: "/products/category/attributes/", attributes: "/products/category/attributes/",
}, },
products: { products: {
theme: {
list: "/products/theme",
create: "/products/theme/create",
update: "/products/theme/update/",
values: "/products/theme/values/",
},
create: "/products/create", create: "/products/create",
list: "/products/list", list: "/products/list",
variants: "/products/variants/", variants: "/products/variants/",
+1 -1
View File
@@ -33,7 +33,7 @@ textarea::placeholder {
} }
@theme { @theme {
--color-primary: #da2129; --color-primary: #a62186;
--color-secondary: #f4f5f9; --color-secondary: #f4f5f9;
--color-border: #d0d0d0; --color-border: #d0d0d0;
--color-description: #888888; --color-description: #888888;
+2 -1
View File
@@ -88,7 +88,8 @@ const UpdateBrand: FC = () => {
logoUrl: brand.logoUrl, logoUrl: brand.logoUrl,
}) })
} }
}, [brandDetail, formik]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [brandDetail])
const categoryOptions = categoriesData?.results?.data?.map((category: CategoryTreeNode) => ({ const categoryOptions = categoriesData?.results?.data?.map((category: CategoryTreeNode) => ({
value: category._id, value: category._id,
+17 -12
View File
@@ -9,7 +9,6 @@ import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox' import UploadBox from '../../components/UploadBox'
import Select from '../../components/Select' import Select from '../../components/Select'
import { type CategoryType, type CategoryTreeNode } from './types/Types' import { type CategoryType, type CategoryTreeNode } from './types/Types'
import { CategoryThemeEnum } from './enum/Enum'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData' import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData'
@@ -17,10 +16,12 @@ import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Pages } from '../../config/Pages' import { Pages } from '../../config/Pages'
import { extractErrorMessage } from '@/helpers/utils' import { extractErrorMessage } from '@/helpers/utils'
import { useGetThemes } from '../theme/hooks/useThemeData'
const CreateCategory: FC = () => { const CreateCategory: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const getThemes = useGetThemes()
const [iconFile, setIconFile] = useState<File>() const [iconFile, setIconFile] = useState<File>()
const [imageFile, setImageFile] = useState<File>() const [imageFile, setImageFile] = useState<File>()
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
@@ -38,14 +39,14 @@ const CreateCategory: FC = () => {
imageUrl: '', imageUrl: '',
description: '', description: '',
parent: undefined, parent: undefined,
theme: '', themeId: undefined,
}, },
validationSchema: Yup.object({ validationSchema: Yup.object({
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'), title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'), title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
description: Yup.string().required('توضیحات دسته الزامی است'), description: Yup.string().required('توضیحات دسته الزامی است'),
parent: Yup.string().optional(), parent: Yup.string().optional(),
theme: Yup.string().optional(), themeId: Yup.string().optional(),
}), }),
onSubmit: async (values) => { onSubmit: async (values) => {
@@ -76,12 +77,21 @@ const CreateCategory: FC = () => {
}) })
const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || [] const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || []
const themesData = getThemes.data?.results?.data || []
const filteredCategories = categoriesData.filter(cat => const filteredCategories = categoriesData.filter(cat =>
cat.title_fa.toLowerCase().includes(searchTerm.toLowerCase()) || cat.title_fa.toLowerCase().includes(searchTerm.toLowerCase()) ||
cat.title_en.toLowerCase().includes(searchTerm.toLowerCase()) cat.title_en.toLowerCase().includes(searchTerm.toLowerCase())
) )
const themeItems = [
{ value: '', label: 'بدون تم' },
...themesData.map(theme => ({
value: theme._id,
label: theme.title
}))
]
const handleParentSelect = (category: CategoryTreeNode) => { const handleParentSelect = (category: CategoryTreeNode) => {
formik.setFieldValue('parent', category._id) formik.setFieldValue('parent', category._id)
} }
@@ -142,15 +152,10 @@ const CreateCategory: FC = () => {
<Select <Select
label='تم دسته' label='تم دسته'
placeholder='انتخاب تم دسته' placeholder='انتخاب تم دسته'
items={[ items={themeItems}
{ value: CategoryThemeEnum.Sized, label: 'سایز' }, value={formik.values.themeId || ''}
{ value: CategoryThemeEnum.Colored, label: 'رنگ' }, onChange={(e) => formik.setFieldValue('themeId', e.target.value || undefined)}
{ value: CategoryThemeEnum.Meterage, label: 'متراژ' }, error_text={formik.touched.themeId && formik.errors.themeId ? formik.errors.themeId : ''}
{ value: CategoryThemeEnum.No_color_No_sized, label: 'بدون تنوع' },
]}
value={formik.values.theme}
onChange={(e) => formik.setFieldValue('theme', e.target.value)}
error_text={formik.touched.theme && formik.errors.theme ? formik.errors.theme : ''}
/> />
</div> </div>
</div> </div>
-6
View File
@@ -73,11 +73,6 @@ const CategoryList: FC = () => {
</div> </div>
</Td> </Td>
<Td text={category.description} /> <Td text={category.description} />
<Td text=''>
{category.leaf && category.theme !== 'noColor_noSize' ? <Link to={`${Pages.category.variant}${category._id}`}>
<Button className='h-7 text-xs w-fit px-4' label='افزودن تنوع' />
</Link> : <Lock size={20} color='#8C90A3' />}
</Td>
<Td text=""> <Td text="">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{category.leaf ? <Link to={`${Pages.category.attributes}${category._id}`}> {category.leaf ? <Link to={`${Pages.category.attributes}${category._id}`}>
@@ -151,7 +146,6 @@ const CategoryList: FC = () => {
<Td text={'عنوان انگلیسی'} /> <Td text={'عنوان انگلیسی'} />
<Td text={'تصویر'} /> <Td text={'تصویر'} />
<Td text={'توضیحات'} /> <Td text={'توضیحات'} />
<Td text={'تنوع'} />
<Td text={'ویژگی‌ها'} /> <Td text={'ویژگی‌ها'} />
<Td text={'عملیات'} /> <Td text={'عملیات'} />
</tr> </tr>
+40 -13
View File
@@ -8,7 +8,6 @@ import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox' import UploadBox from '../../components/UploadBox'
import Select from '../../components/Select' import Select from '../../components/Select'
import { type CategoryType, type CategoryTreeNode } from './types/Types' import { type CategoryType, type CategoryTreeNode } from './types/Types'
import { CategoryThemeEnum } from './enum/Enum'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { useUpdateCategory, useGetCategories, useGetCategoryById, useSingleUpload } from './hooks/useCategoryData' import { useUpdateCategory, useGetCategories, useGetCategoryById, useSingleUpload } from './hooks/useCategoryData'
@@ -16,10 +15,12 @@ import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Pages } from '../../config/Pages' import { Pages } from '../../config/Pages'
import { extractErrorMessage } from '@/helpers/utils' import { extractErrorMessage } from '@/helpers/utils'
import { useGetThemes } from '../theme/hooks/useThemeData'
const UpdateCategory: FC = () => { const UpdateCategory: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const getThemes = useGetThemes()
const [iconFile, setIconFile] = useState<File>() const [iconFile, setIconFile] = useState<File>()
const [imageFile, setImageFile] = useState<File>() const [imageFile, setImageFile] = useState<File>()
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
@@ -41,14 +42,14 @@ const UpdateCategory: FC = () => {
imageUrl: '', imageUrl: '',
description: '', description: '',
parent: undefined, parent: undefined,
theme: '', themeId: undefined,
}, },
validationSchema: Yup.object({ validationSchema: Yup.object({
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'), title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'), title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
description: Yup.string().required('توضیحات دسته الزامی است'), description: Yup.string().required('توضیحات دسته الزامی است'),
parent: Yup.string().optional(), parent: Yup.string().optional(),
theme: Yup.string().optional(), themeId: Yup.string().optional(),
}), }),
onSubmit: async (values) => { onSubmit: async (values) => {
const updatedValues = { ...values } const updatedValues = { ...values }
@@ -91,7 +92,7 @@ const UpdateCategory: FC = () => {
imageUrl: categoryData.imageUrl || '', imageUrl: categoryData.imageUrl || '',
description: categoryData.description || '', description: categoryData.description || '',
parent: categoryData.parent || undefined, parent: categoryData.parent || undefined,
theme: categoryData.theme || '', themeId: categoryData.theme || undefined,
}) })
setIsActive(!categoryData.deleted) setIsActive(!categoryData.deleted)
setIsDataLoaded(true) setIsDataLoaded(true)
@@ -100,6 +101,15 @@ const UpdateCategory: FC = () => {
}, [getCategoryById.data, isDataLoaded]) }, [getCategoryById.data, isDataLoaded])
const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || [] const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || []
const themesData = getThemes.data?.results?.data || []
const themeItems = [
{ value: '', label: 'بدون تم' },
...themesData.map(theme => ({
value: theme._id,
label: theme.title
}))
]
const filteredCategories = categoriesData.filter(cat => const filteredCategories = categoriesData.filter(cat =>
@@ -193,15 +203,10 @@ const UpdateCategory: FC = () => {
<Select <Select
label='تم دسته' label='تم دسته'
placeholder='انتخاب تم دسته' placeholder='انتخاب تم دسته'
items={[ items={themeItems}
{ value: CategoryThemeEnum.Sized, label: 'سایز' }, value={formik.values.themeId || ''}
{ value: CategoryThemeEnum.Colored, label: 'رنگ' }, onChange={(e) => formik.setFieldValue('themeId', e.target.value || undefined)}
{ value: CategoryThemeEnum.Meterage, label: 'متراژ' }, error_text={formik.touched.themeId && formik.errors.themeId ? formik.errors.themeId : ''}
{ value: CategoryThemeEnum.No_color_No_sized, label: 'بدون تنوع' },
]}
value={formik.values.theme}
onChange={(e) => formik.setFieldValue('theme', e.target.value)}
error_text={formik.touched.theme && formik.errors.theme ? formik.errors.theme : ''}
/> />
</div> </div>
</div> </div>
@@ -263,6 +268,28 @@ const UpdateCategory: FC = () => {
<div className='text-sm bg-white py-6 px-4 rounded-2xl'> <div className='text-sm bg-white py-6 px-4 rounded-2xl'>
<h2 className='text-lg font-medium mb-4'>انتخاب دسته والد</h2> <h2 className='text-lg font-medium mb-4'>انتخاب دسته والد</h2>
{formik.values.parent && (
<div className='mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg'>
<div className='text-sm font-medium text-blue-700 mb-1'>دسته انتخاب شده:</div>
<div className='text-xs text-blue-600'>
{(() => {
const findCategory = (cats: CategoryTreeNode[], id: string): CategoryTreeNode | null => {
for (const cat of cats) {
if (cat._id === id) return cat
if (cat.children.length > 0) {
const found = findCategory(cat.children, id)
if (found) return found
}
}
return null
}
const selected = findCategory(categoriesData, formik.values.parent)
return selected ? selected.title_fa : 'نامشخص'
})()}
</div>
</div>
)}
{/* جستجو */} {/* جستجو */}
<div className='mb-4'> <div className='mb-4'>
<div className='relative'> <div className='relative'>
@@ -116,7 +116,7 @@ const VariantModal: FC<VariantModalProps> = ({
title_header={getModalTitle()} title_header={getModalTitle()}
width={500} width={500}
> >
<div className="space-y-6"> <div className="space-y-6 w-[400px]">
{renderFormContent()} {renderFormContent()}
<div className="flex gap-3 pt-4 border-t border-border"> <div className="flex gap-3 pt-4 border-t border-border">
+2 -2
View File
@@ -5,7 +5,7 @@ export type CategoryType = {
imageUrl: string; imageUrl: string;
description: string; description: string;
parent?: string; parent?: string;
theme?: string; themeId?: string;
}; };
export type CategoryResponseType = { export type CategoryResponseType = {
@@ -48,6 +48,7 @@ export type CategoryTreeNode = {
icon: string; icon: string;
imageUrl: string; imageUrl: string;
description: string; description: string;
theme: string;
variants: number[]; variants: number[];
hierarchy: string[]; hierarchy: string[];
leaf: boolean; leaf: boolean;
@@ -56,7 +57,6 @@ export type CategoryTreeNode = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
url: string; url: string;
theme?: string;
children: CategoryTreeNode[]; children: CategoryTreeNode[];
}; };
+15 -2
View File
@@ -93,7 +93,6 @@ const OrderDetail: FC = () => {
شماره سفارش: <span className='font-mono'>#{order._id}</span> شماره سفارش: <span className='font-mono'>#{order._id}</span>
</div> </div>
</div> </div>
{/* کارت‌های اطلاعاتی */} {/* کارت‌های اطلاعاتی */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'> <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'>
{/* اطلاعات کاربر */} {/* اطلاعات کاربر */}
@@ -177,6 +176,11 @@ const OrderDetail: FC = () => {
</div> </div>
<div> <div>
<div className='font-medium'>فروشگاه: {item.shop?.shopName}</div> <div className='font-medium'>فروشگاه: {item.shop?.shopName}</div>
{item.shipper && (
<div className='text-sm text-gray-600 mt-1'>
ارسال کننده: {item.shipper.name}
</div>
)}
<div className='flex items-center gap-2 mt-1'> <div className='flex items-center gap-2 mt-1'>
<StatusWithText <StatusWithText
variant={getOrderItemStatusVariant(item.status)} variant={getOrderItemStatusVariant(item.status)}
@@ -210,10 +214,19 @@ const OrderDetail: FC = () => {
<div key={productIndex} className='flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-100'> <div key={productIndex} className='flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-100'>
<div className='flex-1'> <div className='flex-1'>
<div className='font-medium text-base mb-2'>{product.product.title_fa}</div> <div className='font-medium text-base mb-2'>{product.product.title_fa}</div>
{product.variant.themeValue && (
<div className='mb-2'>
<span className='text-sm font-medium text-gray-700'>تنوع خریداری شده: </span>
<span className='text-sm text-blue-600 font-semibold'>{product.variant.themeValue.name}</span>
</div>
)}
<div className='grid grid-cols-2 gap-4 text-sm text-gray-600'> <div className='grid grid-cols-2 gap-4 text-sm text-gray-600'>
<div>گارانتی: {product.variant.warranty.name}</div> <div>گارانتی: {product.variant.warranty?.name || 'ندارد'}</div>
<div>تعداد: {product.quantity}</div> <div>تعداد: {product.quantity}</div>
<div>قیمت واحد: {formatPrice(product.selling_price)} تومان</div> <div>قیمت واحد: {formatPrice(product.selling_price)} تومان</div>
{product.shipmentCost > 0 && (
<div>هزینه ارسال: {formatPrice(product.shipmentCost)} تومان</div>
)}
</div> </div>
</div> </div>
+10 -1
View File
@@ -136,7 +136,9 @@ export type VariantType = {
isWholeSale: boolean; isWholeSale: boolean;
shop: ShopType; shop: ShopType;
warranty: WarrantyType; warranty: WarrantyType;
meterage: MeterageType; saleFormat: SaleFormatType;
themeValue: ThemeValueType;
meterage?: MeterageType;
}; };
export type PriceType = { export type PriceType = {
@@ -154,6 +156,13 @@ export type SaleFormatType = {
wholeSale: unknown[]; wholeSale: unknown[];
}; };
export type ThemeValueType = {
_id: string;
theme: string;
name: string;
value: number;
};
export type ShopType = { export type ShopType = {
_id: string; _id: string;
shopName?: string; shopName?: string;
+18 -17
View File
@@ -1,6 +1,6 @@
import { type FC } from 'react' import { type FC } from 'react'
import PageTitle from '../../components/PageTitle' import PageTitle from '../../components/PageTitle'
import { useGetProductVariants, useGetProductById } from './hooks/useProductData' import { useDeleteProductVariant, useGetProductVariants } from './hooks/useProductData'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import PageLoading from '@/components/PageLoading' import PageLoading from '@/components/PageLoading'
import type { ProductVariantDetailType } from './types/Types' import type { ProductVariantDetailType } from './types/Types'
@@ -8,31 +8,20 @@ import Td from '@/components/Td'
import StatusWithText from '@/components/StatusWithText' import StatusWithText from '@/components/StatusWithText'
import CreateVariant from './components/CreateVariant' import CreateVariant from './components/CreateVariant'
import UpdateVariant from './components/UpdateVariant' import UpdateVariant from './components/UpdateVariant'
import TrashWithConfrim from '@/components/TrashWithConfrim'
const ProductVariant: FC = () => { const ProductVariant: FC = () => {
const { id } = useParams() const { id } = useParams()
const { data, isLoading, refetch } = useGetProductVariants(id!) const { data, isLoading, refetch } = useGetProductVariants(id!)
const { data: productData } = useGetProductById(id!) const { mutate: deleteVariant } = useDeleteProductVariant()
const categoryTheme = productData?.results?.product?.category?.theme
const getVariantColumnHeader = () => { const getVariantColumnHeader = () => {
switch (categoryTheme) { return 'تنوع'
case 'Size': return 'سایز'
case 'Color': return 'رنگ'
case 'Meterage': return 'متریاژ'
default: return 'تنوع'
}
} }
const getVariantValue = (variant: ProductVariantDetailType) => { const getVariantValue = (variant: ProductVariantDetailType) => {
switch (categoryTheme) { return variant.themeValue?.name || variant.themeValue?.value?.toString() || '-'
case 'Size': return variant.size?.value || '-'
case 'Color': return variant.color?.name || '-'
case 'Meterage': return variant.meterage?.value || '-'
default: return '-'
}
} }
const getMarketStatusVariant = (status: string) => { const getMarketStatusVariant = (status: string) => {
@@ -58,7 +47,8 @@ const ProductVariant: FC = () => {
<Td text=""> <Td text="">
<div> <div>
<div className="font-medium text-gray-900"> <div className="font-medium text-gray-900">
{variant.title_fa} {variant.title_fa + ' '}
( {variant.themeValue?.name} )
</div> </div>
<div className="text-gray-500 text-xs"> <div className="text-gray-500 text-xs">
{variant.title_en} {variant.title_en}
@@ -88,7 +78,18 @@ const ProductVariant: FC = () => {
</span> </span>
</Td> </Td>
<Td text=""> <Td text="">
<div className='flex gap-2 items-center'>
<UpdateVariant variant={variant} onUpdate={refetch} /> <UpdateVariant variant={variant} onUpdate={refetch} />
<TrashWithConfrim
onDelete={() => {
deleteVariant(variant._id, {
onSuccess: () => {
refetch()
}
})
}}
/>
</div>
</Td> </Td>
</tr> </tr>
) )
+34 -58
View File
@@ -6,8 +6,8 @@ import DatePickerComponent from '@/components/DatePicker'
import { useState, type FC } from 'react' import { useState, type FC } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useCreateProductVariant, useGetProductById, useGetProductVariants } from '../hooks/useProductData' import { useCreateProductVariant, useGetProductById, useGetProductVariants } from '../hooks/useProductData'
import { useGetCategoryVariants } from '@/pages/category'
import type { CreateProductVariantRequestType } from '../types/Types' import type { CreateProductVariantRequestType } from '../types/Types'
import { useGetThemeValues } from '@/pages/theme/hooks/useThemeData'
import { useGetWarranties } from '@/pages/warranty/hooks/useWarrantyData' import { useGetWarranties } from '@/pages/warranty/hooks/useWarrantyData'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils' import { extractErrorMessage } from '@/helpers/utils'
@@ -26,7 +26,7 @@ const CreateVariant: FC = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { data: productData } = useGetProductById(id!) const { data: productData } = useGetProductById(id!)
const { data: categoryData } = useGetCategoryVariants(productData?.results?.product?.category?._id ?? '') const { data: themeValues } = useGetThemeValues(productData?.results?.product?.category?.theme as string)
const { data: variantsData, refetch: refetchVariants } = useGetProductVariants(id!) const { data: variantsData, refetch: refetchVariants } = useGetProductVariants(id!)
const createProductVariant = useCreateProductVariant() const createProductVariant = useCreateProductVariant()
const { data: warrantiesData } = useGetWarranties() const { data: warrantiesData } = useGetWarranties()
@@ -34,13 +34,15 @@ const CreateVariant: FC = () => {
const [showModal, setShowModal] = useState<boolean>(false) const [showModal, setShowModal] = useState<boolean>(false)
const categoryTheme = productData?.results?.product?.category?.theme const categoryTheme = productData?.results?.product?.category?.theme
const categoryVariants = categoryData?.results?.data?.category?.categoryVariants
const isNoVariantTheme = categoryTheme === 'noColor_noSize' const isNoVariantTheme = categoryTheme === 'noColor_noSize'
const isThemeNull = !categoryTheme
const existingVariantsCount = variantsData?.results?.count || 0 const existingVariantsCount = variantsData?.results?.count || 0
const canCreateVariant = !isNoVariantTheme || existingVariantsCount === 0 const canCreateVariant = (isThemeNull || isNoVariantTheme)
? existingVariantsCount === 0
: true
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
variantId: isNoVariantTheme themeValueId: (isNoVariantTheme || isThemeNull)
? Yup.string().optional() ? Yup.string().optional()
: Yup.string().required('انتخاب تنوع الزامی است'), : Yup.string().required('انتخاب تنوع الزامی است'),
warrantyId: Yup.string().required('انتخاب گارانتی الزامی است'), warrantyId: Yup.string().required('انتخاب گارانتی الزامی است'),
@@ -48,9 +50,9 @@ const CreateVariant: FC = () => {
sellerSpecialCode: Yup.string(), sellerSpecialCode: Yup.string(),
retail_price: Yup.number().required('قیمت فروش الزامی است').min(0, 'قیمت نمی‌تواند منفی باشد'), retail_price: Yup.number().required('قیمت فروش الزامی است').min(0, 'قیمت نمی‌تواند منفی باشد'),
package_weight: Yup.number().required('وزن بسته الزامی است').min(0.1, 'وزن باید حداقل ۰.۱ کیلوگرم باشد'), package_weight: Yup.number().required('وزن بسته الزامی است').min(0.1, 'وزن باید حداقل ۰.۱ کیلوگرم باشد'),
package_height: Yup.number().required('ارتفاع بسته الزامی است').min(1, 'ارتفاع باید حداقل ۱ سانتی‌متر باشد'), // package_height: Yup.number().required('ارتفاع بسته الزامی است').min(1, 'ارتفاع باید حداقل ۱ سانتی‌متر باشد'),
package_length: Yup.number().required('طول بسته الزامی است').min(1, 'طول باید حداقل ۱ سانتی‌متر باشد'), // package_length: Yup.number().required('طول بسته الزامی است').min(1, 'طول باید حداقل ۱ سانتی‌متر باشد'),
package_width: Yup.number().required('عرض بسته الزامی است').min(1, 'عرض باید حداقل ۱ سانتی‌متر باشد'), // package_width: Yup.number().required('عرض بسته الزامی است').min(1, 'عرض باید حداقل ۱ سانتی‌متر باشد'),
postingTime: Yup.number().required('زمان ارسال الزامی است').min(1, 'زمان ارسال باید حداقل ۱ روز باشد'), postingTime: Yup.number().required('زمان ارسال الزامی است').min(1, 'زمان ارسال باید حداقل ۱ روز باشد'),
stock: Yup.number().required('موجودی انبار الزامی است').min(0, 'موجودی نمی‌تواند منفی باشد'), stock: Yup.number().required('موجودی انبار الزامی است').min(0, 'موجودی نمی‌تواند منفی باشد'),
specialSale_order_limit: Yup.number().min(0, 'محدودیت فروش ویژه نمی‌تواند منفی باشد'), specialSale_order_limit: Yup.number().min(0, 'محدودیت فروش ویژه نمی‌تواند منفی باشد'),
@@ -59,15 +61,15 @@ const CreateVariant: FC = () => {
}) })
const initialValues = { const initialValues = {
variantId: '', themeValueId: '',
warrantyId: '', warrantyId: '',
order_limit: '', order_limit: '',
sellerSpecialCode: '' as string | undefined, sellerSpecialCode: '' as string | undefined,
retail_price: '', retail_price: '',
package_weight: '', package_weight: '',
package_height: '', // package_height: '',
package_length: '', // package_length: '',
package_width: '', // package_width: '',
postingTime: '', postingTime: '',
stock: '', stock: '',
specialSale_order_limit: '', specialSale_order_limit: '',
@@ -76,34 +78,14 @@ const CreateVariant: FC = () => {
} }
const getVariantOptions = () => { const getVariantOptions = () => {
switch (categoryTheme) { return themeValues?.results?.data?.map(themeValue => ({
case 'Size': value: themeValue._id,
return categoryVariants?.sizes?.map(size => ({ label: themeValue.name || themeValue.value?.toString() || '-'
value: size._id.toString(),
label: size.value
})) || [] })) || []
case 'Color':
return categoryVariants?.colors?.map((color, index) => ({
value: index.toString(), // استفاده از index به عنوان ID موقت
label: color.name
})) || []
case 'Meterage':
return categoryVariants?.meterages?.map(meterage => ({
value: meterage._id.toString(),
label: meterage.value
})) || []
default:
return []
}
} }
const getVariantLabel = () => { const getVariantLabel = () => {
switch (categoryTheme) { return categoryTheme ? 'تنوع' : 'تنوع'
case 'Size': return 'سایز'
case 'Color': return 'رنگ'
case 'Meterage': return 'متریاژ'
default: return 'تنوع'
}
} }
const getWarrantyOptions = () => { const getWarrantyOptions = () => {
@@ -121,9 +103,9 @@ const CreateVariant: FC = () => {
sellerSpecialCode: values.sellerSpecialCode, sellerSpecialCode: values.sellerSpecialCode,
retail_price: parseFloat(values.retail_price), retail_price: parseFloat(values.retail_price),
package_weight: parseFloat(values.package_weight), package_weight: parseFloat(values.package_weight),
package_height: parseFloat(values.package_height), package_height: 10, // مقدار پیش‌فرض
package_length: parseFloat(values.package_length), package_length: 10, // مقدار پیش‌فرض
package_width: parseFloat(values.package_width), package_width: 10, // مقدار پیش‌فرض
postingTime: parseInt(values.postingTime), postingTime: parseInt(values.postingTime),
stock: parseInt(values.stock) stock: parseInt(values.stock)
} }
@@ -131,18 +113,11 @@ const CreateVariant: FC = () => {
// اضافه کردن variant بر اساس theme // اضافه کردن variant بر اساس theme
let variantData: CreateProductVariantRequestType let variantData: CreateProductVariantRequestType
if (isNoVariantTheme) { if (isNoVariantTheme || isThemeNull) {
// در حالت noColor_noSize هیچ variantId ارسال نمی‌شود // در حالت noColor_noSize یا theme null هیچ themeValueId ارسال نمی‌شود
variantData = baseData variantData = baseData
} else if (categoryTheme === 'Size') {
variantData = { ...baseData, sizeId: parseInt(values.variantId) }
} else if (categoryTheme === 'Color') {
// برای رنگ‌ها از index به عنوان ID استفاده می‌کنیم
variantData = { ...baseData, colorId: parseInt(values.variantId) + 1 } // +1 برای اینکه ID از 1 شروع شود
} else if (categoryTheme === 'Meterage') {
variantData = { ...baseData, meterageId: parseInt(values.variantId) }
} else { } else {
variantData = baseData variantData = { ...baseData, themeValueId: values.themeValueId }
} }
if (!values.sellerSpecialCode) { if (!values.sellerSpecialCode) {
@@ -160,8 +135,9 @@ const CreateVariant: FC = () => {
toast.success('تنوع جدید با موفقیت ایجاد شد') toast.success('تنوع جدید با موفقیت ایجاد شد')
// Refetch لیست تنوع‌ها // Refetch لیست تنوع‌ها
refetchVariants() refetchVariants()
// یا می‌توانید از invalidateQueries استفاده کنید // Invalidate لیست تنوع‌ها و جزئیات محصول
queryClient.invalidateQueries({ queryKey: ['product-variants', id] }) queryClient.invalidateQueries({ queryKey: ['product-variants', id] })
queryClient.invalidateQueries({ queryKey: ['product-by-id', id] })
}, },
onError: (error) => { onError: (error) => {
toast.error(extractErrorMessage(error)) toast.error(extractErrorMessage(error))
@@ -199,29 +175,29 @@ const CreateVariant: FC = () => {
> >
{({ setFieldValue }) => ( {({ setFieldValue }) => (
<Form className="space-y-4 w-[500px] mt-4"> <Form className="space-y-4 w-[500px] mt-4">
{/* انتخاب variant - فقط در صورتی که theme نوع noColor_noSize نباشد */} {/* انتخاب variant - فقط در صورتی که theme معتبر باشد و نوع noColor_noSize نباشد */}
{!isNoVariantTheme && ( {!isNoVariantTheme && !isThemeNull && (
<div> <div>
<label className="text-sm font-medium text-gray-700 mb-1 block"> <label className="text-sm font-medium text-gray-700 mb-1 block">
{getVariantLabel()} {getVariantLabel()}
</label> </label>
<Field name="variantId"> <Field name="themeValueId">
{({ field }: FormikFieldProps) => ( {({ field }: FormikFieldProps) => (
<Select <Select
{...field} {...field}
placeholder={`انتخاب ${getVariantLabel().toLowerCase()}`} placeholder={`انتخاب ${getVariantLabel().toLowerCase()}`}
value={field.value} value={field.value}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFieldValue('variantId', e.target.value)} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFieldValue('themeValueId', e.target.value)}
items={getVariantOptions()} items={getVariantOptions()}
/> />
)} )}
</Field> </Field>
<ErrorMessage name="variantId" component="div" className="text-red-500 text-sm mt-1" /> <ErrorMessage name="themeValueId" component="div" className="text-red-500 text-sm mt-1" />
</div> </div>
)} )}
{/* پیام اطلاع‌رسانی برای محصولات بدون تنوع */} {/* پیام اطلاع‌رسانی برای محصولات بدون تنوع */}
{isNoVariantTheme && ( {(isNoVariantTheme || isThemeNull) && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg"> <div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800"> <p className="text-sm text-blue-800">
این محصول تنوع ندارد. فقط میتوانید یک نسخه از محصول ایجاد کنید. این محصول تنوع ندارد. فقط میتوانید یک نسخه از محصول ایجاد کنید.
@@ -318,7 +294,7 @@ const CreateVariant: FC = () => {
<ErrorMessage name="package_weight" component="div" className="text-red-500 text-sm mt-1" /> <ErrorMessage name="package_weight" component="div" className="text-red-500 text-sm mt-1" />
</div> </div>
<div> {/* <div>
<Field name="package_height"> <Field name="package_height">
{({ field }: FormikFieldProps) => ( {({ field }: FormikFieldProps) => (
<Input <Input
@@ -364,7 +340,7 @@ const CreateVariant: FC = () => {
)} )}
</Field> </Field>
<ErrorMessage name="package_width" component="div" className="text-red-500 text-sm mt-1" /> <ErrorMessage name="package_width" component="div" className="text-red-500 text-sm mt-1" />
</div> </div> */}
</div> </div>
</div> </div>
+11 -2
View File
@@ -1,4 +1,4 @@
import { type FC, useState, useMemo, useEffect } from 'react'; import { type FC, useState, useMemo, useEffect, useRef } 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';
@@ -48,8 +48,17 @@ const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading,
isFake: false isFake: false
}); });
const isInitializedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (initialData) { if (!initialData || isInitializedRef.current) return;
const hasRealData = Object.values(initialData).some(
(value) => value !== undefined && value !== ''
);
if (hasRealData) {
isInitializedRef.current = true;
setFormData(prev => ({ ...prev, ...initialData })); setFormData(prev => ({ ...prev, ...initialData }));
} }
}, [initialData]); }, [initialData]);
+71 -69
View File
@@ -189,76 +189,76 @@ const TagInput: FC<TagInputProps> = ({ tags, onChange, placeholder }) => {
}; };
// کامپوننت مدیریت لیست مزایا/معایب // کامپوننت مدیریت لیست مزایا/معایب
interface ListInputProps { // interface ListInputProps {
items: string[]; // items: string[];
onChange: (items: string[]) => void; // onChange: (items: string[]) => void;
placeholder?: string; // placeholder?: string;
label: string; // label: string;
} // }
const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label }) => { // const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label }) => {
const [inputValue, setInputValue] = useState(''); // const [inputValue, setInputValue] = useState('');
const addItem = () => { // const addItem = () => {
const newItem = inputValue.trim(); // const newItem = inputValue.trim();
if (newItem && !items.includes(newItem)) { // if (newItem && !items.includes(newItem)) {
onChange([...items, newItem]); // onChange([...items, newItem]);
setInputValue(''); // setInputValue('');
} // }
}; // };
const removeItem = (itemToRemove: string) => { // const removeItem = (itemToRemove: string) => {
onChange(items.filter(item => item !== itemToRemove)); // onChange(items.filter(item => item !== itemToRemove));
}; // };
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { // const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { // if (e.key === 'Enter') {
e.preventDefault(); // e.preventDefault();
addItem(); // addItem();
} // }
}; // };
return ( // return (
<div className="space-y-3"> // <div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">{label}</label> // <label className="block text-sm font-medium text-gray-700">{label}</label>
<div className="flex gap-2"> // <div className="flex gap-2">
<Input // <Input
value={inputValue} // value={inputValue}
onChange={(e) => setInputValue(e.target.value)} // onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown} // onKeyDown={handleKeyDown}
placeholder={placeholder} // placeholder={placeholder}
className="flex-1" // className="flex-1"
/> // />
<Button // <Button
type="button" // type="button"
onClick={addItem} // onClick={addItem}
disabled={!inputValue.trim()} // disabled={!inputValue.trim()}
className="px-4 w-fit" // className="px-4 w-fit"
> // >
افزودن // افزودن
</Button> // </Button>
</div> // </div>
{items.length > 0 && ( // {items.length > 0 && (
<div className="space-y-2 flex flex-wrap gap-3 max-h-40 overflow-y-auto"> // <div className="space-y-2 flex flex-wrap gap-3 max-h-40 overflow-y-auto">
{items.map((item, index) => ( // {items.map((item, index) => (
<div key={index} className="flex min-w-[30%] flex-1 h-10 items-center justify-between p-2 bg-gray-50 rounded-md"> // <div key={index} className="flex min-w-[30%] flex-1 h-10 items-center justify-between p-2 bg-gray-50 rounded-md">
<span className="text-sm text-gray-700">{item}</span> // <span className="text-sm text-gray-700">{item}</span>
<button // <button
type="button" // type="button"
onClick={() => removeItem(item)} // onClick={() => removeItem(item)}
className="text-red-500 hover:text-red-700 p-1" // className="text-red-500 hover:text-red-700 p-1"
> // >
<CloseCircle size={16} color='red' /> // <CloseCircle size={16} color='red' />
</button> // </button>
</div> // </div>
))} // ))}
<div className='min-w-[30%] flex-1 px-2'></div> // <div className='min-w-[30%] flex-1 px-2'></div>
<div className='min-w-[30%] flex-1 px-2'></div> // <div className='min-w-[30%] flex-1 px-2'></div>
</div> // </div>
)} // )}
</div> // </div>
); // );
}; // };
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId, initialData, specifications }) => { const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId, initialData, specifications }) => {
const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>(() => ({ const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>(() => ({
@@ -266,8 +266,10 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
description: initialData?.description || '', description: initialData?.description || '',
metaDescription: initialData?.metaDescription || '', metaDescription: initialData?.metaDescription || '',
tags: initialData?.tags || [], tags: initialData?.tags || [],
advantages: initialData?.advantages || [], // advantages: initialData?.advantages || [],
disAdvantages: initialData?.disAdvantages || [] // disAdvantages: initialData?.disAdvantages || []
advantages: [],
disAdvantages: []
})); }));
const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || ''); const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || '');
@@ -403,7 +405,7 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
/> />
</div> </div>
<ListInput {/* <ListInput
label="مزایا" label="مزایا"
items={formData.advantages} items={formData.advantages}
onChange={(advantages) => setFormData(prev => ({ ...prev, advantages }))} onChange={(advantages) => setFormData(prev => ({ ...prev, advantages }))}
@@ -415,7 +417,7 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
items={formData.disAdvantages} items={formData.disAdvantages}
onChange={(disAdvantages) => setFormData(prev => ({ ...prev, disAdvantages }))} onChange={(disAdvantages) => setFormData(prev => ({ ...prev, disAdvantages }))}
placeholder="عیبی را وارد کنید..." placeholder="عیبی را وارد کنید..."
/> /> */}
<div className="flex gap-2 justify-between"> <div className="flex gap-2 justify-between">
<Button <Button
@@ -12,6 +12,7 @@ import { extractErrorMessage } from '@/helpers/utils'
import { Formik, Form, Field, ErrorMessage } from 'formik' import { Formik, Form, Field, ErrorMessage } from 'formik'
import type { FieldInputProps } from 'formik' import type { FieldInputProps } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { useQueryClient } from '@tanstack/react-query'
interface FormikFieldProps { interface FormikFieldProps {
field: FieldInputProps<string | number | undefined> field: FieldInputProps<string | number | undefined>
@@ -26,6 +27,7 @@ interface UpdateVariantProps {
const UpdateVariant: FC<UpdateVariantProps> = ({ variant, onUpdate }) => { const UpdateVariant: FC<UpdateVariantProps> = ({ variant, onUpdate }) => {
const { id } = useParams() const { id } = useParams()
const queryClient = useQueryClient()
const { data: variantData, isLoading, error } = useGetVariantById(id!, variant._id) const { data: variantData, isLoading, error } = useGetVariantById(id!, variant._id)
const updateVariant = useUpdateVariant() const updateVariant = useUpdateVariant()
@@ -104,7 +106,12 @@ const UpdateVariant: FC<UpdateVariantProps> = ({ variant, onUpdate }) => {
onSuccess: () => { onSuccess: () => {
setShowModal(false) setShowModal(false)
toast.success('تنوع با موفقیت بروزرسانی شد') toast.success('تنوع با موفقیت بروزرسانی شد')
// Refetch لیست تنوع‌ها
onUpdate?.() onUpdate?.()
// Invalidate لیست تنوع‌ها و جزئیات محصول
queryClient.invalidateQueries({ queryKey: ['product-variants', id] })
queryClient.invalidateQueries({ queryKey: ['product-by-id', id] })
queryClient.invalidateQueries({ queryKey: ['variant-by-id', id, variant._id] })
}, },
onError: (error) => { onError: (error) => {
toast.error(extractErrorMessage(error)) toast.error(extractErrorMessage(error))
@@ -142,3 +142,9 @@ export const useDraftProduct = () => {
mutationFn: api.draftProduct, mutationFn: api.draftProduct,
}); });
}; };
export const useDeleteProductVariant = () => {
return useMutation({
mutationFn: api.deleteProductVariant,
});
};
@@ -133,3 +133,8 @@ export const draftProduct = async (id: string) => {
const { data } = await axios.post(`/admin/products/${id}/draft`); const { data } = await axios.post(`/admin/products/${id}/draft`);
return data; return data;
}; };
export const deleteProductVariant = async (variantId: string) => {
const { data } = await axios.delete(`/admin/products/variants/${variantId}`);
return data;
};
+14 -8
View File
@@ -242,6 +242,14 @@ export type ProductVariantDetailType = {
isFreeShip: boolean; isFreeShip: boolean;
isWholeSale: boolean; isWholeSale: boolean;
saleFormat: SaleFormatType; saleFormat: SaleFormatType;
themeValue?: {
_id: string;
theme: {
title: string;
};
name: string;
value: string;
};
size?: SizeType; size?: SizeType;
color?: ColorType; color?: ColorType;
meterage?: MeterageType; meterage?: MeterageType;
@@ -266,17 +274,15 @@ export type GetVariantByIdResponseType = {
export type CreateProductVariantRequestType = { export type CreateProductVariantRequestType = {
productId: string; productId: string;
colorId?: number; themeValueId?: string;
sizeId?: number;
meterageId?: number;
warrantyId: number; warrantyId: number;
order_limit: number; order_limit: number;
sellerSpecialCode?: string; sellerSpecialCode?: string;
retail_price: number; retail_price: number;
package_weight: number; package_weight: number;
package_height: number; package_height?: number;
package_length: number; package_length?: number;
package_width: number; package_width?: number;
postingTime: number; postingTime: number;
stock: number; stock: number;
specialSale_order_limit?: number; specialSale_order_limit?: number;
@@ -305,14 +311,14 @@ export type ProductCategoryType = {
icon: string; icon: string;
imageUrl: string; imageUrl: string;
description: string; description: string;
theme: string;
variants: number[]; variants: number[];
hierarchy: string[]; hierarchy: string[];
leaf: boolean; leaf: boolean;
parent: string; parent: string | null;
deleted: boolean; deleted: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
theme: string;
url: string; url: string;
children: ProductCategoryType[]; children: ProductCategoryType[];
}; };
+90
View File
@@ -0,0 +1,90 @@
import PageTitle from '@/components/PageTitle'
import { useFormik } from 'formik'
import { type FC } from 'react'
import type { CreateThemeType } from './type/Types'
import { ThemeInputType } from './enum/Enum'
import * as Yup from 'yup'
import Input from '@/components/Input'
import Select from '@/components/Select'
import { useCreateTheme } from './hooks/useThemeData'
import { useNavigate } from 'react-router-dom'
import { Pages } from '@/config/Pages'
import { extractErrorMessage } from '@/helpers/utils'
import { toast } from 'react-toastify'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
const CreateTheme: FC = () => {
const navigate = useNavigate()
const { mutate: createTheme, isPending } = useCreateTheme();
const formik = useFormik<CreateThemeType>({
initialValues: {
title: '',
inputType: ThemeInputType.Color,
},
validationSchema: Yup.object({
title: Yup.string().required('عنوان تم الزامی است'),
inputType: Yup.string().required('نوع ورودی تم الزامی است'),
}),
onSubmit: (values) => {
createTheme(values, {
onSuccess: () => {
navigate(Pages.products.theme.list)
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
},
})
return (
<div>
<PageTitle title1='افزودن تم' />
<div className='bg-white p-8 rounded-4xl'>
<div>
<Input
label='عنوان تم'
placeholder='عنوان تم را وارد کنید'
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
</div>
<div className='mt-5'>
<Select
label='نوع ورودی تم'
placeholder='انتخاب نوع ورودی تم'
value={formik.values.inputType}
onChange={(e) => formik.setFieldValue('inputType', e.target.value)}
items={[
{ value: ThemeInputType.Color, label: 'رنگ' },
{ value: ThemeInputType.Text, label: 'متن' },
]}
error_text={formik.touched.inputType && formik.errors.inputType ? formik.errors.inputType : ''}
/>
</div>
<div className='mt-5 flex justify-end'>
<Button
onClick={() => formik.handleSubmit()}
isLoading={isPending}
disabled={!formik.isValid || isPending}
className='w-fit'
>
<div className='flex gap-2 items-center'>
<TickCircle size={16} color='#fff' />
<div>ذخیره</div>
</div>
</Button>
</div>
</div>
</div>
)
}
export default CreateTheme
+89
View File
@@ -0,0 +1,89 @@
import { type FC } from 'react'
import PageTitle from '@/components/PageTitle'
import { useDeleteTheme, useGetThemes } from './hooks/useThemeData'
import Button from '@/components/Button';
import { Link } from 'react-router-dom';
import { Pages } from '@/config/Pages';
import { Plus } from 'lucide-react';
import Td from '@/components/Td';
import TrashWithConfrim from '@/components/TrashWithConfrim';
import { Eye } from 'iconsax-react';
import { toast } from 'react-toastify';
import { extractErrorMessage } from '@/helpers/utils';
const ThemeList: FC = () => {
const { data, isLoading } = useGetThemes();
const { mutate: deleteTheme, isPending } = useDeleteTheme();
if (isLoading) {
return <div className="flex justify-center items-center h-64">در حال بارگذاری...</div>;
}
return (
<div>
<PageTitle title1='تم ها' />
<div className='mt-5 flex justify-end'>
<Link to={Pages.products.theme.create}>
<Button
className='w-fit px-4 flex gap-2'
>
<Plus color='#fff' size={16} />
<span>افزودن تم</span>
</Button>
</Link>
</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={'عملیات'} />
</tr>
</thead>
<tbody>
{
data?.results?.data?.map((item) => {
return (
<tr className='tr' key={item._id}>
<Td text={item.title} />
<Td text={item.inputType} />
<Td text=''>
<Link to={`${Pages.products.theme.values}${item._id}`}>
<Button variant='outline' className='h-7 text-xs w-fit px-4' label='مدیریت مقادیر' />
</Link>
</Td>
<Td text=''>
<div className='flex gap-2'>
<Link to={`${Pages.products.theme.update}${item._id}`}>
<Eye size={16} color='#8C90A3' />
</Link>
<TrashWithConfrim
onDelete={() => deleteTheme(item._id, {
onSuccess: () => {
toast.success('تم با موفقیت حذف شد')
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})}
isLoading={isPending}
/>
</div>
</Td>
</tr>
)
})
}
</tbody>
</table>
</div>
</div>
)
}
export default ThemeList
+102
View File
@@ -0,0 +1,102 @@
import { useEffect, type FC } from 'react'
import { useParams } from 'react-router-dom'
import { useGetThemeById, useUpdateTheme } from './hooks/useThemeData'
import PageTitle from '@/components/PageTitle'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
import Input from '@/components/Input'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import type { CreateThemeType } from './type/Types'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils'
import { Pages } from '@/config/Pages'
import { ThemeInputType } from './enum/Enum'
import { useNavigate } from 'react-router-dom'
import Select from '@/components/Select'
const UpdateTheme: FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const { data } = useGetThemeById(id!)
const { mutate: updateTheme, isPending } = useUpdateTheme()
const formik = useFormik<CreateThemeType>({
initialValues: {
title: '',
inputType: ThemeInputType.Color,
},
validationSchema: Yup.object({
title: Yup.string().required('عنوان تم الزامی است'),
inputType: Yup.string().required('نوع ورودی تم الزامی است'),
}),
onSubmit: (values) => {
updateTheme({ id: id!, params: values }, {
onSuccess: () => {
toast.success('تم با موفقیت ویرایش شد')
navigate(Pages.products.theme.list)
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
},
})
useEffect(() => {
if (data?.results?.data) {
formik.setValues({
title: data?.results?.data?.title,
inputType: data?.results?.data?.inputType,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
return (
<div>
<PageTitle title1='ویرایش تم' />
<div className='bg-white p-8 rounded-4xl'>
<div>
<Input
label='عنوان تم'
placeholder='عنوان تم را وارد کنید'
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
</div>
<div className='mt-5'>
<Select
label='نوع ورودی تم'
placeholder='انتخاب نوع ورودی تم'
value={formik.values.inputType}
onChange={(e) => formik.setFieldValue('inputType', e.target.value)}
items={[
{ value: ThemeInputType.Color, label: 'رنگ' },
{ value: ThemeInputType.Text, label: 'متن' },
]}
error_text={formik.touched.inputType && formik.errors.inputType ? formik.errors.inputType : ''}
/>
</div>
<div className='mt-5 flex justify-end'>
<Button
onClick={() => formik.handleSubmit()}
isLoading={isPending}
disabled={!formik.isValid || isPending}
className='w-fit'
>
<div className='flex gap-2 items-center'>
<TickCircle size={16} color='#fff' />
<div>ذخیره</div>
</div>
</Button>
</div>
</div>
</div>
)
}
export default UpdateTheme
+138
View File
@@ -0,0 +1,138 @@
import { type FC, useState } from 'react'
import { useDeleteThemeValue, useGetThemeValues } from './hooks/useThemeData'
import { useParams } from 'react-router-dom'
import PageTitle from '@/components/PageTitle'
import Td from '@/components/Td'
import { Edit, Add } from 'iconsax-react'
import TrashWithConfrim from '@/components/TrashWithConfrim'
import { toast } from 'react-toastify'
import Button from '@/components/Button'
import CreateThemeValueModal from './components/CreateThemeValueModal'
import UpdateThemeValueModal from './components/UpdateThemeValueModal'
import type { ThemeValueType } from './type/Types'
import { extractErrorMessage } from '@/helpers/utils'
const Values: FC = () => {
const { id } = useParams()
const { mutate: deleteThemeValue, isPending: isDeletingThemeValue } = useDeleteThemeValue()
const { data, isLoading } = useGetThemeValues(id!)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false)
const [selectedThemeValue, setSelectedThemeValue] = useState<ThemeValueType | null>(null)
if (isLoading) {
return <div className="flex justify-center items-center h-64">در حال بارگذاری...</div>
}
const themeInfo = data?.results?.data?.[0]?.theme
return (
<div>
<div className='flex gap-4 justify-between items-center'>
<PageTitle title1='مقادیر تم' />
<Button
onClick={() => setIsModalOpen(true)}
className='w-fit'
>
<div className='flex whitespace-nowrap gap-2 items-center'>
<Add size={16} color='#fff' />
<div>افزودن مقدار</div>
</div>
</Button>
</div>
{themeInfo && (
<div className='mt-5 p-4 bg-gray-50 rounded-lg'>
<div className='flex gap-4'>
<div>
<span className='text-gray-600'>عنوان تم: </span>
<span className='font-semibold'>{themeInfo.title}</span>
</div>
<div>
<span className='text-gray-600'>نوع ورودی: </span>
<span className='font-semibold'>{themeInfo.inputType}</span>
</div>
</div>
</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={'عملیات'} />
</tr>
</thead>
<tbody>
{
data?.results?.data && data.results.data.length > 0 ? (
data.results.data.map((item) => {
return (
<tr className='tr' key={item._id}>
<Td text={item.name} />
<Td text={item.value.toString()} />
<Td text=''>
<div className='flex gap-2'>
<button
onClick={() => {
setSelectedThemeValue(item)
setIsUpdateModalOpen(true)
}}
className='cursor-pointer hover:opacity-70 transition-opacity'
>
<Edit size={16} color='#8C90A3' />
</button>
<TrashWithConfrim
onDelete={() => {
deleteThemeValue(item._id, {
onSuccess: () => {
toast.success('مقدار تم با موفقیت حذف شد')
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
}}
isLoading={isDeletingThemeValue}
/>
</div>
</Td>
</tr>
)
})
) : (
<tr>
<td colSpan={3} className='text-center py-8 text-gray-500'>
هیچ مقداری یافت نشد
</td>
</tr>
)
}
</tbody>
</table>
</div>
{id && (
<CreateThemeValueModal
open={isModalOpen}
close={() => setIsModalOpen(false)}
themeId={id}
/>
)}
<UpdateThemeValueModal
open={isUpdateModalOpen}
close={() => {
setIsUpdateModalOpen(false)
setSelectedThemeValue(null)
}}
themeValue={selectedThemeValue}
/>
</div>
)
}
export default Values
@@ -0,0 +1,114 @@
import { type FC } from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import DefaulModal from '@/components/DefaulModal'
import Input from '@/components/Input'
import Button from '@/components/Button'
import { useCreateThemeValue } from '../hooks/useThemeData'
import { extractErrorMessage } from '@/helpers/utils'
import { toast } from 'react-toastify'
import { TickCircle } from 'iconsax-react'
import type { CreateThemeValueType } from '../type/Types'
interface Props {
open: boolean
close: () => void
themeId: string
}
const CreateThemeValueModal: FC<Props> = ({ open, close, themeId }) => {
const { mutate: createThemeValue, isPending } = useCreateThemeValue()
const formik = useFormik<Omit<CreateThemeValueType, 'theme'>>({
initialValues: {
name: '',
value: '',
},
validationSchema: Yup.object({
name: Yup.string().required('نام الزامی است'),
value: Yup.string().required('مقدار الزامی است'),
}),
onSubmit: (values) => {
createThemeValue(
{
theme: themeId,
name: values.name,
value: values.value,
},
{
onSuccess: () => {
toast.success('مقدار تم با موفقیت ایجاد شد')
formik.resetForm()
close()
},
onError: (error) => {
toast.error(extractErrorMessage(error))
},
}
)
},
})
const handleClose = () => {
formik.resetForm()
close()
}
return (
<DefaulModal
open={open}
close={handleClose}
isHeader
title_header="افزودن مقدار تم"
>
<div className="mt-5 space-y-5 w-[400px]">
<Input
label="نام"
placeholder="نام را وارد کنید"
{...formik.getFieldProps('name')}
error_text={
formik.touched.name && formik.errors.name
? formik.errors.name
: ''
}
/>
<Input
label="مقدار"
placeholder="مقدار را وارد کنید"
{...formik.getFieldProps('value')}
error_text={
formik.touched.value && formik.errors.value
? formik.errors.value
: ''
}
/>
<div className="flex gap-3 pt-3">
<Button
variant="outline"
onClick={handleClose}
disabled={isPending}
className="w-fit"
>
انصراف
</Button>
<Button
onClick={() => formik.handleSubmit()}
isLoading={isPending}
disabled={!formik.isValid || isPending}
className="w-fit"
>
<div className="flex gap-2 items-center">
<TickCircle size={16} color="#fff" />
<div>ذخیره</div>
</div>
</Button>
</div>
</div>
</DefaulModal>
)
}
export default CreateThemeValueModal
@@ -0,0 +1,127 @@
import { type FC, useEffect } from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import DefaulModal from '@/components/DefaulModal'
import Input from '@/components/Input'
import Button from '@/components/Button'
import { useUpdateThemeValue } from '../hooks/useThemeData'
import { extractErrorMessage } from '@/helpers/utils'
import { toast } from 'react-toastify'
import { TickCircle } from 'iconsax-react'
import type { UpdateThemeValueType, ThemeValueType } from '../type/Types'
interface Props {
open: boolean
close: () => void
themeValue: ThemeValueType | null
}
const UpdateThemeValueModal: FC<Props> = ({ open, close, themeValue }) => {
const { mutate: updateThemeValue, isPending } = useUpdateThemeValue()
const formik = useFormik<UpdateThemeValueType>({
initialValues: {
name: '',
value: '',
},
validationSchema: Yup.object({
name: Yup.string().required('نام الزامی است'),
value: Yup.string().required('مقدار الزامی است'),
}),
onSubmit: (values) => {
if (!themeValue?._id) return
updateThemeValue(
{
id: themeValue._id,
params: {
name: values.name,
value: values.value,
},
},
{
onSuccess: () => {
toast.success('مقدار تم با موفقیت به‌روزرسانی شد')
formik.resetForm()
close()
},
onError: (error) => {
toast.error(extractErrorMessage(error))
},
}
)
},
})
useEffect(() => {
if (themeValue && open) {
formik.setValues({
name: themeValue.name,
value: themeValue.value.toString(),
})
}
}, [themeValue, open])
const handleClose = () => {
formik.resetForm()
close()
}
return (
<DefaulModal
open={open}
close={handleClose}
isHeader
title_header="ویرایش مقدار تم"
>
<div className="mt-5 space-y-5 w-[400px]">
<Input
label="نام"
placeholder="نام را وارد کنید"
{...formik.getFieldProps('name')}
error_text={
formik.touched.name && formik.errors.name
? formik.errors.name
: ''
}
/>
<Input
label="مقدار"
placeholder="مقدار را وارد کنید"
{...formik.getFieldProps('value')}
error_text={
formik.touched.value && formik.errors.value
? formik.errors.value
: ''
}
/>
<div className="flex gap-3 pt-3">
<Button
variant="outline"
onClick={handleClose}
disabled={isPending}
className="w-fit"
>
انصراف
</Button>
<Button
onClick={() => formik.handleSubmit()}
isLoading={isPending}
disabled={!formik.isValid || isPending}
className="w-fit"
>
<div className="flex gap-2 items-center">
<TickCircle size={16} color="#fff" />
<div>ذخیره</div>
</div>
</Button>
</div>
</div>
</DefaulModal>
)
}
export default UpdateThemeValueModal
+4
View File
@@ -0,0 +1,4 @@
export enum ThemeInputType {
Color = "color",
Text = "text",
}
+94
View File
@@ -0,0 +1,94 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/ThemeService";
import type { CreateThemeType, UpdateThemeValueType } from "../type/Types";
export const useGetThemes = () => {
return useQuery({
queryKey: ["themes"],
queryFn: api.getThemes,
});
};
export const useCreateTheme = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createTheme,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["themes"] });
},
});
};
export const useGetThemeById = (id: string) => {
return useQuery({
queryKey: ["theme", id],
queryFn: () => api.getThemeById(id),
enabled: !!id,
});
};
export const useUpdateTheme = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, params }: { id: string; params: CreateThemeType }) =>
api.updateTheme(id, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["theme"] });
queryClient.invalidateQueries({ queryKey: ["themes"] });
},
});
};
export const useDeleteTheme = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteTheme,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["themes"] });
},
});
};
export const useGetThemeValues = (id: string) => {
return useQuery({
queryKey: ["theme-values", id],
queryFn: () => api.getThemeValues(id),
enabled: !!id,
});
};
export const useCreateThemeValue = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createThemeValue,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["theme-values"] });
},
});
};
export const useUpdateThemeValue = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
params,
}: {
id: string;
params: UpdateThemeValueType;
}) => api.updateThemeValue(id, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["theme-values"] });
},
});
};
export const useDeleteThemeValue = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteThemeValue,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["theme-values"] });
},
});
};
+70
View File
@@ -0,0 +1,70 @@
import axios from "@/config/axios";
import type {
CreateThemeType,
GetThemesResponseType,
GetThemeByIdResponseType,
GetThemeValuesResponseType,
CreateThemeValueType,
UpdateThemeValueType,
} from "../type/Types";
export const getThemes = async (): Promise<GetThemesResponseType> => {
const { data } = await axios.get<GetThemesResponseType>(
"/admin/category/theme"
);
return data;
};
export const createTheme = async (params: CreateThemeType) => {
const { data } = await axios.post("/admin/category/theme", params);
return data;
};
export const getThemeById = async (
id: string
): Promise<GetThemeByIdResponseType> => {
const { data } = await axios.get<GetThemeByIdResponseType>(
`/admin/category/theme/${id}`
);
return data;
};
export const updateTheme = async (id: string, params: CreateThemeType) => {
const { data } = await axios.put(`/admin/category/theme`, {
themeId: id,
...params,
});
return data;
};
export const deleteTheme = async (id: string) => {
const { data } = await axios.delete(`/admin/category/theme/${id}`);
return data;
};
export const getThemeValues = async (
id: string
): Promise<GetThemeValuesResponseType> => {
const { data } = await axios.get<GetThemeValuesResponseType>(
`/admin/category/theme-value/${id}/values`
);
return data;
};
export const createThemeValue = async (params: CreateThemeValueType) => {
const { data } = await axios.post(`/admin/category/theme-value`, params);
return data;
};
export const updateThemeValue = async (
id: string,
params: UpdateThemeValueType
) => {
const { data } = await axios.put(`/admin/category/theme-value/${id}`, params);
return data;
};
export const deleteThemeValue = async (id: string) => {
const { data } = await axios.delete(`/admin/category/theme-value/${id}`);
return data;
};
+58
View File
@@ -0,0 +1,58 @@
import type { ThemeInputType } from "../enum/Enum";
export type CreateThemeType = {
title: string;
inputType: ThemeInputType;
};
export type ThemeType = {
_id: string;
title: string;
inputType: ThemeInputType;
createdAt: string;
updatedAt: string;
};
export type GetThemesResponseType = {
status: number;
success: boolean;
results: {
data: ThemeType[];
};
};
export type GetThemeByIdResponseType = {
status: number;
success: boolean;
results: {
data: ThemeType;
};
};
export type CreateThemeValueType = {
theme: string;
name: string;
value: string;
};
export type UpdateThemeValueType = {
name: string;
value: string;
};
export type ThemeValueType = {
_id: string;
theme: ThemeType;
name: string;
value: number;
createdAt: string;
updatedAt: string;
};
export type GetThemeValuesResponseType = {
status: number;
success: boolean;
results: {
data: ThemeValueType[];
};
};
+9
View File
@@ -83,6 +83,10 @@ import Questions from '@/pages/products/Questions'
import Returns from '@/pages/orders/Returns' import Returns from '@/pages/orders/Returns'
import ReturnDetails from '@/pages/orders/ReturnDetails' import ReturnDetails from '@/pages/orders/ReturnDetails'
import Cancels from '@/pages/orders/Cancels' import Cancels from '@/pages/orders/Cancels'
import ThemeList from '@/pages/theme/List'
import CreateTheme from '@/pages/theme/Create'
import UpdateTheme from '@/pages/theme/Update'
import Values from '@/pages/theme/Values'
const MainRouter: FC = () => { const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore() const { hasSubMenu } = useSharedStore()
@@ -203,6 +207,11 @@ const MainRouter: FC = () => {
<Route path={Pages.orders.return} element={<Returns />} /> <Route path={Pages.orders.return} element={<Returns />} />
<Route path={`${Pages.orders.returnDetails}:id`} element={<ReturnDetails />} /> <Route path={`${Pages.orders.returnDetails}:id`} element={<ReturnDetails />} />
<Route path={Pages.orders.cancels} element={<Cancels />} /> <Route path={Pages.orders.cancels} element={<Cancels />} />
<Route path={Pages.products.theme.list} element={<ThemeList />} />
<Route path={Pages.products.theme.create} element={<CreateTheme />} />
<Route path={`${Pages.products.theme.update}:id`} element={<UpdateTheme />} />
<Route path={`${Pages.products.theme.values}:id`} element={<Values />} />
</Routes> </Routes>
</div> </div>
</div> </div>
@@ -29,6 +29,11 @@ const ProductsSubMenu: FC = () => {
isActive={isActive('category')} isActive={isActive('category')}
link={Pages.category.list} link={Pages.category.list}
/> />
<SubMenuItem
title={'تم'}
isActive={isActive('theme')}
link={Pages.products.theme.list}
/>
<SubMenuItem <SubMenuItem
title={'برند ها'} title={'برند ها'}
isActive={isActive('brands')} isActive={isActive('brands')}