feat: searchable product select and department select

This commit is contained in:
2025-01-31 20:18:01 +04:00
parent 52619f119f
commit 0a4e20e888
7 changed files with 126 additions and 156 deletions

View File

@@ -2,21 +2,10 @@ import { ProductSchema } from "../../client";
import { FC, useState } from "react";
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
import { omit } from "lodash";
import ObjectSelect, {
ObjectSelectProps,
} from "../ObjectSelect/ObjectSelect.tsx";
import {
ComboboxItem,
Image,
Loader,
OptionsFilter,
rem,
SelectProps,
Text,
Tooltip,
} from "@mantine/core";
import { getProductFields } from "../../types/utils.ts";
import ObjectSelect, { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
import { Loader, OptionsFilter } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import getRenderOptions from "./utils/getRenderOptions.tsx";
type RestProps = {
clientId: number;
@@ -33,68 +22,8 @@ const ProductSelect: FC<Props> = (props: Props) => {
itemsPerPage: MAX_PRODUCTS,
});
const restProps = omit(props, ["clientId"]);
const renderOption: SelectProps["renderOption"] = item => {
const product = products.find(
product => product.id == parseInt(item.option.value)
);
if (!product) return item.option.label;
const productFields = getProductFields(product);
const imageUrl =
product.images && product.images[0]
? product.images[0].imageUrl
: undefined;
return (
<Tooltip
style={{ whiteSpace: "pre-line" }}
multiline
disabled={productFields.length === 0}
label={
<>
{productFields
.map(([key, value]) => {
return `${key.toString()}: ${value.toString()}`;
})
.join("\n")}
{imageUrl && (
<Image
src={imageUrl}
alt={product.name}
maw={rem(250)}
/>
)}
</>
}>
<div>
{product.name}
<br />
{product.barcodes && (
<Text size={"xs"}>{product.barcodes[0]}</Text>
)}
</div>
</Tooltip>
);
};
const optionsFilter: OptionsFilter = ({ options, search }) => {
return options;
const filtered = (options as ComboboxItem[]).filter(option => {
const product = products.find(
product => product.id == parseInt(option.value)
);
if (!product) return true;
return (
product.name.toLowerCase().includes(search.toLowerCase()) ||
product.barcodes.some(value =>
value.toLowerCase().includes(search.toLowerCase())
) ||
product.article?.toLowerCase() === search.toLowerCase()
);
});
filtered.sort((a, b) => a.label.localeCompare(b.label));
return filtered.length > MAX_PRODUCTS
? filtered.slice(0, MAX_PRODUCTS)
: filtered;
};
const optionsFilter: OptionsFilter = ({ options }) => options;
const setSearchValueImpl = (value: string) => {
const names = products.map(product => product.name);
if (names.includes(value)) return;
@@ -103,14 +32,13 @@ const ProductSelect: FC<Props> = (props: Props) => {
return (
<ObjectSelect
// disabled={isLoading}
rightSection={
isLoading || searchValue !== debounced ? (
<Loader size={"sm"} />
) : null
}
onSearchChange={setSearchValueImpl}
renderOption={renderOption}
renderOption={getRenderOptions(products)}
searchable
{...restProps}
data={products}
@@ -118,58 +46,5 @@ const ProductSelect: FC<Props> = (props: Props) => {
/>
);
};
export default ProductSelect;
// type ControlledValueProps = {
// value: ProductSchema;
// onChange: (value: ProductSchema) => void;
// }
// type RestProps = {
// defaultValue?: ProductSchema;
// onChange: (value: ProductSchema) => void;
// clientId: number;
// }
// type Props = (RestProps & Partial<ControlledValueProps>) & Omit<SelectProps, 'value' | 'onChange'>;
//
// const ProductSelect: FC<Props> = (props) => {
// const isControlled = 'value' in props;
// const [intertalValue, setInternalValue] = useState<ProductSchema | undefined>(props.defaultValue);
// const value = isControlled ? props.value : intertalValue
//
// const {products} = useProductsList({clientId: props.clientId});
//
//
// const data = useMemo(() => products.reduce((acc, product) => {
// acc.push({
// label: product.name,
// value: product.id.toString()
// });
// return acc;
// }, [] as { label: string, value: string }[]), [products]);
//
// const handleOnChange = (event: string | null) => {
// if (!event) return;
// const product = products.find(product => parseInt(event) == product.id);
// if (!product) return;
// if (isControlled) {
// props.onChange(product);
// return;
// }
// setInternalValue(product);
// }
// useEffect(() => {
// if (isControlled || !intertalValue) return;
// props.onChange(intertalValue);
// }, [intertalValue]);
// const restProps = omit(props, ['clientId'])
// return (
// <Select
// {...restProps}
// withCheckIcon={false}
// searchable
// value={value?.id.toString()}
// onChange={handleOnChange}
// data={data}
// />
// )
// }
// export default ProductSelect;

View File

@@ -0,0 +1,50 @@
import { ProductSchema } from "../../../client";
import { ComboboxItem, ComboboxLikeRenderOptionInput, Image, rem, SelectProps, Text, Tooltip } from "@mantine/core";
import { getProductFields } from "../../../types/utils.ts";
const getRenderOptions = (products: ProductSchema[]): SelectProps["renderOption"] => {
return (item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
const product = products.find(
product => product.id == parseInt(item.option.value),
);
if (!product) return item.option.label;
const productFields = getProductFields(product);
const imageUrl =
product.images && product.images[0]
? product.images[0].imageUrl
: undefined;
return (
<Tooltip
style={{ whiteSpace: "pre-line" }}
multiline
disabled={productFields.length === 0}
label={
<>
{productFields
.map(([key, value]) => {
return `${key.toString()}: ${value.toString()}`;
})
.join("\n")}
{imageUrl && (
<Image
src={imageUrl}
alt={product.name}
maw={rem(250)}
/>
)}
</>
}>
<div>
{product.name}
<br />
{product.barcodes && (
<Text size={"xs"}>{product.barcodes[0]}</Text>
)}
</div>
</Tooltip>
);
};
};
export default getRenderOptions;

View File

@@ -73,6 +73,7 @@ const DepartmentSelect = ({
value={getDepartmentValue()}
onChange={setDepartmentValue}
label={"Родительский департамент/отдел"}
searchable
/>
);
};

View File

@@ -0,0 +1,40 @@
import { ProductSchema } from "../../../../../client";
import { FC, useState } from "react";
import ObjectSelect, { ObjectSelectProps } from "../../../../../components/ObjectSelect/ObjectSelect.tsx";
import getRenderOptions from "../../../../../components/ProductSelect/utils/getRenderOptions.tsx";
type Props = Omit<ObjectSelectProps<ProductSchema>, "searchValue" | "onSearchChange">;
const ShippingProductSelect: FC<Props> = (props: Props) => {
const [search, setSearch] = useState("");
const getFilteredData = () => {
const searchValue: string = search.toLowerCase();
const data: ProductSchema[] = props.data.filter(product => {
return (
product.name.toLowerCase().includes(searchValue) ||
product.article?.toLowerCase().includes(searchValue) ||
product.barcodes && product.barcodes[0].toLowerCase().includes(searchValue)
);
});
return data;
};
return (
<ObjectSelect
label={"Товар"}
placeholder={"Выберите товар"}
searchable
{...props}
data={getFilteredData()}
searchValue={search}
onSearchChange={setSearch}
renderOption={getRenderOptions(props.data)}
filter={({ options }) => options}
/>
);
};
export default ShippingProductSelect;

View File

@@ -1,6 +1,6 @@
import { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals";
import { Button, Flex, NumberInput, rem, Select, Text } from "@mantine/core";
import { Button, Flex, NumberInput, rem, Text } from "@mantine/core";
import getRestProducts from "../utils/getRestProducts.tsx";
import {
CreateBoxInDealSchema,
@@ -8,14 +8,15 @@ import {
CreateShippingProductSchema,
DealProductSchema,
DealSchema,
ProductSchema,
ShippingService,
UpdateBoxSchema,
UpdateShippingProductSchema,
} from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import { ShippingData, ShippingModalForm, ShippingProductOption } from "../types/ShippingProductData.tsx";
import { ShippingData, ShippingModalForm } from "../types/ShippingProductData.tsx";
import { useEffect, useState } from "react";
import ShippingProductSelect from "../components/ShippingProductSelect.tsx";
type Props = {
@@ -31,11 +32,11 @@ const ShippingProductModal = ({
innerProps,
}: ContextModalProps<Props>) => {
const [restProducts, setRestProducts] = useState<Map<number, DealProductSchema>>(new Map());
const [restProductsSelectData, setRestProductSelectData] = useState<ShippingProductOption[]>([]);
const [restProductsSelectData, setRestProductSelectData] = useState<ProductSchema[]>([]);
const getRestProductQuantity = () => {
if (form.values.productId) {
const restProduct = restProducts.get(Number(form.values.productId));
if (form.values.product) {
const restProduct = restProducts.get(form.values.product.id);
if (restProduct) {
return restProduct.quantity;
}
@@ -43,14 +44,19 @@ const ShippingProductModal = ({
return 10000;
};
const findProductById = (productId?: number | null) => {
const dealProduct = innerProps.deal.products.find(p => p.product.id === productId);
return dealProduct ? dealProduct.product : null;
};
const initialValues: ShippingModalForm = {
quantity: innerProps.shippingData.quantity ?? 0,
productId: innerProps.shippingData.productId,
product: findProductById(innerProps.shippingData.productId),
};
const form = useForm<ShippingModalForm>({
initialValues,
validate: {
productId: productId => !productId && "Необходимо выбрать товар",
product: product => !product && "Необходимо выбрать товар",
quantity: quantity => quantity > getRestProductQuantity() ? "Слишком много товара" :
quantity === 0 && "Слишком мало товара",
},
@@ -69,6 +75,7 @@ const ShippingProductModal = ({
const data = {
...innerProps.shippingData,
...form.values,
productId: form.values.product?.id,
} as CreateBoxInPalletSchema | CreateBoxInDealSchema | UpdateBoxSchema;
ShippingService.updateBox({
@@ -86,6 +93,7 @@ const ShippingProductModal = ({
const data = {
...innerProps.shippingData,
...form.values,
productId: form.values.product?.id,
} as CreateShippingProductSchema | UpdateShippingProductSchema;
ShippingService.updateShippingProduct({
@@ -108,7 +116,7 @@ const ShippingProductModal = ({
};
const getRestQuantityText = () => {
if (!form.values.productId) return;
if (!form.values.product) return;
const restQuantityInDeal = getRestProductQuantity();
const restQuantity = restQuantityInDeal - form.values.quantity;
@@ -125,12 +133,9 @@ const ShippingProductModal = ({
direction={"column"}
gap={rem(10)}
>
<Select
<ShippingProductSelect
{...form.getInputProps("product")}
data={restProductsSelectData}
label={"Товар"}
placeholder={"Выберите товар"}
{...form.getInputProps("productId")}
value={form.values.productId?.toString()}
/>
{getRestQuantityText()}

View File

@@ -1,13 +1,13 @@
import { CreateShippingProductSchema, UpdateBoxSchema, UpdateShippingProductSchema } from "../../../../../client";
import {
CreateShippingProductSchema,
ProductSchema,
UpdateBoxSchema,
UpdateShippingProductSchema,
} from "../../../../../client";
export type ShippingModalForm = {
quantity: number;
productId?: number | null;
}
export type ShippingProductOption = {
value: string;
label: string;
product?: ProductSchema | null;
}
export type ShippingData =

View File

@@ -1,4 +1,3 @@
import { ShippingProductOption } from "../types/ShippingProductData.tsx";
import { DealProductSchema, DealSchema, ProductSchema } from "../../../../../client";
type UnaccountedValues = {
@@ -61,11 +60,11 @@ const getRestProducts = ({
}
});
const restProductsSelectData: ShippingProductOption[] = [];
const restProductsSelectData: ProductSchema[] = [];
restProducts.forEach(
(restProduct, id) => {
restProductsSelectData.push({ value: String(id), label: restProduct.product.name });
(restProduct) => {
restProductsSelectData.push(restProduct.product);
},
);