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 { FC, useState } from "react";
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx"; import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
import { omit } from "lodash"; import { omit } from "lodash";
import ObjectSelect, { import ObjectSelect, { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
ObjectSelectProps, import { Loader, OptionsFilter } from "@mantine/core";
} from "../ObjectSelect/ObjectSelect.tsx";
import {
ComboboxItem,
Image,
Loader,
OptionsFilter,
rem,
SelectProps,
Text,
Tooltip,
} from "@mantine/core";
import { getProductFields } from "../../types/utils.ts";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import getRenderOptions from "./utils/getRenderOptions.tsx";
type RestProps = { type RestProps = {
clientId: number; clientId: number;
@@ -33,68 +22,8 @@ const ProductSelect: FC<Props> = (props: Props) => {
itemsPerPage: MAX_PRODUCTS, itemsPerPage: MAX_PRODUCTS,
}); });
const restProps = omit(props, ["clientId"]); 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 ( const optionsFilter: OptionsFilter = ({ options }) => options;
<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 setSearchValueImpl = (value: string) => { const setSearchValueImpl = (value: string) => {
const names = products.map(product => product.name); const names = products.map(product => product.name);
if (names.includes(value)) return; if (names.includes(value)) return;
@@ -103,14 +32,13 @@ const ProductSelect: FC<Props> = (props: Props) => {
return ( return (
<ObjectSelect <ObjectSelect
// disabled={isLoading}
rightSection={ rightSection={
isLoading || searchValue !== debounced ? ( isLoading || searchValue !== debounced ? (
<Loader size={"sm"} /> <Loader size={"sm"} />
) : null ) : null
} }
onSearchChange={setSearchValueImpl} onSearchChange={setSearchValueImpl}
renderOption={renderOption} renderOption={getRenderOptions(products)}
searchable searchable
{...restProps} {...restProps}
data={products} data={products}
@@ -118,58 +46,5 @@ const ProductSelect: FC<Props> = (props: Props) => {
/> />
); );
}; };
export default ProductSelect; 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()} value={getDepartmentValue()}
onChange={setDepartmentValue} onChange={setDepartmentValue}
label={"Родительский департамент/отдел"} 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 { useForm } from "@mantine/form";
import { ContextModalProps } from "@mantine/modals"; 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 getRestProducts from "../utils/getRestProducts.tsx";
import { import {
CreateBoxInDealSchema, CreateBoxInDealSchema,
@@ -8,14 +8,15 @@ import {
CreateShippingProductSchema, CreateShippingProductSchema,
DealProductSchema, DealProductSchema,
DealSchema, DealSchema,
ProductSchema,
ShippingService, ShippingService,
UpdateBoxSchema, UpdateBoxSchema,
UpdateShippingProductSchema, UpdateShippingProductSchema,
} from "../../../../../client"; } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts"; 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 { useEffect, useState } from "react";
import ShippingProductSelect from "../components/ShippingProductSelect.tsx";
type Props = { type Props = {
@@ -31,11 +32,11 @@ const ShippingProductModal = ({
innerProps, innerProps,
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const [restProducts, setRestProducts] = useState<Map<number, DealProductSchema>>(new Map()); const [restProducts, setRestProducts] = useState<Map<number, DealProductSchema>>(new Map());
const [restProductsSelectData, setRestProductSelectData] = useState<ShippingProductOption[]>([]); const [restProductsSelectData, setRestProductSelectData] = useState<ProductSchema[]>([]);
const getRestProductQuantity = () => { const getRestProductQuantity = () => {
if (form.values.productId) { if (form.values.product) {
const restProduct = restProducts.get(Number(form.values.productId)); const restProduct = restProducts.get(form.values.product.id);
if (restProduct) { if (restProduct) {
return restProduct.quantity; return restProduct.quantity;
} }
@@ -43,14 +44,19 @@ const ShippingProductModal = ({
return 10000; 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 = { const initialValues: ShippingModalForm = {
quantity: innerProps.shippingData.quantity ?? 0, quantity: innerProps.shippingData.quantity ?? 0,
productId: innerProps.shippingData.productId, product: findProductById(innerProps.shippingData.productId),
}; };
const form = useForm<ShippingModalForm>({ const form = useForm<ShippingModalForm>({
initialValues, initialValues,
validate: { validate: {
productId: productId => !productId && "Необходимо выбрать товар", product: product => !product && "Необходимо выбрать товар",
quantity: quantity => quantity > getRestProductQuantity() ? "Слишком много товара" : quantity: quantity => quantity > getRestProductQuantity() ? "Слишком много товара" :
quantity === 0 && "Слишком мало товара", quantity === 0 && "Слишком мало товара",
}, },
@@ -69,6 +75,7 @@ const ShippingProductModal = ({
const data = { const data = {
...innerProps.shippingData, ...innerProps.shippingData,
...form.values, ...form.values,
productId: form.values.product?.id,
} as CreateBoxInPalletSchema | CreateBoxInDealSchema | UpdateBoxSchema; } as CreateBoxInPalletSchema | CreateBoxInDealSchema | UpdateBoxSchema;
ShippingService.updateBox({ ShippingService.updateBox({
@@ -86,6 +93,7 @@ const ShippingProductModal = ({
const data = { const data = {
...innerProps.shippingData, ...innerProps.shippingData,
...form.values, ...form.values,
productId: form.values.product?.id,
} as CreateShippingProductSchema | UpdateShippingProductSchema; } as CreateShippingProductSchema | UpdateShippingProductSchema;
ShippingService.updateShippingProduct({ ShippingService.updateShippingProduct({
@@ -108,7 +116,7 @@ const ShippingProductModal = ({
}; };
const getRestQuantityText = () => { const getRestQuantityText = () => {
if (!form.values.productId) return; if (!form.values.product) return;
const restQuantityInDeal = getRestProductQuantity(); const restQuantityInDeal = getRestProductQuantity();
const restQuantity = restQuantityInDeal - form.values.quantity; const restQuantity = restQuantityInDeal - form.values.quantity;
@@ -125,12 +133,9 @@ const ShippingProductModal = ({
direction={"column"} direction={"column"}
gap={rem(10)} gap={rem(10)}
> >
<Select <ShippingProductSelect
{...form.getInputProps("product")}
data={restProductsSelectData} data={restProductsSelectData}
label={"Товар"}
placeholder={"Выберите товар"}
{...form.getInputProps("productId")}
value={form.values.productId?.toString()}
/> />
{getRestQuantityText()} {getRestQuantityText()}

View File

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

View File

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