up
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
VITE_TOKEN_NAME = 'negareh_t'
|
VITE_TOKEN_NAME = negareh_t
|
||||||
VITE_REFRESH_TOKEN_NAME = 'negareh_rt'
|
VITE_REFRESH_TOKEN_NAME = negareh_rt
|
||||||
# VITE_API_BASE_URL = 'https://negare-api.danakcorp.com'
|
# VITE_API_BASE_URL = https://negare-api.danakcorp.com
|
||||||
VITE_API_BASE_URL = 'http://localhost:4000'
|
VITE_API_BASE_URL = 'http://localhost:4000'
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>negareh-console</title>
|
<title>
|
||||||
|
پنل کاربری
|
||||||
|
</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface TableInvoiceItem extends RowDataType {
|
|||||||
id: string
|
id: string
|
||||||
image: string | null
|
image: string | null
|
||||||
title: string
|
title: string
|
||||||
|
description: string
|
||||||
quantity: string
|
quantity: string
|
||||||
unitPrice: string
|
unitPrice: string
|
||||||
discountValue: string
|
discountValue: string
|
||||||
@@ -67,6 +68,7 @@ const InvoiceDetail: FC = () => {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
image: item.product?.images?.[0] ?? null,
|
image: item.product?.images?.[0] ?? null,
|
||||||
title: item.product?.title || '-',
|
title: item.product?.title || '-',
|
||||||
|
description: item.description || '-',
|
||||||
quantity: String(item.quantity),
|
quantity: String(item.quantity),
|
||||||
unitPrice: formatPrice(item.unitPrice),
|
unitPrice: formatPrice(item.unitPrice),
|
||||||
discountValue: discounts.value,
|
discountValue: discounts.value,
|
||||||
@@ -95,6 +97,10 @@ const InvoiceDetail: FC = () => {
|
|||||||
key: 'title',
|
key: 'title',
|
||||||
title: 'عنوان',
|
title: 'عنوان',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
title: 'توضیحات',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'quantity',
|
key: 'quantity',
|
||||||
title: 'تعداد',
|
title: 'تعداد',
|
||||||
|
|||||||
@@ -1,27 +1,184 @@
|
|||||||
import { type FC, type SelectHTMLAttributes } from 'react'
|
import { type ChangeEvent, type FC, type SelectHTMLAttributes, useEffect, useMemo, useState } from 'react'
|
||||||
import { useGetProducts } from '../hooks/useRequestData'
|
import { useGetCategories, useGetProducts } from '../hooks/useRequestData'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
import type { CategoryType, ProductType } from '../type/Types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error_text?: string,
|
error_text?: string,
|
||||||
|
onProductSelect?: (product: ProductType | undefined) => void,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const ProductsSelect: FC<Props> = (props) => {
|
const findCategoryById = (categories: CategoryType[], targetId: string): CategoryType | null => {
|
||||||
|
for (const category of categories) {
|
||||||
|
if (category.id === targetId) {
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = useGetProducts()
|
if (category.children?.length) {
|
||||||
|
const found = findCategoryById(category.children, targetId)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const findCategoryPath = (categories: CategoryType[], targetId: string): string[] | null => {
|
||||||
|
for (const category of categories) {
|
||||||
|
if (category.id === targetId) {
|
||||||
|
return [category.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.children?.length) {
|
||||||
|
const childPath = findCategoryPath(category.children, targetId)
|
||||||
|
if (childPath) {
|
||||||
|
return [category.id, ...childPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoriesAtLevel = (
|
||||||
|
categories: CategoryType[],
|
||||||
|
level: number,
|
||||||
|
selectedPath: string[],
|
||||||
|
): CategoryType[] => {
|
||||||
|
if (level === 0) return categories
|
||||||
|
|
||||||
|
const parentId = selectedPath[level - 1]
|
||||||
|
if (!parentId) return []
|
||||||
|
|
||||||
|
const parent = findCategoryById(categories, parentId)
|
||||||
|
return parent?.children ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductsSelect: FC<Props> = (props) => {
|
||||||
|
const { error_text, onProductSelect, value, onChange, ...rest } = props
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string[]>([])
|
||||||
|
|
||||||
|
const { data: categoriesData } = useGetCategories()
|
||||||
|
const categories = categoriesData?.data ?? []
|
||||||
|
|
||||||
|
const levelsToShow = useMemo(() => {
|
||||||
|
const levels = [0]
|
||||||
|
|
||||||
|
for (let level = 0; level < selectedPath.length; level++) {
|
||||||
|
const category = findCategoryById(categories, selectedPath[level])
|
||||||
|
if (category?.children?.length) {
|
||||||
|
levels.push(level + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels
|
||||||
|
}, [categories, selectedPath])
|
||||||
|
|
||||||
|
const activeCategoryId = selectedPath.at(-1) ?? ''
|
||||||
|
const activeCategory = activeCategoryId
|
||||||
|
? findCategoryById(categories, activeCategoryId)
|
||||||
|
: null
|
||||||
|
const isLeafCategory = !!activeCategory && !(activeCategory.children?.length)
|
||||||
|
const canLoadProducts = isLeafCategory
|
||||||
|
|
||||||
|
const { data: productsData } = useGetProducts(activeCategoryId || undefined, {
|
||||||
|
enabled: canLoadProducts,
|
||||||
|
})
|
||||||
|
|
||||||
|
const needsResolve = !!value && !selectedPath.length
|
||||||
|
const { data: resolveProductsData } = useGetProducts(undefined, {
|
||||||
|
enabled: needsResolve,
|
||||||
|
})
|
||||||
|
|
||||||
|
const productItems = useMemo(
|
||||||
|
() => (productsData?.data ?? []).map((product) => ({
|
||||||
|
label: product.title,
|
||||||
|
value: product.id,
|
||||||
|
})),
|
||||||
|
[productsData],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
setSelectedPath([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPath.length || !categories.length) return
|
||||||
|
|
||||||
|
const product = resolveProductsData?.data?.find((item) => item.id === value)
|
||||||
|
if (!product?.category) return
|
||||||
|
|
||||||
|
const categoryId = typeof product.category === 'string'
|
||||||
|
? product.category
|
||||||
|
: product.category.id
|
||||||
|
|
||||||
|
const path = findCategoryPath(categories, categoryId)
|
||||||
|
if (!path) return
|
||||||
|
|
||||||
|
setSelectedPath(path)
|
||||||
|
}, [value, resolveProductsData, categories, selectedPath.length])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
onProductSelect?.(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = productsData?.data?.find((item) => item.id === value)
|
||||||
|
?? resolveProductsData?.data?.find((item) => item.id === value)
|
||||||
|
|
||||||
|
onProductSelect?.(product)
|
||||||
|
}, [value, productsData, resolveProductsData, onProductSelect])
|
||||||
|
|
||||||
|
const emitProductChange = (nextValue: string) => {
|
||||||
|
onChange?.({
|
||||||
|
target: { value: nextValue },
|
||||||
|
} as ChangeEvent<HTMLSelectElement>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCategoryChange = (level: number, nextValue: string) => {
|
||||||
|
setSelectedPath((prev) => [...prev.slice(0, level), nextValue])
|
||||||
|
emitProductChange('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onChange?.(e)
|
||||||
|
|
||||||
|
const product = productsData?.data?.find((item) => item.id === e.target.value)
|
||||||
|
onProductSelect?.(product)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{levelsToShow.map((level) => {
|
||||||
|
const items = getCategoriesAtLevel(categories, level, selectedPath).map((category) => ({
|
||||||
|
label: category.title,
|
||||||
|
value: category.id,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
items={data?.data?.map((item) => {
|
key={level}
|
||||||
return {
|
items={items}
|
||||||
label: item.title,
|
label={level === 0 ? 'دستهبندی' : 'زیر دستهبندی'}
|
||||||
value: item.id + ''
|
placeholder={level === 0 ? 'انتخاب دستهبندی' : 'انتخاب زیر دستهبندی'}
|
||||||
}
|
value={selectedPath[level] ?? ''}
|
||||||
}) || []}
|
onChange={(e) => handleCategoryChange(level, e.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Select
|
||||||
|
items={productItems}
|
||||||
label='محصول'
|
label='محصول'
|
||||||
placeholder='انتخاب محصول'
|
placeholder='انتخاب محصول'
|
||||||
{...props}
|
value={value}
|
||||||
|
onChange={handleProductChange}
|
||||||
|
error_text={error_text}
|
||||||
|
disabled={!canLoadProducts}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'
|
import { useCallback, useState, type ChangeEvent, type FC } from 'react'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import VoiceRecorder from '@/components/VoiceRecorder'
|
import VoiceRecorder from '@/components/VoiceRecorder'
|
||||||
@@ -8,7 +8,7 @@ import ProductsSelect from './ProductsSelect'
|
|||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import type { AttachmentsType, RequestType, ProductType } from '../type/Types'
|
import type { AttachmentsType, RequestType, ProductType } from '../type/Types'
|
||||||
import { useGetAttributes, useGetProducts } from '../hooks/useRequestData'
|
import { useGetAttributes } from '../hooks/useRequestData'
|
||||||
import ManageAttribute from './ManageAttribute'
|
import ManageAttribute from './ManageAttribute'
|
||||||
import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
||||||
import { useRequestStore } from '../store/RequestStore'
|
import { useRequestStore } from '../store/RequestStore'
|
||||||
@@ -31,7 +31,6 @@ type Props = {
|
|||||||
const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) => {
|
const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) => {
|
||||||
const isEditing = editIndex !== null
|
const isEditing = editIndex !== null
|
||||||
|
|
||||||
const { data } = useGetProducts()
|
|
||||||
const setItems = useRequestStore((state) => state.setItems)
|
const setItems = useRequestStore((state) => state.setItems)
|
||||||
const [productSelected, setProductSelected] = useState<ProductType>()
|
const [productSelected, setProductSelected] = useState<ProductType>()
|
||||||
const [voiceFile, setVoiceFile] = useState<File>()
|
const [voiceFile, setVoiceFile] = useState<File>()
|
||||||
@@ -89,34 +88,26 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeProductId = formik.values.productId ?? initialItem?.productId
|
const resolvedProduct = productSelected
|
||||||
|
|
||||||
const resolvedProduct = useMemo(() => {
|
|
||||||
if (productSelected) return productSelected
|
|
||||||
if (!activeProductId || !data?.data) return undefined
|
|
||||||
return data.data.find((p) => p.id === activeProductId)
|
|
||||||
}, [productSelected, activeProductId, data])
|
|
||||||
|
|
||||||
const { data: attributes } = useGetAttributes(resolvedProduct?.id)
|
const { data: attributes } = useGetAttributes(resolvedProduct?.id)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initialItem?.productId || !data?.data) return
|
|
||||||
|
|
||||||
const product = data.data.find((p) => p.id === initialItem.productId)
|
|
||||||
if (!product) return
|
|
||||||
|
|
||||||
setProductSelected(product)
|
|
||||||
}, [initialItem, data])
|
|
||||||
|
|
||||||
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
const productId = e.target.value
|
const productId = e.target.value
|
||||||
const product = data?.data?.find((o) => o.id === productId)
|
|
||||||
|
|
||||||
if (product) {
|
if (!productId) {
|
||||||
setProductSelected(product)
|
setProductSelected(undefined)
|
||||||
|
formik.setFieldValue('productId', undefined)
|
||||||
|
formik.setFieldValue('attributes', [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
formik.setFieldValue('productId', productId)
|
formik.setFieldValue('productId', productId)
|
||||||
formik.setFieldValue('attributes', [])
|
formik.setFieldValue('attributes', [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleProductSelect = (product: ProductType | undefined) => {
|
||||||
|
setProductSelected(product)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUploading = singleUpload.isPending || multiUpload.isPending
|
const isUploading = singleUpload.isPending || multiUpload.isPending
|
||||||
@@ -155,6 +146,7 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
<ProductsSelect
|
<ProductsSelect
|
||||||
value={formik.values.productId ?? ''}
|
value={formik.values.productId ?? ''}
|
||||||
onChange={handleProductChange}
|
onChange={handleProductChange}
|
||||||
|
onProductSelect={handleProductSelect}
|
||||||
error_text={
|
error_text={
|
||||||
formik.touched.productId && formik.errors.productId
|
formik.touched.productId && formik.errors.productId
|
||||||
? formik.errors.productId
|
? formik.errors.productId
|
||||||
|
|||||||
@@ -2,10 +2,21 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||||||
import * as api from "../service/RequestService";
|
import * as api from "../service/RequestService";
|
||||||
import type { AddTicketType } from "../type/Types";
|
import type { AddTicketType } from "../type/Types";
|
||||||
|
|
||||||
export const useGetProducts = () => {
|
export const useGetCategories = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["products"],
|
queryKey: ["categories"],
|
||||||
queryFn: api.getProducts,
|
queryFn: api.getCategories,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetProducts = (
|
||||||
|
categoryId?: string,
|
||||||
|
options?: { enabled?: boolean },
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["products", categoryId ?? "all"],
|
||||||
|
queryFn: () => api.getProducts(categoryId),
|
||||||
|
enabled: options?.enabled ?? true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios from "@/config/axios";
|
|||||||
import type {
|
import type {
|
||||||
AddTicketType,
|
AddTicketType,
|
||||||
AttributeResponseType,
|
AttributeResponseType,
|
||||||
|
CategoriesResponseType,
|
||||||
MyRequestsResponseType,
|
MyRequestsResponseType,
|
||||||
RequestDetailResponseType,
|
RequestDetailResponseType,
|
||||||
RequestType,
|
RequestType,
|
||||||
@@ -9,8 +10,14 @@ import type {
|
|||||||
TicketsResponseType,
|
TicketsResponseType,
|
||||||
} from "../type/Types";
|
} from "../type/Types";
|
||||||
|
|
||||||
export const getProducts = async () => {
|
export const getCategories = async () => {
|
||||||
const { data } = await axios.get<ProductsResponseType>(`/public/products`);
|
const { data } = await axios.get<CategoriesResponseType>(`/public/categories`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProducts = async (categoryId?: string) => {
|
||||||
|
const query = categoryId ? `?categoryId=${categoryId}` : "";
|
||||||
|
const { data } = await axios.get<ProductsResponseType>(`/public/products${query}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type CategoryType = {
|
|||||||
order: number;
|
order: number;
|
||||||
parent?: CategoryType;
|
parent?: CategoryType;
|
||||||
title: string;
|
title: string;
|
||||||
|
children?: CategoryType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AttachmentsType = {
|
export type AttachmentsType = {
|
||||||
@@ -60,6 +61,8 @@ export type ProductType = {
|
|||||||
|
|
||||||
export type ProductsResponseType = BaseResponse<ProductType[]>;
|
export type ProductsResponseType = BaseResponse<ProductType[]>;
|
||||||
|
|
||||||
|
export type CategoriesResponseType = BaseResponse<CategoryType[]>;
|
||||||
|
|
||||||
export type StoreType = {
|
export type StoreType = {
|
||||||
items: RequestType[];
|
items: RequestType[];
|
||||||
setItems: (value: RequestType[]) => void;
|
setItems: (value: RequestType[]) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user