up
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
VITE_TOKEN_NAME = 'negareh_t'
|
||||
VITE_REFRESH_TOKEN_NAME = 'negareh_rt'
|
||||
# VITE_API_BASE_URL = 'https://negare-api.danakcorp.com'
|
||||
VITE_TOKEN_NAME = negareh_t
|
||||
VITE_REFRESH_TOKEN_NAME = negareh_rt
|
||||
# VITE_API_BASE_URL = https://negare-api.danakcorp.com
|
||||
VITE_API_BASE_URL = 'http://localhost:4000'
|
||||
|
||||
+3
-1
@@ -4,7 +4,9 @@
|
||||
<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>negareh-console</title>
|
||||
<title>
|
||||
پنل کاربری
|
||||
</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface TableInvoiceItem extends RowDataType {
|
||||
id: string
|
||||
image: string | null
|
||||
title: string
|
||||
description: string
|
||||
quantity: string
|
||||
unitPrice: string
|
||||
discountValue: string
|
||||
@@ -67,6 +68,7 @@ const InvoiceDetail: FC = () => {
|
||||
id: item.id,
|
||||
image: item.product?.images?.[0] ?? null,
|
||||
title: item.product?.title || '-',
|
||||
description: item.description || '-',
|
||||
quantity: String(item.quantity),
|
||||
unitPrice: formatPrice(item.unitPrice),
|
||||
discountValue: discounts.value,
|
||||
@@ -95,6 +97,10 @@ const InvoiceDetail: FC = () => {
|
||||
key: 'title',
|
||||
title: 'عنوان',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
title: 'توضیحات',
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
title: 'تعداد',
|
||||
|
||||
@@ -1,27 +1,184 @@
|
||||
import { type FC, type SelectHTMLAttributes } from 'react'
|
||||
import { useGetProducts } from '../hooks/useRequestData'
|
||||
import { type ChangeEvent, type FC, type SelectHTMLAttributes, useEffect, useMemo, useState } from 'react'
|
||||
import { useGetCategories, useGetProducts } from '../hooks/useRequestData'
|
||||
import Select from '@/components/Select'
|
||||
import type { CategoryType, ProductType } from '../type/Types'
|
||||
|
||||
type Props = {
|
||||
error_text?: string,
|
||||
onProductSelect?: (product: ProductType | undefined) => void,
|
||||
} & 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 (
|
||||
<Select
|
||||
items={data?.data?.map((item) => {
|
||||
return {
|
||||
label: item.title,
|
||||
value: item.id + ''
|
||||
}
|
||||
}) || []}
|
||||
key={level}
|
||||
items={items}
|
||||
label={level === 0 ? 'دستهبندی' : 'زیر دستهبندی'}
|
||||
placeholder={level === 0 ? 'انتخاب دستهبندی' : 'انتخاب زیر دستهبندی'}
|
||||
value={selectedPath[level] ?? ''}
|
||||
onChange={(e) => handleCategoryChange(level, e.target.value)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Select
|
||||
items={productItems}
|
||||
label='محصول'
|
||||
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 UploadBox from '@/components/UploadBox'
|
||||
import VoiceRecorder from '@/components/VoiceRecorder'
|
||||
@@ -8,7 +8,7 @@ import ProductsSelect from './ProductsSelect'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import type { AttachmentsType, RequestType, ProductType } from '../type/Types'
|
||||
import { useGetAttributes, useGetProducts } from '../hooks/useRequestData'
|
||||
import { useGetAttributes } from '../hooks/useRequestData'
|
||||
import ManageAttribute from './ManageAttribute'
|
||||
import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
||||
import { useRequestStore } from '../store/RequestStore'
|
||||
@@ -31,7 +31,6 @@ type Props = {
|
||||
const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) => {
|
||||
const isEditing = editIndex !== null
|
||||
|
||||
const { data } = useGetProducts()
|
||||
const setItems = useRequestStore((state) => state.setItems)
|
||||
const [productSelected, setProductSelected] = useState<ProductType>()
|
||||
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 = useMemo(() => {
|
||||
if (productSelected) return productSelected
|
||||
if (!activeProductId || !data?.data) return undefined
|
||||
return data.data.find((p) => p.id === activeProductId)
|
||||
}, [productSelected, activeProductId, data])
|
||||
const resolvedProduct = productSelected
|
||||
|
||||
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 productId = e.target.value
|
||||
const product = data?.data?.find((o) => o.id === productId)
|
||||
|
||||
if (product) {
|
||||
setProductSelected(product)
|
||||
if (!productId) {
|
||||
setProductSelected(undefined)
|
||||
formik.setFieldValue('productId', undefined)
|
||||
formik.setFieldValue('attributes', [])
|
||||
return
|
||||
}
|
||||
|
||||
formik.setFieldValue('productId', productId)
|
||||
formik.setFieldValue('attributes', [])
|
||||
}
|
||||
|
||||
const handleProductSelect = (product: ProductType | undefined) => {
|
||||
setProductSelected(product)
|
||||
}
|
||||
|
||||
const isUploading = singleUpload.isPending || multiUpload.isPending
|
||||
@@ -155,6 +146,7 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
||||
<ProductsSelect
|
||||
value={formik.values.productId ?? ''}
|
||||
onChange={handleProductChange}
|
||||
onProductSelect={handleProductSelect}
|
||||
error_text={
|
||||
formik.touched.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 type { AddTicketType } from "../type/Types";
|
||||
|
||||
export const useGetProducts = () => {
|
||||
export const useGetCategories = () => {
|
||||
return useQuery({
|
||||
queryKey: ["products"],
|
||||
queryFn: api.getProducts,
|
||||
queryKey: ["categories"],
|
||||
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 {
|
||||
AddTicketType,
|
||||
AttributeResponseType,
|
||||
CategoriesResponseType,
|
||||
MyRequestsResponseType,
|
||||
RequestDetailResponseType,
|
||||
RequestType,
|
||||
@@ -9,8 +10,14 @@ import type {
|
||||
TicketsResponseType,
|
||||
} from "../type/Types";
|
||||
|
||||
export const getProducts = async () => {
|
||||
const { data } = await axios.get<ProductsResponseType>(`/public/products`);
|
||||
export const getCategories = async () => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export type CategoryType = {
|
||||
order: number;
|
||||
parent?: CategoryType;
|
||||
title: string;
|
||||
children?: CategoryType[];
|
||||
};
|
||||
|
||||
export type AttachmentsType = {
|
||||
@@ -60,6 +61,8 @@ export type ProductType = {
|
||||
|
||||
export type ProductsResponseType = BaseResponse<ProductType[]>;
|
||||
|
||||
export type CategoriesResponseType = BaseResponse<CategoryType[]>;
|
||||
|
||||
export type StoreType = {
|
||||
items: RequestType[];
|
||||
setItems: (value: RequestType[]) => void;
|
||||
|
||||
Reference in New Issue
Block a user