This commit is contained in:
2026-07-07 18:57:34 +03:30
parent 8cab9d04c8
commit adad81467c
8 changed files with 225 additions and 47 deletions
+3 -3
View File
@@ -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
View File
@@ -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>
+6
View File
@@ -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: 'تعداد',
+168 -11
View File
@@ -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>
)
}
+14 -22
View File
@@ -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
+14 -3
View File
@@ -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,
});
};
+9 -2
View File
@@ -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;
};
+3
View File
@@ -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;