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 { 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;
|
|
||||||
|
|||||||
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()}
|
value={getDepartmentValue()}
|
||||||
onChange={setDepartmentValue}
|
onChange={setDepartmentValue}
|
||||||
label={"Родительский департамент/отдел"}
|
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 { 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()}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user