diff --git a/src/client/models/DealAddProductRequest.ts b/src/client/models/DealAddProductRequest.ts index a338f85..a32be09 100644 --- a/src/client/models/DealAddProductRequest.ts +++ b/src/client/models/DealAddProductRequest.ts @@ -2,9 +2,9 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { DealProductSchema } from './DealProductSchema'; export type DealAddProductRequest = { dealId: number; - productId: number; - quantity: number; + product: DealProductSchema; }; diff --git a/src/client/models/DealProductServiceSchema.ts b/src/client/models/DealProductServiceSchema.ts index 4b0e4eb..a0b144b 100644 --- a/src/client/models/DealProductServiceSchema.ts +++ b/src/client/models/DealProductServiceSchema.ts @@ -5,7 +5,6 @@ import type { ServiceSchema } from './ServiceSchema'; export type DealProductServiceSchema = { service: ServiceSchema; - quantity: number; price: number; }; diff --git a/src/client/models/ProductCreateRequest.ts b/src/client/models/ProductCreateRequest.ts index f188405..44d05c5 100644 --- a/src/client/models/ProductCreateRequest.ts +++ b/src/client/models/ProductCreateRequest.ts @@ -9,5 +9,10 @@ export type ProductCreateRequest = { clientId: number; barcodes: Array; barcodeTemplate?: (BarcodeTemplateSchema | null); + brand?: (string | null); + color?: (string | null); + composition?: (string | null); + size?: (string | null); + additionalInfo?: (string | null); }; diff --git a/src/client/models/ProductSchema.ts b/src/client/models/ProductSchema.ts index 27107d9..b40e116 100644 --- a/src/client/models/ProductSchema.ts +++ b/src/client/models/ProductSchema.ts @@ -4,7 +4,6 @@ /* eslint-disable */ import type { BarcodeTemplateSchema } from './BarcodeTemplateSchema'; export type ProductSchema = { - id: number; name: string; article: string; clientId: number; @@ -15,5 +14,6 @@ export type ProductSchema = { composition?: (string | null); size?: (string | null); additionalInfo?: (string | null); + id: number; }; diff --git a/src/components/ObjectSelect/ObjectSelect.tsx b/src/components/ObjectSelect/ObjectSelect.tsx index db99248..bd5aaca 100644 --- a/src/components/ObjectSelect/ObjectSelect.tsx +++ b/src/components/ObjectSelect/ObjectSelect.tsx @@ -1,6 +1,7 @@ import {Select, SelectProps} from "@mantine/core"; import {useEffect, useMemo, useState} from "react"; import {ObjectWithNameAndId} from "../../types/utils.ts"; +import {groupBy} from "lodash"; export type SelectObjectType = T; @@ -14,6 +15,7 @@ type RestProps = { defaultValue?: SelectObjectType onChange: (value: SelectObjectType) => void; data: SelectObjectType[]; + groupBy?: (item: SelectObjectType) => string; } export type ObjectSelectProps = @@ -27,14 +29,23 @@ const ObjectSelect = (props: ObjectSelectProps< const value = isControlled ? props.value : internalValue; - const data = useMemo(() => props.data.reduce((acc, item) => { - acc.push({ - label: item.name, - value: item.id.toString() - }); - return acc; - }, [] as { label: string, value: string }[]), [props.data]); - + const data = useMemo(() => { + if (props.groupBy) { + const groupedData = groupBy(props.data, props.groupBy); + return Object.entries(groupedData).map(([group, items]) => ({ + group, + items: items.map(item => ({ + label: item.name, + value: item.id.toString() + })) + })); + } else { + return props.data.map(item => ({ + label: item.name, + value: item.id.toString() + })); + } + }, [props.data, props.groupBy]); const handleOnChange = (event: string | null) => { if (!event) return; diff --git a/src/components/ProductSelect/ProductSelect.tsx b/src/components/ProductSelect/ProductSelect.tsx index d7df780..f17953f 100644 --- a/src/components/ProductSelect/ProductSelect.tsx +++ b/src/components/ProductSelect/ProductSelect.tsx @@ -2,57 +2,75 @@ import {ProductSchema} from "../../client"; import {Select, SelectProps} from "@mantine/core"; import {FC, useEffect, useMemo, useState} from "react"; import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx"; +import {omit} from "lodash"; +import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx"; -type ControlledValueProps = { - value: ProductSchema; - onChange: (value: ProductSchema) => void; -} type RestProps = { - defaultValue?: ProductSchema; - onChange: (value: ProductSchema) => void; clientId: number; } -type Props = (RestProps & Partial) & Omit; - -const ProductSelect: FC = (props) => { - const isControlled = 'value' in props; - const [intertalValue, setInternalValue] = useState(props.defaultValue); - const value = isControlled ? props.value : intertalValue - +type Props = Omit, 'data'> & RestProps; +const ProductSelect: FC = (props: Props) => { 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 ( - +// ) +// } +// export default ProductSelect; \ No newline at end of file diff --git a/src/components/Selects/ServiceSelectNew/ServiceSelectNew.tsx b/src/components/Selects/ServiceSelectNew/ServiceSelectNew.tsx index a8807c1..0704b0f 100644 --- a/src/components/Selects/ServiceSelectNew/ServiceSelectNew.tsx +++ b/src/components/Selects/ServiceSelectNew/ServiceSelectNew.tsx @@ -13,12 +13,14 @@ type Props = Omit, 'data'> & RestProps; const ServiceSelectNew: FC = (props: Props) => { const {services} = useServicesList(); const data = props.filterType ? services.filter(service => service.serviceType === props.filterType) : services; + const restProps = omit(props, ['filterType']); return ( item.category.name} /> ) } diff --git a/src/components/ServiceWithPriceInput/ServiceWithPriceInput.tsx b/src/components/ServiceWithPriceInput/ServiceWithPriceInput.tsx new file mode 100644 index 0000000..42c8db4 --- /dev/null +++ b/src/components/ServiceWithPriceInput/ServiceWithPriceInput.tsx @@ -0,0 +1,73 @@ +import {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx"; +import {ServiceSchema} from "../../client"; +import {Flex, FlexProps, NumberInput, NumberInputProps, rem} from "@mantine/core"; +import {FC, useEffect, useState} from "react"; +import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx"; + +type ServiceProps = Omit, 'data'>; +type PriceProps = NumberInputProps; + +type Props = { + serviceProps: ServiceProps, + priceProps: PriceProps + quantity: number; + containerProps: FlexProps +} +const ServiceWithPriceInput: FC = ({serviceProps, priceProps, quantity, containerProps}) => { + const [price, setPrice] = useState( + typeof priceProps.value === 'number' ? priceProps.value : undefined); + const [service, setService] = useState(serviceProps.value); + + const setPriceBasedOnQuantity = (): boolean => { + if (!service || !service.priceRanges.length) return false; + const range = service.priceRanges.find(priceRange => + quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0]; + setPrice(range.price); + return true; + } + const setPriceBasedOnService = () => { + if (!service) return; + if (setPriceBasedOnQuantity()) return; + setPrice(service.price); + } + const onServiceManualChange = (service: ServiceSchema) => { + setService(service); + } + const onPriceManualChange = (value: number | string) => { + if (typeof value !== 'number') return; + setPrice(value); + } + useEffect(() => { + setPriceBasedOnQuantity(); + }, [quantity]); + + useEffect(() => { + if (!priceProps.onChange || !price) return; + priceProps.onChange(price); + }, [price]); + + useEffect(() => { + if (!serviceProps.onChange || !service) return; + setPriceBasedOnService(); + serviceProps.onChange(service); + }, [service]); + return ( + + + + + ) +} + +export default ServiceWithPriceInput; \ No newline at end of file diff --git a/src/pages/LeadsPage/components/DealProductsTable/DealProductServiceTable.tsx b/src/pages/LeadsPage/components/DealProductsTable/DealProductServiceTable.tsx index 104d105..bec6c68 100644 --- a/src/pages/LeadsPage/components/DealProductsTable/DealProductServiceTable.tsx +++ b/src/pages/LeadsPage/components/DealProductsTable/DealProductServiceTable.tsx @@ -1,30 +1,73 @@ -import {Checkbox, Flex, NumberInput} from "@mantine/core"; -import ServiceSelectNew from "../../../../components/Selects/ServiceSelectNew/ServiceSelectNew.tsx"; -import {ServiceType} from "../../../../shared/enums/ServiceType.ts"; -import {useForm} from "@mantine/form"; -import {DealProductServiceSchema} from "../../../../client"; +import {ActionIcon, Button, Flex, Input, rem} from "@mantine/core"; +import {BaseFormInputProps} from "../../../../types/utils.ts"; +import {DealProductServiceSchema, ServiceSchema} from "../../../../client"; +import {FC, useEffect, useState} from "react"; +import ServiceWithPriceInput from "../../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx"; +import {isNumber} from "lodash"; +import {notifications} from "../../../../shared/lib/notifications.ts"; +import {IconTrash} from "@tabler/icons-react"; -const DealProductServiceTable = () => { - const initialValues: Partial = {} - const form = useForm>( - ); +type RestProps = { + quantity: number; +} +type Props = BaseFormInputProps & RestProps; +const DealProductServiceTable: FC = (props: Props) => { + const {value, onChange, quantity, error} = props; + const [innerValue, setInnerValue] = useState[]>(value || []); + const onServiceChange = (idx: number, value: ServiceSchema) => { + setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, service: value} : item)); + } + const onQuantityChange = (idx: number, value: string | number) => { + if (!isNumber(value)) return; + setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, price: value} : item)); + } + const onCreate = () => { + if (innerValue.length > 0 && !innerValue.at(-1)?.service) { + notifications.error({message: "Заполните последнюю услугу"}) + return; + } + setInnerValue(prevState => [...prevState, {service: undefined, quantity: 1}]) + } + const onDelete = (idx: number) => { + setInnerValue(oldValue => oldValue.filter((_, i) => i !== idx)); + + } + useEffect(() => { + onChange(innerValue as DealProductServiceSchema[]); + }, [innerValue]); return ( - - { - }}/> - - - + + + {innerValue.map((service, idx) => ( + + onDelete(idx)} variant={"default"}> + + + onServiceChange(idx, event), + value: service.service, + placeholder: "Выберите услугу", + style: {width: "100%"} + }} + priceProps={{ + onChange: (event) => onQuantityChange(idx, event), + value: service.price, + placeholder: "Введите стоимость", + hideControls: true, + style: {width: "100%"}, + suffix: "₽" - + }} + containerProps={{w: "100%"}} + quantity={quantity} + /> + + + ))} + + + ) } export default DealProductServiceTable; \ No newline at end of file diff --git a/src/pages/LeadsPage/components/DealProductsTable/DealProductsTable.tsx b/src/pages/LeadsPage/components/DealProductsTable/DealProductsTable.tsx index 4fe8c68..bfd0a1f 100644 --- a/src/pages/LeadsPage/components/DealProductsTable/DealProductsTable.tsx +++ b/src/pages/LeadsPage/components/DealProductsTable/DealProductsTable.tsx @@ -6,7 +6,7 @@ import {DealProductSchema, ProductService} from "../../../../client"; import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core"; import {MRT_TableOptions} from "mantine-react-table"; import {modals} from "@mantine/modals"; -import {IconBarcode, IconTrash} from "@tabler/icons-react"; +import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react"; import {notifications} from "../../../../shared/lib/notifications.ts"; import {CreateProductRequest} from "../../../ProductsPage/types.ts"; @@ -39,7 +39,8 @@ const DealProductsTable: FC = (props: Props) => { innerProps: { onCreate: (product) => onCreate(product as DealProductSchema), clientId - } + }, + size:"lg" }) } const onPrintBarcodeClick = (product: DealProductSchema) => { @@ -71,6 +72,21 @@ const DealProductsTable: FC = (props: Props) => { } }) } + const onEditClick = (product: DealProductSchema) => { + modals.openContextModal({ + modal: "addDealProduct", + title: 'Создание товара', + withCloseButton: false, + innerProps: { + clientId: clientId, + element: product, + onChange: () => { + } + }, + size:"lg" + + }) + } return ( = (props: Props) => { + + onEditClick(row.original)} variant={"default"}> + + + ), renderBottomToolbar: ({table}) => ( diff --git a/src/pages/LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx b/src/pages/LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx index 01629f2..a53279f 100644 --- a/src/pages/LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx +++ b/src/pages/LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx @@ -214,8 +214,7 @@ const useDealProductTableState = () => { DealService.addDealProduct({ requestBody: { dealId: selectedDeal.id, - productId: product.product.id, - quantity: product.quantity + product: product } }).then(async ({ok, message}) => { if (!ok) { diff --git a/src/pages/LeadsPage/modals/AddDealProductModal.tsx b/src/pages/LeadsPage/modals/AddDealProductModal.tsx index 52898a6..5726e89 100644 --- a/src/pages/LeadsPage/modals/AddDealProductModal.tsx +++ b/src/pages/LeadsPage/modals/AddDealProductModal.tsx @@ -1,37 +1,53 @@ import {ContextModalProps} from "@mantine/modals"; import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx"; -import {DealProductSchema} from "../../../client"; +import {DealProductSchema, DealProductServiceSchema} from "../../../client"; import {useForm} from "@mantine/form"; -import {NumberInput} from "@mantine/core"; +import {Fieldset, NumberInput} from "@mantine/core"; import ProductSelect from "../../../components/ProductSelect/ProductSelect.tsx"; +import DealProductServiceTable from "../components/DealProductsTable/DealProductServiceTable.tsx"; +import {omit} from "lodash"; +import {BaseFormInputProps} from "../../../types/utils.ts"; type RestProps = { clientId: number } -type Props = CreateEditFormProps> & RestProps; +type Props = CreateEditFormProps & RestProps; const AddDealProductModal = ({ context, id, innerProps }: ContextModalProps) => { + const isEditing = 'element' in innerProps; + const restProps = omit(innerProps, ['clientId']); + + const validateServices = (services?: DealProductServiceSchema[]) => { + if (!services || services.length == 0) return null; + console.log("validating..."); + console.log( services.filter(service => service.service === undefined)) + return services.find(service => service.service === undefined) ? "Удалите пустые услуги" : null; + } const form = useForm>({ - initialValues: { + initialValues: isEditing ? innerProps.element : { product: undefined, - quantity: 0 + services: [], + quantity: 1 }, validate: { product: (product?: DealProductSchema['product']) => product !== undefined ? null : "Необходимо выбрать товар", - quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0" + quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0", + services: validateServices } }); + const onClose = () => { context.closeContextModal(id); } - + console.log(form.values) return ( + } form={form} closeOnSubmit onClose={onClose}> @@ -49,10 +65,19 @@ const AddDealProductModal = ({ min={1} {...form.getInputProps('quantity')} /> +
+ } + /> +
+
+ ) } diff --git a/src/routes/test.lazy.tsx b/src/routes/test.lazy.tsx index bbaedad..2fd17a4 100644 --- a/src/routes/test.lazy.tsx +++ b/src/routes/test.lazy.tsx @@ -1,6 +1,12 @@ import {createLazyFileRoute} from "@tanstack/react-router"; import ServiceSelectNew from "../components/Selects/ServiceSelectNew/ServiceSelectNew.tsx"; import {ServiceType} from "../shared/enums/ServiceType.ts"; +import ServiceWithPriceInput from "../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx"; +import {useEffect, useState} from "react"; +import {ServiceSchema, ServiceService} from "../client"; +import {NumberInput} from "@mantine/core"; +import {isNumber} from "lodash"; +import useServicesList from "../pages/ServicesPage/hooks/useServicesList.tsx"; export const Route = createLazyFileRoute('/test')({ component: TestPage @@ -18,12 +24,48 @@ const data = [{ ] function TestPage() { + const [service, setService] = useState({ + "id": 96, + "name": "123", + "category": {"id": 1, "name": "Услуги по работе с товаром с учетом суммы всех сторон в см."}, + "price": 0, + "serviceType": 1, + "priceRanges": [{"id": 4, "fromQuantity": 1, "toQuantity": 200, "price": 35}, { + "id": 3, + "fromQuantity": 201, + "toQuantity": 300, + "price": 24 + }] + }); + const [price, setPrice] = useState(); + const [q, setQ] = useState(1); + + + console.log('service:---------'); + console.log(service); + console.log('price:---------'); + console.log(price); return ( <> - { + { + if (!isNumber(event)) return; + setQ(event); + }} + /> +