feat: searchable product select and department select
This commit is contained in:
		@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								src/components/ProductSelect/utils/getRenderOptions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/components/ProductSelect/utils/getRenderOptions.tsx
									
									
									
									
									
										Normal 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;
 | 
			
		||||
@@ -73,6 +73,7 @@ const DepartmentSelect = ({
 | 
			
		||||
            value={getDepartmentValue()}
 | 
			
		||||
            onChange={setDepartmentValue}
 | 
			
		||||
            label={"Родительский департамент/отдел"}
 | 
			
		||||
            searchable
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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()}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 =
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user