diff --git a/src/api/integration.ts b/src/api/integration.ts new file mode 100644 index 00000000..7d59a3f4 --- /dev/null +++ b/src/api/integration.ts @@ -0,0 +1,114 @@ +import { makeRequest } from "@api/makeRequest"; +import { parseAxiosError } from "@utils/parse-error"; + +export type PaginationRequest = { + page: number; + size: number; +}; + +export type Tag = { + ID: number; + AmoID: number; + AccountID: number; + Entity: string; + Name: string; + Color: string; + Deleted: boolean; + CreatedAt: number; +}; + +export type TagsResponse = { + count: number; + items: Tag[]; +}; +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm/amocrm`; + +export const getTags = async ({ + page, + size, +}: PaginationRequest): Promise<[TagsResponse | null, string?]> => { + try { + const tagsResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/tags?page=${page}&size=${size}`, + }); + return [tagsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список тегов. ${error}`]; + } +}; + +export type User = { + ID: number; + AccountID: string; + AmoID: number; + Name: string; + Email: string; + Role: string; + Group: number; + Deleted: boolean; + CreatedAt: number; + Subdomain: string; + Amoiserid: number; + Country: string; +}; + +export type UsersResponse = { + count: number; + items: User[]; +}; + +export const getUsers = async ({ + page, + size, +}: PaginationRequest): Promise<[UsersResponse | null, string?]> => { + try { + const usersResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/users?page=${page}&size=${size}`, + }); + return [usersResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список пользователей. ${error}`]; + } +}; + +export type Step = { + ID: number; + AmoID: number; + PipelineID: number; + AccountID: number; + Name: string; + Color: string; + Deleted: boolean; + CreatedAt: number; +}; + +export type StepsResponse = { + count: number; + items: Step[]; +}; + +export const getSteps = async ({ + page, + size, + pipelineId, +}: PaginationRequest & { pipelineId: number }): Promise< + [StepsResponse | null, string?] +> => { + try { + const stepsResponse = await makeRequest< + PaginationRequest & { pipelineId: number }, + StepsResponse + >({ + method: "GET", + url: `${API_URL}/steps?page=${page}&size=${size}&pipelineID=${pipelineId}`, + }); + return [stepsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список шагов. ${error}`]; + } +}; diff --git a/src/components/CustomRadioGroup/CustomRadioGroup.tsx b/src/components/CustomRadioGroup/CustomRadioGroup.tsx index 3710f25d..f70395c1 100644 --- a/src/components/CustomRadioGroup/CustomRadioGroup.tsx +++ b/src/components/CustomRadioGroup/CustomRadioGroup.tsx @@ -1,33 +1,195 @@ import * as React from "react"; -import { FC } from "react"; +import { FC, useEffect, useMemo, useRef, useState } from "react"; import Radio from "@mui/material/Radio"; import RadioGroup from "@mui/material/RadioGroup"; import FormControlLabel from "@mui/material/FormControlLabel"; import Box from "@mui/material/Box"; import CheckboxIcon from "@icons/Checkbox"; -import { useTheme } from "@mui/material"; +import { Typography, useTheme } from "@mui/material"; +import { + getSteps, + getTags, + PaginationRequest, + Step, + Tag, +} from "@api/integration"; type CustomRadioGroupProps = { - items: string[]; + type?: string; selectedValue: string | null; setSelectedValue: (value: string | null) => void; + pipelineId?: number | null; }; +const SIZE = 25; + export const CustomRadioGroup: FC = ({ - items, + type, selectedValue, setSelectedValue, + pipelineId, }) => { const theme = useTheme(); - const [currentValue, setCurrentValue] = React.useState( + const [currentValue, setCurrentValue] = useState( selectedValue, ); + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [tags, setTags] = useState([]); + const [steps, setSteps] = useState([]); + const [hasMoreItems, setHasMoreItems] = useState(true); + const boxRef = useRef(null); + const handleChange = (event: React.ChangeEvent) => { setSelectedValue((event.target as HTMLInputElement).value); setCurrentValue((event.target as HTMLInputElement).value); }; + + const handleScroll = (e: React.UIEvent) => { + const scrollHeight = e.currentTarget.scrollHeight; + const scrollTop = e.currentTarget.scrollTop; + const clientHeight = e.currentTarget.clientHeight; + const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9; + + if (scrolledToBottom && !isLoading && hasMoreItems) { + setPage((prevPage) => prevPage + 1); + } + }; + + useEffect(() => { + if (type === "typeTags" && hasMoreItems) { + setIsLoading(true); + const pagination: PaginationRequest = { + page: page, + size: SIZE, + }; + getTags(pagination).then(([response]) => { + if (response && response.items !== null) { + setTags((prevItems) => [...prevItems, ...response.items]); + if (response.items.length < SIZE) { + setHasMoreItems(false); + } + } + setIsLoading(false); + }); + } + if (type === "typeSteps" && hasMoreItems && pipelineId) { + setIsLoading(true); + const pagination: PaginationRequest & { pipelineId: number } = { + page: page, + size: SIZE, + pipelineId: pipelineId, + }; + getSteps(pagination).then(([response]) => { + if (response && response.items !== null) { + setSteps((prevItems) => [...prevItems, ...response.items]); + if (response.items.length < SIZE) { + setHasMoreItems(false); + } + } + setIsLoading(false); + }); + } + }, [page, type, hasMoreItems, pipelineId]); + + const formControlLabels = useMemo(() => { + if (type === "typeTags" && tags && tags.length !== 0) { + return tags.map((item) => ( + + } + icon={} + /> + } + // label={item.Name} + label={ + + + {item.Name} + + {item.Entity} + + } + labelPlacement={"start"} + /> + )); + } + if (type === "typeSteps" && steps && steps.length !== 0) { + return steps.map((step) => ( + + } + icon={} + /> + } + label={step.Name} + labelPlacement={"start"} + /> + )); + } + return ( + + Нет элементов + + ); + }, [tags, currentValue, type]); + return ( = ({ value={currentValue} onChange={handleChange} > - {items.map((item) => ( - - } - icon={} - /> - } - label={item} - labelPlacement={"start"} - /> - ))} + {formControlLabels} ); diff --git a/src/components/CustomSelect/CustomSelect.tsx b/src/components/CustomSelect/CustomSelect.tsx index c98c5d55..ce2db1b6 100644 --- a/src/components/CustomSelect/CustomSelect.tsx +++ b/src/components/CustomSelect/CustomSelect.tsx @@ -8,26 +8,34 @@ import { useTheme, } from "@mui/material"; import Box from "@mui/material/Box"; -import { FC, useCallback, useRef, useState } from "react"; +import * as React from "react"; +import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import "./CustomSelect.css"; import arrow_down from "../../assets/icons/arrow_down.svg"; +import { getUsers, PaginationRequest, User } from "@api/integration"; type CustomSelectProps = { - items: string[]; selectedItem: string | null; setSelectedItem: (value: string | null) => void; + type?: string; }; export const CustomSelect: FC = ({ - items, selectedItem, setSelectedItem, + type, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); const [opened, setOpened] = useState(false); const [currentValue, setCurrentValue] = useState(selectedItem); + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [page, setPage] = useState(1); + const [hasMoreItems, setHasMoreItems] = useState(true); + const SIZE = 25; const ref = useRef(null); + console.log("page", page); const onSelectItem = useCallback( (event: SelectChangeEvent) => { @@ -42,6 +50,55 @@ export const CustomSelect: FC = ({ setOpened((isOpened) => !isOpened); }, []); + const handleScroll = (e: React.UIEvent) => { + const scrollHeight = e.currentTarget.scrollHeight; + const scrollTop = e.currentTarget.scrollTop; + const clientHeight = e.currentTarget.clientHeight; + const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9; + + if (scrolledToBottom && !isLoading && hasMoreItems) { + setPage((prevPage) => prevPage + 1); + } + }; + + useEffect(() => { + if (type === "typeUsers" && hasMoreItems) { + setIsLoading(true); + const pagination: PaginationRequest = { + page: page, + size: SIZE, + }; + getUsers(pagination).then(([response]) => { + if (response && response.items !== null) { + setUsers((prevItems) => [...prevItems, ...response.items]); + if (response.items.length < SIZE) { + setHasMoreItems(false); + } + } + setIsLoading(false); + }); + } + }, [page, type, hasMoreItems]); + + const menuItems = useMemo(() => { + if (type === "typeUsers" && users && users.length !== 0) { + return users.map((user) => ( + + {user.Name} + + )); + } + return ( + + нет данных + + ); + }, [users, type]); + return ( = ({ MenuProps={{ disablePortal: true, PaperProps: { + onScroll: handleScroll, style: { zIndex: 2, maxHeight: "300px", @@ -111,18 +169,7 @@ export const CustomSelect: FC = ({ onChange={onSelectItem} onClick={toggleOpened} > - {items.map((item) => { - const uniqueKey = `${item}-${Date.now()}`; - return ( - - {item} - - ); - })} + {menuItems} ); diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx index 7a15289e..dc924892 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal.tsx @@ -115,6 +115,7 @@ export const AmoCRMModal: FC = ({ setSelectedStage={setSelectedStage} performers={performersMock} stages={stagesMock} + pipelineId={selectedFunnel} /> ), }, diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx index edd16fd2..129dd95e 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep2/IntegrationStep2.tsx @@ -41,8 +41,8 @@ export const IntegrationStep2: FC = ({ = ({ }} > diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx index 75d77f10..ecf8bc99 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx @@ -13,6 +13,7 @@ type IntegrationStep3Props = { setSelectedStage: (value: string | null) => void; performers: string[]; stages: string[]; + pipelineId: string | null; }; export const IntegrationStep3: FC = ({ @@ -22,8 +23,7 @@ export const IntegrationStep3: FC = ({ setSelectedStagePerformer, selectedStage, setSelectedStage, - performers, - stages, + pipelineId, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -42,7 +42,7 @@ export const IntegrationStep3: FC = ({ @@ -55,7 +55,8 @@ export const IntegrationStep3: FC = ({ }} > diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx index 88009b2e..c50cecfb 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx @@ -35,8 +35,8 @@ export const IntegrationStep4: FC = ({ void; @@ -17,6 +18,7 @@ export const ItemsSelectionView: FC = ({ setSelectedValue, onLargeBtnClick, onSmallBtnClick, + type, }) => { return ( = ({ }} > = ({ items={items} selectedValue={selectedValue} setSelectedValue={setSelectedValue} + type={"typeTags"} onSmallBtnClick={() => { setActiveItem(null); setIsSelection(false);