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_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
View File
@@ -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>
+6
View File
@@ -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: 'تعداد',
+168 -11
View File
@@ -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>
) )
} }
+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 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
+14 -3
View File
@@ -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,
}); });
}; };
+9 -2
View File
@@ -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;
}; };
+3
View File
@@ -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;