Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35e5feb22c | |||
| 19169e43de | |||
| 40ae2a0fba | |||
| 7009bcc660 | |||
| 6331cce7ac | |||
| bda60b7550 | |||
| 77bf29cb3f | |||
| 5126fd5b7c | |||
| f5c1f27b9c | |||
| 1295725deb | |||
| 5d134ba05d | |||
| 7e281ec794 | |||
| ded2c83662 | |||
| 533137a5af | |||
| a1443911fa | |||
| a492251e96 | |||
| e8a10b5780 | |||
| 9646f76001 | |||
| 7cf27823ee | |||
| b999a764a6 | |||
| 04ba8f3203 | |||
| 2d31a2eacf | |||
| 322e2f6173 | |||
| a4aaf21803 | |||
| 4e8068071f | |||
| 259fc8af97 | |||
| 15c6d29bb1 | |||
| 0a3e3bd316 |
@@ -1,4 +1,6 @@
|
||||
VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com'
|
||||
VITE_BASE_URL = 'https://api.fajrtabloshop.com'
|
||||
# VITE_BASE_URL = 'http://192.168.99.235:4000'
|
||||
|
||||
# VITE_BASE_URL = 'https://api.shinan.ir'
|
||||
VITE_TOKEN_NAME = 'sh_admin_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token'
|
||||
@@ -3,7 +3,7 @@ name: deploy to danak
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- tablofajr
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
|
||||
+13
-12
@@ -1,24 +1,25 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Install tzdata to support timezone settings
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories \
|
||||
&& npm config set registry https://package-mirror.liara.ir/repository/npm/
|
||||
|
||||
# Set the timezone to Asia/Tehran
|
||||
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||
ENV NPM_CONFIG_REGISTRY=https://package-mirror.liara.ir/repository/npm/
|
||||
|
||||
WORKDIR /build
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . ./
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
FROM nginx:stable-alpine AS production
|
||||
|
||||
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
|
||||
|
||||
COPY --from=builder /build/dist /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
|
||||
RUN cat /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>پنل مدیریت | فجر مرکزی</title>
|
||||
<title>پنل مدیریت | فجر تابلو مرکزی</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Generated
+22
-2
@@ -103,6 +103,7 @@
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -2497,6 +2498,7 @@
|
||||
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
@@ -2506,6 +2508,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2516,6 +2519,7 @@
|
||||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
@@ -2572,6 +2576,7 @@
|
||||
"integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.41.0",
|
||||
"@typescript-eslint/types": "8.41.0",
|
||||
@@ -2824,6 +2829,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3008,6 +3014,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001737",
|
||||
"electron-to-chromium": "^1.5.211",
|
||||
@@ -3554,6 +3561,7 @@
|
||||
"integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4166,6 +4174,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -4989,6 +4998,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -5089,6 +5099,7 @@
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
|
||||
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.3.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
@@ -5141,6 +5152,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -5156,6 +5168,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -5226,7 +5239,8 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-multi-date-picker": {
|
||||
"version": "4.5.2",
|
||||
@@ -5272,6 +5286,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -5461,7 +5476,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -5778,6 +5794,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5864,6 +5881,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6023,6 +6041,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -6114,6 +6133,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -24,22 +24,63 @@ export const ShipmentManagementModal = ({
|
||||
const removeShipmentFromAdminMutation = useRemoveShipmentFromAdmin();
|
||||
|
||||
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) =>
|
||||
!adminShipments.some((adminShipment: ShipmentMethod) => adminShipment._id === shipment._id)
|
||||
);
|
||||
const availableShipments = shipments.filter((shipment: Shipper) => {
|
||||
// اگر 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 () => {
|
||||
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) => {
|
||||
toast.error(extractErrorMessage(error));
|
||||
},
|
||||
onSuccess: () => {
|
||||
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('روش ارسال با موفقیت اضافه شد');
|
||||
}
|
||||
});
|
||||
@@ -87,10 +128,14 @@ export const ShipmentManagementModal = ({
|
||||
placeholder="انتخاب روش ارسال"
|
||||
value={selectedShipment}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setSelectedShipment(e.target.value)}
|
||||
items={availableShipments.map((shipment: Shipper) => ({
|
||||
value: shipment._id.toString(),
|
||||
label: shipment.name
|
||||
}))}
|
||||
items={availableShipments.map((shipment: Shipper, index: number) => {
|
||||
// اگر _id وجود داشته باشد، از آن استفاده کن، در غیر این صورت از index
|
||||
const value = shipment._id != null ? String(shipment._id) : `index-${index}`;
|
||||
return {
|
||||
value,
|
||||
label: shipment.name
|
||||
};
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
label="اضافه کردن"
|
||||
@@ -109,26 +154,28 @@ export const ShipmentManagementModal = ({
|
||||
<p className="text-gray-500 text-center py-8">هیچ روش ارسالی فعالی وجود ندارد</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{adminShipments.map((shipment: ShipmentMethod) => (
|
||||
<div key={shipment._id} className="flex items-center justify-between p-4 bg-gray-50 rounded-xl">
|
||||
<div>
|
||||
<h4 className="font-medium">{shipment.name}</h4>
|
||||
<p className="text-sm text-gray-600">{shipment.description}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
نوع ارسال: {getDeliveryTypeText(shipment.deliveryType)} |
|
||||
زمان ارسال: {shipment.deliveryTime} روز
|
||||
</p>
|
||||
{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>
|
||||
<h4 className="font-medium">{shipment.name}</h4>
|
||||
<p className="text-sm text-gray-600">{shipment.description}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
نوع ارسال: {getDeliveryTypeText(shipment.deliveryType)} |
|
||||
زمان ارسال: {shipment.deliveryTime} روز
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
label="حذف"
|
||||
onClick={() => handleRemoveShipmentFromAdmin(String(shipment._id))}
|
||||
isLoading={removeShipmentFromAdminMutation.isPending}
|
||||
className="w-fit"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
label="حذف"
|
||||
onClick={() => handleRemoveShipmentFromAdmin(shipment._id.toString())}
|
||||
isLoading={removeShipmentFromAdminMutation.isPending}
|
||||
className="w-fit"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+18
-5
@@ -1,19 +1,32 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
|
||||
interface Props {
|
||||
text: string,
|
||||
children?: ReactNode,
|
||||
dir?: string,
|
||||
align?: 'start' | 'center' | 'end',
|
||||
className?: string,
|
||||
}
|
||||
|
||||
const Td: FC<Props> = (props: Props) => {
|
||||
const alignClass = props.align === 'end'
|
||||
? 'text-end'
|
||||
: props.align === 'center'
|
||||
? 'text-center'
|
||||
: props.align === 'start'
|
||||
? 'text-start'
|
||||
: ''
|
||||
|
||||
return (
|
||||
<td className='td' style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
|
||||
<td
|
||||
className={clx('td', alignClass, props.className)}
|
||||
style={{ direction: props.dir === 'ltr' ? 'ltr' : 'rtl' }}
|
||||
>
|
||||
{
|
||||
props.text ?
|
||||
props.text
|
||||
:
|
||||
props.children
|
||||
props.text
|
||||
? props.text
|
||||
: props.children
|
||||
}
|
||||
</td>
|
||||
)
|
||||
|
||||
@@ -154,6 +154,12 @@ export const Pages = {
|
||||
attributes: "/products/category/attributes/",
|
||||
},
|
||||
products: {
|
||||
theme: {
|
||||
list: "/products/theme",
|
||||
create: "/products/theme/create",
|
||||
update: "/products/theme/update/",
|
||||
values: "/products/theme/values/",
|
||||
},
|
||||
create: "/products/create",
|
||||
list: "/products/list",
|
||||
variants: "/products/variants/",
|
||||
|
||||
@@ -88,7 +88,8 @@ const UpdateBrand: FC = () => {
|
||||
logoUrl: brand.logoUrl,
|
||||
})
|
||||
}
|
||||
}, [brandDetail, formik])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [brandDetail])
|
||||
|
||||
const categoryOptions = categoriesData?.results?.data?.map((category: CategoryTreeNode) => ({
|
||||
value: category._id,
|
||||
|
||||
@@ -9,7 +9,6 @@ import Textarea from '../../components/Textarea'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import Select from '../../components/Select'
|
||||
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
||||
import { CategoryThemeEnum } from './enum/Enum'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useCreateCategory, useGetCategories, useSingleUpload } from './hooks/useCategoryData'
|
||||
@@ -17,10 +16,12 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { useGetThemes } from '../theme/hooks/useThemeData'
|
||||
|
||||
const CreateCategory: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const getThemes = useGetThemes()
|
||||
const [iconFile, setIconFile] = useState<File>()
|
||||
const [imageFile, setImageFile] = useState<File>()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
@@ -38,14 +39,14 @@ const CreateCategory: FC = () => {
|
||||
imageUrl: '',
|
||||
description: '',
|
||||
parent: undefined,
|
||||
theme: '',
|
||||
themeId: undefined,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
||||
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
||||
description: Yup.string().required('توضیحات دسته الزامی است'),
|
||||
parent: Yup.string().optional(),
|
||||
theme: Yup.string().optional(),
|
||||
themeId: Yup.string().optional(),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
|
||||
@@ -76,12 +77,21 @@ const CreateCategory: FC = () => {
|
||||
})
|
||||
|
||||
const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || []
|
||||
const themesData = getThemes.data?.results?.data || []
|
||||
|
||||
const filteredCategories = categoriesData.filter(cat =>
|
||||
cat.title_fa.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) => {
|
||||
formik.setFieldValue('parent', category._id)
|
||||
}
|
||||
@@ -142,15 +152,10 @@ const CreateCategory: FC = () => {
|
||||
<Select
|
||||
label='تم دسته'
|
||||
placeholder='انتخاب تم دسته'
|
||||
items={[
|
||||
{ value: CategoryThemeEnum.Sized, label: 'سایز' },
|
||||
{ value: CategoryThemeEnum.Colored, label: 'رنگ' },
|
||||
{ value: CategoryThemeEnum.Meterage, label: 'متراژ' },
|
||||
{ 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 : ''}
|
||||
items={themeItems}
|
||||
value={formik.values.themeId || ''}
|
||||
onChange={(e) => formik.setFieldValue('themeId', e.target.value || undefined)}
|
||||
error_text={formik.touched.themeId && formik.errors.themeId ? formik.errors.themeId : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,11 +73,6 @@ const CategoryList: FC = () => {
|
||||
</div>
|
||||
</Td>
|
||||
<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="">
|
||||
<div className="flex items-center gap-2">
|
||||
{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={'عملیات'} />
|
||||
</tr>
|
||||
|
||||
@@ -8,7 +8,6 @@ import Textarea from '../../components/Textarea'
|
||||
import UploadBox from '../../components/UploadBox'
|
||||
import Select from '../../components/Select'
|
||||
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
||||
import { CategoryThemeEnum } from './enum/Enum'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
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 { Pages } from '../../config/Pages'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { useGetThemes } from '../theme/hooks/useThemeData'
|
||||
|
||||
const UpdateCategory: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const getThemes = useGetThemes()
|
||||
const [iconFile, setIconFile] = useState<File>()
|
||||
const [imageFile, setImageFile] = useState<File>()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
@@ -41,14 +42,14 @@ const UpdateCategory: FC = () => {
|
||||
imageUrl: '',
|
||||
description: '',
|
||||
parent: undefined,
|
||||
theme: '',
|
||||
themeId: undefined,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
||||
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
||||
description: Yup.string().required('توضیحات دسته الزامی است'),
|
||||
parent: Yup.string().optional(),
|
||||
theme: Yup.string().optional(),
|
||||
themeId: Yup.string().optional(),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
const updatedValues = { ...values }
|
||||
@@ -91,7 +92,7 @@ const UpdateCategory: FC = () => {
|
||||
imageUrl: categoryData.imageUrl || '',
|
||||
description: categoryData.description || '',
|
||||
parent: categoryData.parent || undefined,
|
||||
theme: categoryData.theme || '',
|
||||
themeId: categoryData.theme || undefined,
|
||||
})
|
||||
setIsActive(!categoryData.deleted)
|
||||
setIsDataLoaded(true)
|
||||
@@ -100,6 +101,15 @@ const UpdateCategory: FC = () => {
|
||||
}, [getCategoryById.data, isDataLoaded])
|
||||
|
||||
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 =>
|
||||
@@ -193,15 +203,10 @@ const UpdateCategory: FC = () => {
|
||||
<Select
|
||||
label='تم دسته'
|
||||
placeholder='انتخاب تم دسته'
|
||||
items={[
|
||||
{ value: CategoryThemeEnum.Sized, label: 'سایز' },
|
||||
{ value: CategoryThemeEnum.Colored, label: 'رنگ' },
|
||||
{ value: CategoryThemeEnum.Meterage, label: 'متراژ' },
|
||||
{ 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 : ''}
|
||||
items={themeItems}
|
||||
value={formik.values.themeId || ''}
|
||||
onChange={(e) => formik.setFieldValue('themeId', e.target.value || undefined)}
|
||||
error_text={formik.touched.themeId && formik.errors.themeId ? formik.errors.themeId : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,6 +268,28 @@ const UpdateCategory: FC = () => {
|
||||
<div className='text-sm bg-white py-6 px-4 rounded-2xl'>
|
||||
<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='relative'>
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
error_text?: string
|
||||
categories: CategoryTreeNode[]
|
||||
disabled?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
const CategorySelect: FC<Props> = ({
|
||||
@@ -22,7 +23,8 @@ const CategorySelect: FC<Props> = ({
|
||||
onChange,
|
||||
error_text,
|
||||
categories,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
compact = false
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
@@ -163,7 +165,10 @@ const CategorySelect: FC<Props> = ({
|
||||
return (
|
||||
<div className="w-full relative" ref={dropdownRef}>
|
||||
{label && (
|
||||
<label className="text-sm text-gray-700 mb-2 block font-medium">
|
||||
<label className={clx(
|
||||
'text-sm block',
|
||||
compact ? 'text-black' : 'text-gray-700 mb-2 font-medium'
|
||||
)}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
@@ -171,39 +176,54 @@ const CategorySelect: FC<Props> = ({
|
||||
{/* دکمه انتخاب */}
|
||||
<div
|
||||
className={clx(
|
||||
'w-full border border-gray-300 rounded-xl bg-white cursor-pointer transition-all duration-200',
|
||||
'hover:border-blue-400 hover:shadow-sm',
|
||||
isOpen && 'border-blue-500 ring-2 ring-blue-100',
|
||||
'w-full border bg-white cursor-pointer transition-all duration-200',
|
||||
compact
|
||||
? 'h-10 rounded-[10px] border-border hover:border-border'
|
||||
: 'border-gray-300 rounded-xl hover:border-blue-400 hover:shadow-sm',
|
||||
isOpen && (compact ? 'border-primary' : 'border-blue-500 ring-2 ring-blue-100'),
|
||||
disabled && 'opacity-50 cursor-not-allowed bg-gray-50',
|
||||
label && 'mt-1'
|
||||
)}
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className={clx(
|
||||
'flex items-center justify-between',
|
||||
compact ? 'h-full px-[10px]' : 'p-4'
|
||||
)}>
|
||||
<div className="flex-1 min-w-0">
|
||||
{selectedCategory ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{selectedCategory.children.length > 0 ? (
|
||||
<FolderOpen size={20} className="text-blue-500" />
|
||||
) : (
|
||||
<DocumentText size={20} className="text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{selectedCategory.title_fa}
|
||||
compact ? (
|
||||
<span className="text-sm text-black truncate block">
|
||||
{selectedCategory.title_fa}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{selectedCategory.children.length > 0 ? (
|
||||
<FolderOpen size={20} className="text-blue-500" />
|
||||
) : (
|
||||
<DocumentText size={20} className="text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{getCategoryPath(selectedCategory)}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{selectedCategory.title_fa}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{getCategoryPath(selectedCategory)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Folder2 size={20} className="text-gray-400" />
|
||||
<span className="text-gray-400">{placeholder}</span>
|
||||
</div>
|
||||
compact ? (
|
||||
<span className="text-sm text-gray-400">{placeholder}</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Folder2 size={20} className="text-gray-400" />
|
||||
<span className="text-gray-400">{placeholder}</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ const VariantModal: FC<VariantModalProps> = ({
|
||||
title_header={getModalTitle()}
|
||||
width={500}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 w-[400px]">
|
||||
{renderFormContent()}
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-border">
|
||||
|
||||
@@ -5,7 +5,7 @@ export type CategoryType = {
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
parent?: string;
|
||||
theme?: string;
|
||||
themeId?: string;
|
||||
};
|
||||
|
||||
export type CategoryResponseType = {
|
||||
@@ -48,6 +48,7 @@ export type CategoryTreeNode = {
|
||||
icon: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
theme: string;
|
||||
variants: number[];
|
||||
hierarchy: string[];
|
||||
leaf: boolean;
|
||||
@@ -56,7 +57,6 @@ export type CategoryTreeNode = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
theme?: string;
|
||||
children: CategoryTreeNode[];
|
||||
};
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ const OrderDetail: FC = () => {
|
||||
شماره سفارش: <span className='font-mono'>#{order._id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* کارتهای اطلاعاتی */}
|
||||
<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 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'>
|
||||
<StatusWithText
|
||||
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 className='flex-1'>
|
||||
<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>گارانتی: {product.variant.warranty.name}</div>
|
||||
<div>گارانتی: {product.variant.warranty?.name || 'ندارد'}</div>
|
||||
<div>تعداد: {product.quantity}</div>
|
||||
<div>قیمت واحد: {formatPrice(product.selling_price)} تومان</div>
|
||||
{product.shipmentCost > 0 && (
|
||||
<div>هزینه ارسال: {formatPrice(product.shipmentCost)} تومان</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export type PaymentMethodType = {
|
||||
|
||||
export type PriceDetailsType = {
|
||||
shipping_cost: number;
|
||||
shipping_cost_on_delivery?: number;
|
||||
process_cost: number;
|
||||
total_retail_price: number;
|
||||
total_payable_price: number;
|
||||
@@ -136,7 +137,9 @@ export type VariantType = {
|
||||
isWholeSale: boolean;
|
||||
shop: ShopType;
|
||||
warranty: WarrantyType;
|
||||
meterage: MeterageType;
|
||||
saleFormat: SaleFormatType;
|
||||
themeValue: ThemeValueType;
|
||||
meterage?: MeterageType;
|
||||
};
|
||||
|
||||
export type PriceType = {
|
||||
@@ -154,6 +157,13 @@ export type SaleFormatType = {
|
||||
wholeSale: unknown[];
|
||||
};
|
||||
|
||||
export type ThemeValueType = {
|
||||
_id: string;
|
||||
theme: string;
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type ShopType = {
|
||||
_id: string;
|
||||
shopName?: string;
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface PaymentMethod {
|
||||
|
||||
export interface PriceDetails {
|
||||
shipping_cost: number;
|
||||
shipping_cost_on_delivery?: number;
|
||||
process_cost: number;
|
||||
total_retail_price: number;
|
||||
total_payable_price: number;
|
||||
|
||||
+124
-38
@@ -2,6 +2,9 @@ import { type FC, useState } from 'react'
|
||||
import { useDeleteProduct, useDraftProduct, useGetProducts } from './hooks/useProductData';
|
||||
import { type ProductListType } from './types/Types';
|
||||
import { ProductStatus } from './enum/ProductEnum';
|
||||
import CategorySelect from '../category/components/CategorySelect';
|
||||
import { useGetCategories } from '../category/hooks/useCategoryData';
|
||||
import Select from '@/components/Select';
|
||||
import PageLoading from '../../components/PageLoading';
|
||||
import Error from '../../components/Error';
|
||||
import PaginationUi from '../../components/PaginationUi';
|
||||
@@ -10,7 +13,6 @@ import Td from '../../components/Td';
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim';
|
||||
import ModalConfrim from '../../components/ModalConfrim';
|
||||
import PageTitle from '../../components/PageTitle';
|
||||
import { Edit } from 'iconsax-react';
|
||||
import Button from '@/components/Button';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Pages } from '@/config/Pages';
|
||||
@@ -22,9 +24,16 @@ const List: FC = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [categoryId, setCategoryId] = useState('');
|
||||
const [status, setStatus] = useState<ProductStatus | ''>('');
|
||||
const [isDraftConfirmOpen, setIsDraftConfirmOpen] = useState(false);
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
const { data: productsData, isLoading, error, refetch } = useGetProducts(currentPage);
|
||||
const { data: categoriesData } = useGetCategories();
|
||||
const categories = categoriesData?.results?.data || [];
|
||||
const { data: productsData, isLoading, error, refetch } = useGetProducts(currentPage, 10, {
|
||||
...(categoryId && { categoryId }),
|
||||
...(status && { status }),
|
||||
});
|
||||
const deleteProductMutation = useDeleteProduct();
|
||||
const draftProductMutation = useDraftProduct();
|
||||
|
||||
@@ -68,18 +77,70 @@ const List: FC = () => {
|
||||
};
|
||||
|
||||
|
||||
const hasActiveFilters = !!categoryId || !!status;
|
||||
|
||||
const handleCategoryChange = (value: string) => {
|
||||
setCategoryId(value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleStatusChange = (value: string) => {
|
||||
setStatus(value as ProductStatus | '');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setCategoryId('');
|
||||
setStatus('');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle />
|
||||
<div className='flex justify-end mt-5'>
|
||||
<div className='flex flex-wrap items-end justify-between gap-4 mt-5'>
|
||||
<div className='flex flex-wrap items-end gap-4'>
|
||||
<div className='w-72'>
|
||||
<CategorySelect
|
||||
label='دستهبندی'
|
||||
placeholder='همه دستهبندیها'
|
||||
value={categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
categories={categories}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<div className='w-48'>
|
||||
<Select
|
||||
label='وضعیت'
|
||||
value={status}
|
||||
onChange={(e) => handleStatusChange(e.target.value)}
|
||||
items={[
|
||||
{ value: '', label: 'همه وضعیتها' },
|
||||
{ value: ProductStatus.Approved, label: 'تایید شده' },
|
||||
{ value: ProductStatus.Pending, label: 'در انتظار' },
|
||||
{ value: ProductStatus.Rejected, label: 'رد شده' },
|
||||
{ value: ProductStatus.Draft, label: 'پیشنویس' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
label='پاک کردن فیلترها'
|
||||
variant='outline'
|
||||
onClick={clearFilters}
|
||||
className='w-fit h-10'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
label='افزودن محصول'
|
||||
onClick={() => navigate(`${Pages.products.create}`)}
|
||||
className='w-fit'
|
||||
/>
|
||||
</div>
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||
<table className='w-full text-sm'>
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full border border-[#EAEDF5]'>
|
||||
<table className='w-full text-sm min-w-[960px]'>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={'تصویر'} />
|
||||
@@ -88,15 +149,15 @@ const List: FC = () => {
|
||||
<Td text={'دستهبندی'} />
|
||||
<Td text='تنوع' />
|
||||
<Td text={'وضعیت'} />
|
||||
<Td text={'قیمت'} />
|
||||
<Td text={'موجودی'} />
|
||||
<Td text={'عملیات'} />
|
||||
<Td text={'قیمت'} align='end' />
|
||||
<Td text={'موجودی'} align='end' />
|
||||
<Td text={'عملیات'} align='center' className='min-w-[120px]' />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.length === 0 ? (
|
||||
<tr className='tr'>
|
||||
<td colSpan={9} className="text-center py-8 text-gray-500">
|
||||
<td colSpan={9} className="text-center py-8 text-gray-500 text-sm">
|
||||
هیچ محصولی یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
@@ -104,7 +165,7 @@ const List: FC = () => {
|
||||
products.map((product: ProductListType) => (
|
||||
<tr key={product._id} className='tr'>
|
||||
<Td text="">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden">
|
||||
<div className="w-11 h-11 rounded-lg overflow-hidden bg-gray-50 shrink-0">
|
||||
<img
|
||||
src={product.imagesUrl?.cover || '/placeholder-image.png'}
|
||||
alt={product.title_fa}
|
||||
@@ -113,25 +174,35 @@ const List: FC = () => {
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
<div className="max-w-[220px]">
|
||||
<div className="font-medium text-gray-900 truncate" title={product.title_fa}>
|
||||
{product.title_fa}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">
|
||||
<div className="text-gray-400 text-xs truncate mt-0.5" title={product.title_en}>
|
||||
{product.title_en}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text={product.brand_title_fa} />
|
||||
<Td text={product.category_title_fa} />
|
||||
<Td text="">
|
||||
<span className="text-gray-700 max-w-[120px] truncate inline-block" title={product.brand_title_fa}>
|
||||
{product.brand_title_fa}
|
||||
</span>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<span className="text-gray-700 max-w-[120px] truncate inline-block" title={product.category_title_fa}>
|
||||
{product.category_title_fa}
|
||||
</span>
|
||||
</Td>
|
||||
<Td text=''>
|
||||
<Link
|
||||
to={`${Pages.products.variants}${product._id}`}
|
||||
>
|
||||
<Link to={`${Pages.products.variants}${product._id}`}>
|
||||
<Button
|
||||
className='h-8 text-xs'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='w-fit px-2.5 whitespace-nowrap'
|
||||
>
|
||||
مدیریت تنوع
|
||||
<span className="flex items-center gap-1">
|
||||
مدیریت تنوع
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</Td>
|
||||
@@ -141,26 +212,41 @@ const List: FC = () => {
|
||||
text={getStatusText(product.status)}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={`${product.default_variant?.price?.selling_price?.toLocaleString('fa-IR') || 0} تومان`} />
|
||||
<Td text={String(product.default_variant?.stock || 0)} />
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-2">
|
||||
{
|
||||
product.status !== ProductStatus.Draft && (
|
||||
<Button
|
||||
variant='outline'
|
||||
label='پیش نویس'
|
||||
className='text-xs h-8 px-2'
|
||||
onClick={() => {
|
||||
setSelectedProductId(String(product._id));
|
||||
setIsDraftConfirmOpen(true);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Td text="" align='end'>
|
||||
<span className="text-gray-800 font-medium whitespace-nowrap">
|
||||
{(product.default_variant?.price?.selling_price ?? 0).toLocaleString('fa-IR')}
|
||||
<span className="text-gray-400 font-normal ms-1">تومان</span>
|
||||
</span>
|
||||
</Td>
|
||||
<Td text="" align='end'>
|
||||
<span className="text-gray-700 tabular-nums">
|
||||
{product.default_variant?.stock ?? 0}
|
||||
</span>
|
||||
</Td>
|
||||
<Td text="" align='center' className='min-w-[120px]'>
|
||||
<div className="flex items-center justify-center gap-1.5">
|
||||
{product.status !== ProductStatus.Draft && (
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='w-fit px-2.5 shrink-0'
|
||||
onClick={() => {
|
||||
setSelectedProductId(String(product._id));
|
||||
setIsDraftConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
پیشنویس
|
||||
</Button>
|
||||
)}
|
||||
{product.status === ProductStatus.Draft && (
|
||||
<Link to={`${Pages.products.update}${product._id}`}>
|
||||
<Edit color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500" />
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='w-fit px-2.5 shrink-0'
|
||||
>
|
||||
ویرایش
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import PageTitle from '../../components/PageTitle'
|
||||
import { useGetProductVariants, useGetProductById } from './hooks/useProductData'
|
||||
import { useDeleteProductVariant, useGetProductVariants } from './hooks/useProductData'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import type { ProductVariantDetailType } from './types/Types'
|
||||
@@ -8,31 +8,20 @@ import Td from '@/components/Td'
|
||||
import StatusWithText from '@/components/StatusWithText'
|
||||
import CreateVariant from './components/CreateVariant'
|
||||
import UpdateVariant from './components/UpdateVariant'
|
||||
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||
|
||||
const ProductVariant: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const { data, isLoading, refetch } = useGetProductVariants(id!)
|
||||
const { data: productData } = useGetProductById(id!)
|
||||
|
||||
const categoryTheme = productData?.results?.product?.category?.theme
|
||||
const { mutate: deleteVariant } = useDeleteProductVariant()
|
||||
|
||||
const getVariantColumnHeader = () => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size': return 'سایز'
|
||||
case 'Color': return 'رنگ'
|
||||
case 'Meterage': return 'متریاژ'
|
||||
default: return 'تنوع'
|
||||
}
|
||||
return 'تنوع'
|
||||
}
|
||||
|
||||
const getVariantValue = (variant: ProductVariantDetailType) => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size': return variant.size?.value || '-'
|
||||
case 'Color': return variant.color?.name || '-'
|
||||
case 'Meterage': return variant.meterage?.value || '-'
|
||||
default: return '-'
|
||||
}
|
||||
return variant.themeValue?.name || variant.themeValue?.value?.toString() || '-'
|
||||
}
|
||||
|
||||
const getMarketStatusVariant = (status: string) => {
|
||||
@@ -58,7 +47,8 @@ const ProductVariant: FC = () => {
|
||||
<Td text="">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{variant.title_fa}
|
||||
{variant.title_fa + ' '}
|
||||
( {variant.themeValue?.name} )
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">
|
||||
{variant.title_en}
|
||||
@@ -88,7 +78,18 @@ const ProductVariant: FC = () => {
|
||||
</span>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<UpdateVariant variant={variant} onUpdate={refetch} />
|
||||
<div className='flex gap-2 items-center'>
|
||||
<UpdateVariant variant={variant} onUpdate={refetch} />
|
||||
<TrashWithConfrim
|
||||
onDelete={() => {
|
||||
deleteVariant(variant._id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -6,8 +6,8 @@ import DatePickerComponent from '@/components/DatePicker'
|
||||
import { useState, type FC } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useCreateProductVariant, useGetProductById, useGetProductVariants } from '../hooks/useProductData'
|
||||
import { useGetCategoryVariants } from '@/pages/category'
|
||||
import type { CreateProductVariantRequestType } from '../types/Types'
|
||||
import { useGetThemeValues } from '@/pages/theme/hooks/useThemeData'
|
||||
import { useGetWarranties } from '@/pages/warranty/hooks/useWarrantyData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
@@ -26,7 +26,7 @@ const CreateVariant: FC = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
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 createProductVariant = useCreateProductVariant()
|
||||
const { data: warrantiesData } = useGetWarranties()
|
||||
@@ -34,13 +34,15 @@ const CreateVariant: FC = () => {
|
||||
const [showModal, setShowModal] = useState<boolean>(false)
|
||||
|
||||
const categoryTheme = productData?.results?.product?.category?.theme
|
||||
const categoryVariants = categoryData?.results?.data?.category?.categoryVariants
|
||||
const isNoVariantTheme = categoryTheme === 'noColor_noSize'
|
||||
const isThemeNull = !categoryTheme
|
||||
const existingVariantsCount = variantsData?.results?.count || 0
|
||||
const canCreateVariant = !isNoVariantTheme || existingVariantsCount === 0
|
||||
const canCreateVariant = (isThemeNull || isNoVariantTheme)
|
||||
? existingVariantsCount === 0
|
||||
: true
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
variantId: isNoVariantTheme
|
||||
themeValueId: (isNoVariantTheme || isThemeNull)
|
||||
? Yup.string().optional()
|
||||
: Yup.string().required('انتخاب تنوع الزامی است'),
|
||||
warrantyId: Yup.string().required('انتخاب گارانتی الزامی است'),
|
||||
@@ -48,9 +50,9 @@ const CreateVariant: FC = () => {
|
||||
sellerSpecialCode: Yup.string(),
|
||||
retail_price: Yup.number().required('قیمت فروش الزامی است').min(0, 'قیمت نمیتواند منفی باشد'),
|
||||
package_weight: Yup.number().required('وزن بسته الزامی است').min(0.1, 'وزن باید حداقل ۰.۱ کیلوگرم باشد'),
|
||||
package_height: Yup.number().required('ارتفاع بسته الزامی است').min(1, 'ارتفاع باید حداقل ۱ سانتیمتر باشد'),
|
||||
package_length: Yup.number().required('طول بسته الزامی است').min(1, 'طول باید حداقل ۱ سانتیمتر باشد'),
|
||||
package_width: Yup.number().required('عرض بسته الزامی است').min(1, 'عرض باید حداقل ۱ سانتیمتر باشد'),
|
||||
// package_height: Yup.number().required('ارتفاع بسته الزامی است').min(1, 'ارتفاع باید حداقل ۱ سانتیمتر باشد'),
|
||||
// package_length: Yup.number().required('طول بسته الزامی است').min(1, 'طول باید حداقل ۱ سانتیمتر باشد'),
|
||||
// package_width: Yup.number().required('عرض بسته الزامی است').min(1, 'عرض باید حداقل ۱ سانتیمتر باشد'),
|
||||
postingTime: Yup.number().required('زمان ارسال الزامی است').min(1, 'زمان ارسال باید حداقل ۱ روز باشد'),
|
||||
stock: Yup.number().required('موجودی انبار الزامی است').min(0, 'موجودی نمیتواند منفی باشد'),
|
||||
specialSale_order_limit: Yup.number().min(0, 'محدودیت فروش ویژه نمیتواند منفی باشد'),
|
||||
@@ -59,15 +61,15 @@ const CreateVariant: FC = () => {
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
variantId: '',
|
||||
themeValueId: '',
|
||||
warrantyId: '',
|
||||
order_limit: '',
|
||||
sellerSpecialCode: '' as string | undefined,
|
||||
retail_price: '',
|
||||
package_weight: '',
|
||||
package_height: '',
|
||||
package_length: '',
|
||||
package_width: '',
|
||||
// package_height: '',
|
||||
// package_length: '',
|
||||
// package_width: '',
|
||||
postingTime: '',
|
||||
stock: '',
|
||||
specialSale_order_limit: '',
|
||||
@@ -76,34 +78,14 @@ const CreateVariant: FC = () => {
|
||||
}
|
||||
|
||||
const getVariantOptions = () => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size':
|
||||
return categoryVariants?.sizes?.map(size => ({
|
||||
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 []
|
||||
}
|
||||
return themeValues?.results?.data?.map(themeValue => ({
|
||||
value: themeValue._id,
|
||||
label: themeValue.name || themeValue.value?.toString() || '-'
|
||||
})) || []
|
||||
}
|
||||
|
||||
const getVariantLabel = () => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size': return 'سایز'
|
||||
case 'Color': return 'رنگ'
|
||||
case 'Meterage': return 'متریاژ'
|
||||
default: return 'تنوع'
|
||||
}
|
||||
return categoryTheme ? 'تنوع' : 'تنوع'
|
||||
}
|
||||
|
||||
const getWarrantyOptions = () => {
|
||||
@@ -121,9 +103,9 @@ const CreateVariant: FC = () => {
|
||||
sellerSpecialCode: values.sellerSpecialCode,
|
||||
retail_price: parseFloat(values.retail_price),
|
||||
package_weight: parseFloat(values.package_weight),
|
||||
package_height: parseFloat(values.package_height),
|
||||
package_length: parseFloat(values.package_length),
|
||||
package_width: parseFloat(values.package_width),
|
||||
package_height: 10, // مقدار پیشفرض
|
||||
package_length: 10, // مقدار پیشفرض
|
||||
package_width: 10, // مقدار پیشفرض
|
||||
postingTime: parseInt(values.postingTime),
|
||||
stock: parseInt(values.stock)
|
||||
}
|
||||
@@ -131,18 +113,11 @@ const CreateVariant: FC = () => {
|
||||
// اضافه کردن variant بر اساس theme
|
||||
let variantData: CreateProductVariantRequestType
|
||||
|
||||
if (isNoVariantTheme) {
|
||||
// در حالت noColor_noSize هیچ variantId ارسال نمیشود
|
||||
if (isNoVariantTheme || isThemeNull) {
|
||||
// در حالت noColor_noSize یا theme null هیچ themeValueId ارسال نمیشود
|
||||
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 {
|
||||
variantData = baseData
|
||||
variantData = { ...baseData, themeValueId: values.themeValueId }
|
||||
}
|
||||
|
||||
if (!values.sellerSpecialCode) {
|
||||
@@ -160,8 +135,9 @@ const CreateVariant: FC = () => {
|
||||
toast.success('تنوع جدید با موفقیت ایجاد شد')
|
||||
// Refetch لیست تنوعها
|
||||
refetchVariants()
|
||||
// یا میتوانید از invalidateQueries استفاده کنید
|
||||
// Invalidate لیست تنوعها و جزئیات محصول
|
||||
queryClient.invalidateQueries({ queryKey: ['product-variants', id] })
|
||||
queryClient.invalidateQueries({ queryKey: ['product-by-id', id] })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
@@ -199,29 +175,29 @@ const CreateVariant: FC = () => {
|
||||
>
|
||||
{({ setFieldValue }) => (
|
||||
<Form className="space-y-4 w-[500px] mt-4">
|
||||
{/* انتخاب variant - فقط در صورتی که theme نوع noColor_noSize نباشد */}
|
||||
{!isNoVariantTheme && (
|
||||
{/* انتخاب variant - فقط در صورتی که theme معتبر باشد و نوع noColor_noSize نباشد */}
|
||||
{!isNoVariantTheme && !isThemeNull && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 mb-1 block">
|
||||
{getVariantLabel()}
|
||||
</label>
|
||||
<Field name="variantId">
|
||||
<Field name="themeValueId">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Select
|
||||
{...field}
|
||||
placeholder={`انتخاب ${getVariantLabel().toLowerCase()}`}
|
||||
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()}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* پیام اطلاعرسانی برای محصولات بدون تنوع */}
|
||||
{isNoVariantTheme && (
|
||||
{(isNoVariantTheme || isThemeNull) && (
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* <div>
|
||||
<Field name="package_height">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
@@ -364,7 +340,7 @@ const CreateVariant: FC = () => {
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="package_width" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 Input from '../../../components/Input';
|
||||
import Select from '../../../components/Select';
|
||||
@@ -48,8 +48,17 @@ const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading,
|
||||
isFake: false
|
||||
});
|
||||
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
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 }));
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
@@ -189,76 +189,76 @@ const TagInput: FC<TagInputProps> = ({ tags, onChange, placeholder }) => {
|
||||
};
|
||||
|
||||
// کامپوننت مدیریت لیست مزایا/معایب
|
||||
interface ListInputProps {
|
||||
items: string[];
|
||||
onChange: (items: string[]) => void;
|
||||
placeholder?: string;
|
||||
label: string;
|
||||
}
|
||||
// interface ListInputProps {
|
||||
// items: string[];
|
||||
// onChange: (items: string[]) => void;
|
||||
// placeholder?: string;
|
||||
// label: string;
|
||||
// }
|
||||
|
||||
const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label }) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
// const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label }) => {
|
||||
// const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const addItem = () => {
|
||||
const newItem = inputValue.trim();
|
||||
if (newItem && !items.includes(newItem)) {
|
||||
onChange([...items, newItem]);
|
||||
setInputValue('');
|
||||
}
|
||||
};
|
||||
// const addItem = () => {
|
||||
// const newItem = inputValue.trim();
|
||||
// if (newItem && !items.includes(newItem)) {
|
||||
// onChange([...items, newItem]);
|
||||
// setInputValue('');
|
||||
// }
|
||||
// };
|
||||
|
||||
const removeItem = (itemToRemove: string) => {
|
||||
onChange(items.filter(item => item !== itemToRemove));
|
||||
};
|
||||
// const removeItem = (itemToRemove: string) => {
|
||||
// onChange(items.filter(item => item !== itemToRemove));
|
||||
// };
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addItem();
|
||||
}
|
||||
};
|
||||
// const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
// if (e.key === 'Enter') {
|
||||
// e.preventDefault();
|
||||
// addItem();
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
disabled={!inputValue.trim()}
|
||||
className="px-4 w-fit"
|
||||
>
|
||||
افزودن
|
||||
</Button>
|
||||
</div>
|
||||
{items.length > 0 && (
|
||||
<div className="space-y-2 flex flex-wrap gap-3 max-h-40 overflow-y-auto">
|
||||
{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">
|
||||
<span className="text-sm text-gray-700">{item}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeItem(item)}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
>
|
||||
<CloseCircle size={16} color='red' />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className='min-w-[30%] flex-1 px-2'></div>
|
||||
<div className='min-w-[30%] flex-1 px-2'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <div className="space-y-3">
|
||||
// <label className="block text-sm font-medium text-gray-700">{label}</label>
|
||||
// <div className="flex gap-2">
|
||||
// <Input
|
||||
// value={inputValue}
|
||||
// onChange={(e) => setInputValue(e.target.value)}
|
||||
// onKeyDown={handleKeyDown}
|
||||
// placeholder={placeholder}
|
||||
// className="flex-1"
|
||||
// />
|
||||
// <Button
|
||||
// type="button"
|
||||
// onClick={addItem}
|
||||
// disabled={!inputValue.trim()}
|
||||
// className="px-4 w-fit"
|
||||
// >
|
||||
// افزودن
|
||||
// </Button>
|
||||
// </div>
|
||||
// {items.length > 0 && (
|
||||
// <div className="space-y-2 flex flex-wrap gap-3 max-h-40 overflow-y-auto">
|
||||
// {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">
|
||||
// <span className="text-sm text-gray-700">{item}</span>
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => removeItem(item)}
|
||||
// className="text-red-500 hover:text-red-700 p-1"
|
||||
// >
|
||||
// <CloseCircle size={16} color='red' />
|
||||
// </button>
|
||||
// </div>
|
||||
// ))}
|
||||
// <div className='min-w-[30%] flex-1 px-2'></div>
|
||||
// <div className='min-w-[30%] flex-1 px-2'></div>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId, initialData, specifications }) => {
|
||||
const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>(() => ({
|
||||
@@ -266,8 +266,10 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
|
||||
description: initialData?.description || '',
|
||||
metaDescription: initialData?.metaDescription || '',
|
||||
tags: initialData?.tags || [],
|
||||
advantages: initialData?.advantages || [],
|
||||
disAdvantages: initialData?.disAdvantages || []
|
||||
// advantages: initialData?.advantages || [],
|
||||
// disAdvantages: initialData?.disAdvantages || []
|
||||
advantages: [],
|
||||
disAdvantages: []
|
||||
}));
|
||||
|
||||
const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || '');
|
||||
@@ -403,7 +405,7 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ListInput
|
||||
{/* <ListInput
|
||||
label="مزایا"
|
||||
items={formData.advantages}
|
||||
onChange={(advantages) => setFormData(prev => ({ ...prev, advantages }))}
|
||||
@@ -415,7 +417,7 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
|
||||
items={formData.disAdvantages}
|
||||
onChange={(disAdvantages) => setFormData(prev => ({ ...prev, disAdvantages }))}
|
||||
placeholder="عیبی را وارد کنید..."
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<div className="flex gap-2 justify-between">
|
||||
<Button
|
||||
|
||||
@@ -12,6 +12,7 @@ import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik'
|
||||
import type { FieldInputProps } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
interface FormikFieldProps {
|
||||
field: FieldInputProps<string | number | undefined>
|
||||
@@ -26,6 +27,7 @@ interface UpdateVariantProps {
|
||||
const UpdateVariant: FC<UpdateVariantProps> = ({ variant, onUpdate }) => {
|
||||
|
||||
const { id } = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const { data: variantData, isLoading, error } = useGetVariantById(id!, variant._id)
|
||||
|
||||
const updateVariant = useUpdateVariant()
|
||||
@@ -104,7 +106,12 @@ const UpdateVariant: FC<UpdateVariantProps> = ({ variant, onUpdate }) => {
|
||||
onSuccess: () => {
|
||||
setShowModal(false)
|
||||
toast.success('تنوع با موفقیت بروزرسانی شد')
|
||||
// Refetch لیست تنوعها
|
||||
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) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type UpdateProductRequestType
|
||||
} from "../types/Types";
|
||||
import { useSharedStore } from '@/shared/store/sharedStore';
|
||||
import { type GetProductsFilters } from '../service/ProductService';
|
||||
|
||||
export const useCreateProductDetail = () => {
|
||||
return useMutation({
|
||||
@@ -29,11 +30,15 @@ export const useSaveProduct = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProducts = (page: number = 1, limit: number = 10) => {
|
||||
export const useGetProducts = (
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
filters?: Pick<GetProductsFilters, 'categoryId' | 'status'>
|
||||
) => {
|
||||
const { search } = useSharedStore();
|
||||
return useQuery({
|
||||
queryKey: ['products', page, limit, search],
|
||||
queryFn: () => api.getProducts(page, limit, search),
|
||||
queryKey: ['products', page, limit, search, filters?.categoryId, filters?.status],
|
||||
queryFn: () => api.getProducts(page, limit, { search, ...filters }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -94,7 +99,7 @@ export const useUpdateProduct = () => {
|
||||
export const useSearchProducts = (search: string, enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['products-search', search],
|
||||
queryFn: () => api.getProducts(1, 50, search),
|
||||
queryFn: () => api.getProducts(1, 50, { search }),
|
||||
enabled: enabled,
|
||||
});
|
||||
};
|
||||
@@ -141,4 +146,10 @@ export const useDraftProduct = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.draftProduct,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteProductVariant = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.deleteProductVariant,
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { ProductStatus } from "../enum/ProductEnum";
|
||||
import {
|
||||
type CreateProductDetailRequestType,
|
||||
type CreateProductDetailResponseType,
|
||||
@@ -24,6 +25,12 @@ import {
|
||||
type UpdateProductResponseType
|
||||
} from "../types/Types";
|
||||
|
||||
export type GetProductsFilters = {
|
||||
search?: string;
|
||||
categoryId?: string;
|
||||
status?: ProductStatus;
|
||||
};
|
||||
|
||||
export const createProductDetail = async (
|
||||
variables: CreateProductDetailRequestType
|
||||
): Promise<CreateProductDetailResponseType> => {
|
||||
@@ -45,10 +52,20 @@ export const saveProduct = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getProducts = async (page: number = 1, limit: number = 10, search?: string): Promise<GetProductsResponseType> => {
|
||||
export const getProducts = async (
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
filters?: GetProductsFilters
|
||||
): Promise<GetProductsResponseType> => {
|
||||
const params: Record<string, string | number> = { page, limit };
|
||||
if (search && search.trim()) {
|
||||
params.q = search.trim();
|
||||
if (filters?.search?.trim()) {
|
||||
params.q = filters.search.trim();
|
||||
}
|
||||
if (filters?.categoryId) {
|
||||
params.categoryId = filters.categoryId;
|
||||
}
|
||||
if (filters?.status) {
|
||||
params.status = filters.status;
|
||||
}
|
||||
|
||||
const { data } = await axios.get(`/admin/products/native`, {
|
||||
@@ -132,4 +149,9 @@ export const answerProductQuestion = async (params: {id: string, answer: string}
|
||||
export const draftProduct = async (id: string) => {
|
||||
const { data } = await axios.post(`/admin/products/${id}/draft`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteProductVariant = async (variantId: string) => {
|
||||
const { data } = await axios.delete(`/admin/products/variants/${variantId}`);
|
||||
return data;
|
||||
};
|
||||
@@ -242,6 +242,14 @@ export type ProductVariantDetailType = {
|
||||
isFreeShip: boolean;
|
||||
isWholeSale: boolean;
|
||||
saleFormat: SaleFormatType;
|
||||
themeValue?: {
|
||||
_id: string;
|
||||
theme: {
|
||||
title: string;
|
||||
};
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
size?: SizeType;
|
||||
color?: ColorType;
|
||||
meterage?: MeterageType;
|
||||
@@ -266,17 +274,15 @@ export type GetVariantByIdResponseType = {
|
||||
|
||||
export type CreateProductVariantRequestType = {
|
||||
productId: string;
|
||||
colorId?: number;
|
||||
sizeId?: number;
|
||||
meterageId?: number;
|
||||
themeValueId?: string;
|
||||
warrantyId: number;
|
||||
order_limit: number;
|
||||
sellerSpecialCode?: string;
|
||||
retail_price: number;
|
||||
package_weight: number;
|
||||
package_height: number;
|
||||
package_length: number;
|
||||
package_width: number;
|
||||
package_height?: number;
|
||||
package_length?: number;
|
||||
package_width?: number;
|
||||
postingTime: number;
|
||||
stock: number;
|
||||
specialSale_order_limit?: number;
|
||||
@@ -305,14 +311,14 @@ export type ProductCategoryType = {
|
||||
icon: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
theme: string;
|
||||
variants: number[];
|
||||
hierarchy: string[];
|
||||
leaf: boolean;
|
||||
parent: string;
|
||||
parent: string | null;
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
theme: string;
|
||||
url: string;
|
||||
children: ProductCategoryType[];
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TickCircle, Add, Trash } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '../../helpers/utils'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
|
||||
const CreateShipment: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
@@ -25,6 +26,7 @@ const CreateShipment: FC = () => {
|
||||
costs: [],
|
||||
deliveryTime: 0,
|
||||
deliveryType: 'Standard',
|
||||
payDeliveryFeeOnDelivery: false,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('عنوان روش ارسال الزامی است'),
|
||||
@@ -134,6 +136,19 @@ const CreateShipment: FC = () => {
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between border border-gray-200 rounded-lg p-4'>
|
||||
<div>
|
||||
<div className='font-medium text-gray-900'>پرداخت هزینه ارسال در زمان تحویل</div>
|
||||
<div className='text-xs text-gray-500 mt-1'>
|
||||
در صورت فعال بودن، هزینه ارسال در زمان تحویل پرداخت میشود و در پرداخت آنلاین لحاظ نمیگردد.
|
||||
</div>
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.payDeliveryFeeOnDelivery ?? false}
|
||||
onChange={(value) => formik.setFieldValue('payDeliveryFeeOnDelivery', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ const ShippingList: FC = () => {
|
||||
<Td text={'توضیحات'} />
|
||||
<Td text={'نوع ارسال'} />
|
||||
<Td text={'زمان ارسال (روز)'} />
|
||||
<Td text={'پرداخت در محل'} />
|
||||
<Td text={'هزینهها'} />
|
||||
<Td text={'عملیات'} />
|
||||
</tr>
|
||||
@@ -106,6 +107,7 @@ const ShippingList: FC = () => {
|
||||
<Td text={shipment.description} />
|
||||
<Td text={getDeliveryTypeText(shipment.deliveryType)} />
|
||||
<Td text={shipment.deliveryTime.toString()} />
|
||||
<Td text={shipment.payDeliveryFeeOnDelivery ? 'بله' : 'خیر'} />
|
||||
<Td text="">
|
||||
<div className="text-sm">
|
||||
{shipment.costs.length > 0 ? (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TickCircle, Add, Trash } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '../../helpers/utils'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import SwitchComponent from '../../components/Switch'
|
||||
|
||||
const UpdateShipment: FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
@@ -27,6 +28,7 @@ const UpdateShipment: FC = () => {
|
||||
costs: shipmentDetail?.costs || [],
|
||||
deliveryTime: shipmentDetail?.deliveryTime || 0,
|
||||
deliveryType: shipmentDetail?.deliveryType || 'Standard',
|
||||
payDeliveryFeeOnDelivery: shipmentDetail?.payDeliveryFeeOnDelivery ?? false,
|
||||
},
|
||||
enableReinitialize: true,
|
||||
validationSchema: Yup.object({
|
||||
@@ -170,6 +172,19 @@ const UpdateShipment: FC = () => {
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between border border-gray-200 rounded-lg p-4'>
|
||||
<div>
|
||||
<div className='font-medium text-gray-900'>پرداخت هزینه ارسال در زمان تحویل</div>
|
||||
<div className='text-xs text-gray-500 mt-1'>
|
||||
در صورت فعال بودن، هزینه ارسال در زمان تحویل پرداخت میشود و در پرداخت آنلاین لحاظ نمیگردد.
|
||||
</div>
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.payDeliveryFeeOnDelivery ?? false}
|
||||
onChange={(value) => formik.setFieldValue('payDeliveryFeeOnDelivery', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Shipper {
|
||||
costs: Cost[];
|
||||
deliveryType: "Standard" | "SameDay" | "Express";
|
||||
deliveryTime: number;
|
||||
payDeliveryFeeOnDelivery: boolean;
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -35,6 +36,7 @@ export interface CreateShipper {
|
||||
description: string;
|
||||
costs: Cost[];
|
||||
deliveryTime: number;
|
||||
payDeliveryFeeOnDelivery?: boolean;
|
||||
deliveryType?: "Standard" | "SameDay" | "Express";
|
||||
}
|
||||
|
||||
@@ -62,6 +64,7 @@ export interface ShipmentMethod {
|
||||
costs: Cost[];
|
||||
deliveryType: string;
|
||||
deliveryTime: number;
|
||||
payDeliveryFeeOnDelivery: boolean;
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum ThemeInputType {
|
||||
Color = "color",
|
||||
Text = "text",
|
||||
}
|
||||
@@ -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"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
};
|
||||
@@ -83,6 +83,10 @@ import Questions from '@/pages/products/Questions'
|
||||
import Returns from '@/pages/orders/Returns'
|
||||
import ReturnDetails from '@/pages/orders/ReturnDetails'
|
||||
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 { hasSubMenu } = useSharedStore()
|
||||
@@ -203,6 +207,11 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.orders.return} element={<Returns />} />
|
||||
<Route path={`${Pages.orders.returnDetails}:id`} element={<ReturnDetails />} />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,11 @@ const ProductsSubMenu: FC = () => {
|
||||
isActive={isActive('category')}
|
||||
link={Pages.category.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={'تم'}
|
||||
isActive={isActive('theme')}
|
||||
link={Pages.products.theme.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={'برند ها'}
|
||||
isActive={isActive('brands')}
|
||||
|
||||
Reference in New Issue
Block a user