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