Merge branch 'amo' into dev

This commit is contained in:
Nastya 2024-08-15 13:43:48 +03:00
commit 3a224b897e
41 changed files with 1635 additions and 1289 deletions

@ -1,5 +1,5 @@
import { Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import AmoLogo from "../../assets/icons/Amologo.png";
import AmoLogo from "@/assets/icons/Amologo.png";
import { FC } from "react";
type AmoButtonProps = {

@ -3,6 +3,8 @@ import { FC, useMemo } from "react";
import CheckboxIcon from "@icons/Checkbox";
import { SelectChangeEvent, Typography, useTheme, Box, FormControlLabel, RadioGroup, Radio, useMediaQuery } from "@mui/material";
import { MinifiedData, TagKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
import { determineScrollBottom } from "@/utils/throttle";
import { throttle } from "@frontend/kitui";
type CustomRadioGroupProps = {
items: MinifiedData[] | [];
@ -34,17 +36,6 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
return item.entity === activeScope;
}) : items
const onScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => {
const scrollHeight = e.currentTarget.scrollHeight;
const scrollTop = e.currentTarget.scrollTop;
const clientHeight = e.currentTarget.clientHeight;
const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9;
if (scrolledToBottom) {
handleScroll();
}
}, []);
const formControlLabels =
(filteredItems.length !== 0) ?
filteredItems.map((item) => (
@ -101,12 +92,13 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
return (
<Box
onScroll={handleScroll}
onScroll={(e) => determineScrollBottom(e, throttle(handleScroll, 700))}
sx={{
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "12px",
padding: "5px",
height: "100%",
maxHeight: "305px",
overflowY: "auto",
}}
>
@ -115,7 +107,6 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
name="controlled-radio-buttons-group"
value={selectedItemId}
onChange={({ target }: SelectChangeEvent<string>) => setSelectedItem(target.value)}
onScroll={onScroll}
>
{formControlLabels}
</RadioGroup>

@ -1,8 +1,10 @@
import * as React from "react";
import { FC, useCallback, useMemo, useRef, useState } from "react";
import { Avatar, MenuItem, Select, SelectChangeEvent, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import arrow_down from "../../assets/icons/arrow_down.svg";
import arrow_down from "@/assets/icons/arrow_down.svg";
import { MinifiedData } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
import { throttle } from "@frontend/kitui";
import { determineScrollBottom } from "@/utils/throttle";
type CustomSelectProps = {
items: MinifiedData[] | [];
@ -22,17 +24,6 @@ export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, set
setOpened((isOpened) => !isOpened);
}, []);
const onScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const scrollHeight = e.currentTarget.scrollHeight;
const scrollTop = e.currentTarget.scrollTop;
const clientHeight = e.currentTarget.clientHeight;
const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9;
if (scrolledToBottom) {
handleScroll();
}
}, []);
const currentItem = useMemo(() => items.find((item) => item.id === selectedItemId) || null, [selectedItemId, items]);
const menuItems = useMemo(() => {
@ -43,7 +34,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, set
value={item.id}
sx={{
padding: "6px 0",
zIndex: 2,
zIndex: 3,
borderTop: "1px solid rgba(154, 154, 175, 0.1)",
width: "auto",
}}
@ -85,7 +76,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, set
<MenuItem
key={"-1"}
disabled
sx={{ padding: "12px", zIndex: 2 }}
sx={{ padding: "12px", zIndex: 3 }}
>
нет данных
</MenuItem>
@ -93,10 +84,10 @@ export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, set
}, [items, selectedItemId]);
return (
<Box>
<Box mt={"20px"}>
<Box
sx={{
zIndex: 3,
zIndex: 4,
position: "relative",
width: "100%",
height: "56px",
@ -149,9 +140,9 @@ export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, set
MenuProps={{
disablePortal: true,
PaperProps: {
onScroll: onScroll,
onScroll: (e) => determineScrollBottom(e, throttle(handleScroll, 700)),
style: {
zIndex: 2,
zIndex: 3,
maxHeight: "300px",
overflow: "auto",
overflowX: "auto",
@ -173,7 +164,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, set
border: 0,
},
"& .MuiMenu-root.MuiModal-root": {
zIndex: 0,
zIndex: 2,
},
}}
onChange={({ target }: SelectChangeEvent<string>) => setSelectedItem(target.value)}

@ -0,0 +1,184 @@
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "./StepButtonsBlock";
import { FC } from "react";
import { AccountResponse } from "@api/integration";
import AccountSetting from "@icons/AccountSetting";
type AmoAccountInfoProps = {
handleNextStep: () => void;
accountInfo: AccountResponse | null;
toChangeAccount: () => void;
};
export const AccountInfo: FC<AmoAccountInfoProps> = ({ handleNextStep, accountInfo, toChangeAccount }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const infoItem = (title: string, value: string | number | undefined) => (
<Box
sx={{
display: "flex",
flexDirection: "column",
mt: "20px",
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{
fontSize: "16px",
lineHeight: "18.96px",
color: theme.palette.grey2.main
}}>{title}:</Typography>
</Box>
<Box sx={{
width: isMobile ? "100%" : "45%",
mt: "5px",
}}>
<Typography>{value || "нет данных"}</Typography>
</Box>
</Box>
);
const infoItemLink = (title: string, link: string | undefined) => (
<Box
sx={{
display: "flex",
flexDirection: "column",
mt: "20px",
}}
>
<Box sx={{ width: "100%" }}>
<Typography sx={{
color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "18.96px",
}}>{title}:</Typography>
</Box>
<Box sx={{ width: "100%" }}>
{
link ?
<a
target="_blank"
href={link}
style={{
wordBreak: "break-word",
fontSize: "18px",
lineHeight: "21.33px",
color: "#7E2AEA"
}}
>
{link}
</a>
:
<Typography>не указана</Typography>
}
</Box>
</Box >
);
return (
<Box
sx={{
display: "inline-flex",
flexDirection: "column",
justifyContent: "space-between",
height: "100%",
overflow: "auto",
flexGrow: 1,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isMobile ? "column" : "row",
width: "100%",
height: "100%",
overflow: "auto",
}}
>
<Box>
<Typography
sx={{
fontSize: "18px",
color: theme.palette.grey3.main,
lineHeight: "21.33px",
}}
>
Информация об аккаунте
</Typography>
<Typography
sx={{
m: "5px 0 19px 0",
lineHeight: "16.59px",
color: theme.palette.grey2.main,
fontSize: "14px",
}}
>
1 шаг
</Typography>
{infoItem("Amo ID", accountInfo?.amoID)}
{infoItem("Имя аккаунта", accountInfo?.name)}
{infoItemLink("ЛК в amo", `https://${accountInfo?.subdomain}.amocrm.ru/dashboard/`)}
{infoItemLink("Профиль пользователя в amo", `https://${accountInfo?.subdomain}.amocrm.ru/settings/users/`)}
{infoItem("Страна пользователя", accountInfo?.country)}
</Box>
<Box>
<Button
variant="outlined"
startIcon={
<AccountSetting
color={theme.palette.brightPurple.main}
height={"20px"}
width={"20px"}
/>
}
onClick={toChangeAccount}
sx={{
height: "44px",
padding: "0",
mt: isMobile ? "30px" : "0",
width: "205px",
backgroundColor: "transparent",
color: theme.palette.brightPurple.main,
"& .MuiButton-startIcon": {
marginRight: isMobile ? 0 : "8px",
marginLeft: 0,
},
"&:hover": {
backgroundColor: theme.palette.brightPurple.main,
color: theme.palette.common.white,
"& path": {
stroke: theme.palette.common.white,
},
"& circle": {
stroke: theme.palette.common.white,
},
},
"&:active": {
backgroundColor: "#581CA7",
color: theme.palette.common.white,
"& path": {
stroke: theme.palette.common.white,
},
"& circle": {
stroke: theme.palette.common.white,
},
},
}}
>
Сменить аккаунт
</Button>
</Box>
</Box>
<StepButtonsBlock
isSmallBtnDisabled={true}
onLargeBtnClick={handleNextStep}
largeBtnText={"Далее"}
/>
</Box>
);
};

@ -1,87 +0,0 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
import { AccountResponse } from "@api/integration";
type AmoAccountInfoProps = {
handleNextStep: () => void;
accountInfo: AccountResponse;
};
export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({ handleNextStep, accountInfo }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const infoItem = (title: string, value: string | number) => (
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
justifyContent: "space-between",
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{ color: theme.palette.grey2.main }}>{title}:</Typography>
</Box>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography>{value || "нет данных"}</Typography>
</Box>
</Box>
);
const infoItemLink = (title: string, link: string) => (
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
justifyContent: "space-between",
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{ color: theme.palette.grey2.main }}>{title}:</Typography>
</Box>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<a
target="_blank"
href={link}
style={{wordBreak: "break-word"}}
>
{link}
</a>
</Box>
</Box>
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
marginTop: isMobile ? "20px" : "60px",
display: "flex",
flexDirection: "column",
gap: isMobile ? "10px" : "20px",
}}
>
{infoItem("Amo ID", accountInfo.amoID)}
{infoItem("Имя аккаунта", accountInfo.name)}
{/* {infoItem("Email аккаунта", accountInfo.email)} */}
{infoItemLink("ЛК в amo", `https://${accountInfo.subdomain}.amocrm.ru/dashboard/`)}
{infoItemLink("Профиль пользователя в amo", `https://${accountInfo.subdomain}.amocrm.ru/settings/users/`)}
{infoItem("Страна пользователя", accountInfo.country)}
</Box>
<StepButtonsBlock
isSmallBtnDisabled={true}
onLargeBtnClick={handleNextStep}
largeBtnText={"Далее"}
/>
</Box>
);
};

@ -1,461 +0,0 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Skeleton } from "@mui/material";
import { useQuestions } from "@/stores/questions/hooks";
import { redirect } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import CloseIcon from "@mui/icons-material/Close";
import { AmoRemoveAccount } from "./AmoRemoveAccount/AmoRemoveAccount";
import { AmoDeleteTagQuestion } from "./AmoRemoveAccount/AmoDeleteTagQuestion";
import { AmoLogin } from "./AmoLogin/AmoLogin";
import { Pipelines } from "./Pipelines/Pipelines";
import { PipelineSteps } from "./PipelineSteps/PipelineSteps";
import { DealPerformers } from "./DealPerformers/DealPerformers";
import { AmoTags } from "./AmoTags/AmoTags";
import { AmoQuestions } from "./AmoQuestions/AmoQuestions";
import { AmoModalTitle } from "./AmoModalTitle/AmoModalTitle";
import { AmoSettingsBlock } from "./SettingsBlock/AmoSettingsBlock";
import { AmoAccountInfo } from "./AmoAccountInfo/AmoAccountInfo";
import { useAmoIntegration } from "./useAmoIntegration";
import { MinifiedData, QuestionKeys, TagKeys, TagQuestionHC } from "./types";
import { Quiz } from "@/model/quiz/quiz";
import { setIntegrationRules, updateIntegrationRules } from "@/api/integration";
type IntegrationsModalProps = {
isModalOpen: boolean;
handleCloseModal: () => void;
companyName: string | null;
quiz: Quiz;
};
const FCTranslate = {
"name": "имя",
"email": "почта",
"phone": "телефон",
"text": "номер",
"address": "адрес",
}
export const AmoCRMModal: FC<IntegrationsModalProps> = ({ isModalOpen, handleCloseModal, companyName, quiz }) => {
//Если нет контекста квиза, то и делать на этой страничке нечего
if (quiz?.backendId === undefined) {
redirect("/list");
return null;
}
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const { questions } = useQuestions();
const minifiedQuestions = useMemo(
() =>
questions
.filter((q) => q.type !== "result" && q.type !== null)
.map(({ backendId, title }) => ({
id: backendId.toString() as string,
title,
})),
[questions]
);
const FieldsAllowedFC = useMemo(
() => {
const list: MinifiedData[] = []
if (quiz.config.showfc) {
const fields = quiz.config.formContact.fields
for (let key in fields) {
if (fields[key].used) list.push({
id: key,
title: FCTranslate[key],
entity: "Contact",
})
}
}
return list;
},
[quiz]
);
const [step, setStep] = useState<number>(0);
const [isSettingsBlock, setIsSettingsBlock] = useState<boolean>(false);
const [isTryRemoveAccount, setIsTryRemoveAccount] = useState<boolean>(false);
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
const {
isLoadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
arrayOfFields,
selectedPipeline,
setSelectedPipeline,
selectedCurrentFields,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
setSelectedCurrentFields,
} = useAmoIntegration({
quizID: quiz.backendId,
isModalOpen,
isTryRemoveAccount,
questions,
});
const handleAddTagQuestion = useCallback(
(scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => {
if (!scope || !id) return;
if (type === "tag") {
setSelectedTags((prevState) => ({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
}));
}
if (type === "question") {
const q = questions.find(e => e.backendId === Number(id))
setSelectedQuestions((prevState) => ({
...prevState,
[scope]: [...prevState[scope as QuestionKeys], {
id,
title: q?.title || "вопрос",
entity: scope,
}],
}));
}
},
[setSelectedQuestions, setSelectedTags, questions]
);
const handleDeleteTagQuestion = useCallback(() => {
if (openDelete === null || !openDelete.scope || !openDelete.id || !openDelete.type) return;
if (openDelete.type === "tag") {
let newArray = selectedTags[openDelete.scope];
const index = newArray.indexOf(openDelete.id);
if (index !== -1) newArray.splice(index, 1);
setSelectedTags((prevState) => ({
...prevState,
[openDelete.scope]: newArray,
}));
}
if (openDelete.type === "question") {
let newArray = selectedQuestions
newArray[openDelete.scope as QuestionKeys] = newArray[openDelete.scope as QuestionKeys].filter(e => e.id !== openDelete.id)
// let index = -1
// selectedQuestions[openDelete.scope].forEach((e, i) => {
// if (e.subTitle === openDelete.id) index = i
// })
// if (index !== -1) newArray.splice(index, 1);
// setSelectedQuestions((prevState) => ({
// ...prevState,
// [openDelete.scope]: newArray,
// }));
setSelectedQuestions(newArray);
setSelectedCurrentFields(selectedCurrentFields.filter(e => e.id !== openDelete.id));
}
setOpenDelete(null);
}, [openDelete]);
const handleNextStep = () => {
setStep((prevState) => prevState + 1);
};
const handlePrevStep = () => {
setStep((prevState) => prevState - 1);
};
const handleSave = () => {
if (quiz?.backendId === undefined) return;
if (selectedPipeline === null) return enqueueSnackbar("Выберите воронку");
if (selectedPipeline === null) return enqueueSnackbar("Выберите этап воронки");
const body = {
PipelineID: Number(selectedPipeline),
StepID: Number(selectedPipelineStep),
PerformerID: Number(selectedDealUser),
// FieldsRule: questionsBackend,
TagsToAdd: selectedTags,
};
const FieldsRule = {
Company: { QuestionID: {} },
Lead: { QuestionID: {} },
Customer: { QuestionID: {} },
Contact: {
QuestionID: {},
ContactRuleMap: {
}
},
};
for (let key in FieldsRule) {
selectedQuestions[key as QuestionKeys].forEach((data) => {
FieldsRule[key as QuestionKeys].QuestionID[data.id] = 0;
});
}
selectedCurrentFields.forEach((data) => {
if (data.entity === "Contact") {
FieldsRule.Contact.ContactRuleMap[data.id] = Number(data.amoId)
} else {
FieldsRule[data.entity].QuestionID[data.id] = Number(data.amoId) || 0
}
})
for (let key in body.TagsToAdd) {
body.TagsToAdd[key as TagKeys] = body.TagsToAdd[key as TagKeys].map((id) => Number(id));
}
body.FieldsRule = FieldsRule;
if (firstRules) {
setIntegrationRules(quiz.backendId.toString(), body);
} else {
updateIntegrationRules(quiz.backendId.toString(), body);
}
handleCloseModal();
setStep(1);
};
const steps = useMemo(
() => [
{
title: accountInfo ? "Информация об аккаунте" : "Авторизация в аккаунте",
isSettingsAvailable: false,
component: accountInfo ? (
<AmoAccountInfo
handleNextStep={handleNextStep}
accountInfo={accountInfo}
/>
) : (
<AmoLogin handleNextStep={handleNextStep} />
),
},
{
title: "Выбор воронки",
isSettingsAvailable: true,
component: (
<Pipelines
users={arrayOfUsers}
pipelines={arrayOfPipelines}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
selectedPipeline={selectedPipeline}
setSelectedPipeline={setSelectedPipeline}
/>
),
},
{
title: "Выбор этапа воронки",
isSettingsAvailable: true,
component: (
<PipelineSteps
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
selectedStep={selectedPipelineStep}
steps={arrayOfPipelinesSteps}
setSelectedDealPerformer={setSelectedDealPerformer}
setSelectedStep={setSelectedPipelineStep}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
/>
),
},
{
title: "Сделка",
isSettingsAvailable: true,
component: (
<DealPerformers
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
/>
),
},
{
title: "Добавление тегов",
isSettingsAvailable: true,
component: (
<AmoTags
tagsItems={arrayOfTags}
selectedTags={selectedTags}
openDelete={setOpenDelete}
handleScroll={() => { }}
handleAddTag={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
/>
),
},
{
title: "Соотнесение вопросов и сущностей",
isSettingsAvailable: true,
component: (
<AmoQuestions
setSelectedCurrentFields={setSelectedCurrentFields}
fieldsItems={arrayOfFields}
selectedCurrentFields={selectedCurrentFields}
questionsItems={minifiedQuestions}
selectedQuestions={selectedQuestions}
openDelete={setOpenDelete}
handleAddQuestion={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleSave}
FieldsAllowedFC={FieldsAllowedFC}
/>
),
},
],
[
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
arrayOfFields,
selectedPipeline,
selectedPipelineStep,
selectedDealUser,
selectedQuestions,
selectedTags,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
minifiedQuestions,
selectedCurrentFields,
]
);
const stepTitles = steps.map((step) => step.title);
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
fullWidth
// fullScreen={isMobile}
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "920px",
maxHeight: isTablet ? "100%" : "660px",
borderRadius: "12px",
},
}}
>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
color: theme.palette.grey2.main,
}}
>
Интеграция с {companyName ? companyName : "партнером"}
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: isTablet ? "100%" : "920px",
height: "600px",
padding: "15px 20px 15px",
flexGrow: 1,
}}
>
{isLoadingPage ?
<Skeleton
sx={{
width: "100%",
height: "100%",
transform: "none",
}}
/> :
<>
<AmoModalTitle
step={step}
steps={steps}
isSettingsBlock={isSettingsBlock}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
startRemoveAccount={() => setIsTryRemoveAccount(true)}
/>
{openDelete !== null ? (
<AmoDeleteTagQuestion
close={() => setOpenDelete(null)}
deleteItem={handleDeleteTagQuestion}
/>
) : (
<>
{isTryRemoveAccount && <AmoRemoveAccount handleCloseModal={handleCloseModal} stopThisPage={() => setIsTryRemoveAccount(false)} />}
{isSettingsBlock && (
<Box sx={{ flexGrow: 1, width: "100%" }}>
<AmoSettingsBlock
stepTitles={stepTitles}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedDealUser={arrayOfUsers.find((u) => u.id === selectedDealUser)?.title || "не указан"}
selectedFunnel={arrayOfPipelines.find((p) => p.id === selectedPipeline)?.title || "нет данных"}
selectedStage={
arrayOfPipelinesSteps.find((s) => s.id === selectedPipelineStep)?.title || "нет данных"
}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
/>
</Box>
)}
{!isSettingsBlock && !isTryRemoveAccount && (
<Box sx={{ flexGrow: 1, width: "100%" }}>{steps[step].component}</Box>
)}
</>
)}
</>
}
</Box>
</Dialog>
);
};
export const diffArr = (arr_A: MinifiedData[], arr_B: MinifiedData[]) => {
return arr_A.filter(person_A => !arr_B.some(person_B => person_A.id === person_B.id));
}

@ -1,6 +1,6 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { AmoButton } from "../../../../../components/AmoButton/AmoButton";
import { AmoButton } from "../../../../components/AmoButton/AmoButton";
import { connectAmo } from "@api/integration";
type IntegrationStep1Props = {

@ -1,97 +0,0 @@
import { FC, useState } from "react";
import { Box } from "@mui/material";
import { ItemsSelectionView } from "../AmoQuestions/ItemsSelectionView/ItemsSelectionView";
import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView";
import { MinifiedData, QuestionKeys, SelectedTags, TagKeys, TagQuestionHC } from "../types";
import { DataConstrictor } from "../Components/DataConstrictor";
type Props = {
tagsItems: MinifiedData[] | [];
selectedTags: SelectedTags;
handleAddTag: (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => void;
openDelete: (data: TagQuestionHC) => void;
handleScroll: () => void;
handlePrevStep: () => void;
handleNextStep: () => void;
};
export const AmoTags: FC<Props> = ({
tagsItems,
selectedTags,
handleAddTag,
openDelete,
handleScroll,
handlePrevStep,
handleNextStep,
}) => {
const [sortedTagsItems, setSortedTagsItems] = useState<MinifiedData[] | []>(tagsItems);
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<TagKeys | null>(null);
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const handleAdd = () => {
if (activeScope === null || selectedTag === null) return;
setActiveScope(null);
handleAddTag(activeScope, selectedTag, "tag");
};
const handleDelete = (id: string, scope: TagKeys) => {
openDelete({
id,
scope,
type: "tag",
});
};
const startConstrictor = (substr: string) => {
const a = tagsItems.filter((mData) => mData.title.toLowerCase().startsWith(substr.toLowerCase()))
setSortedTagsItems(a);
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
// Здесь выбираем элемент в табличку
<>
<DataConstrictor
isError={sortedTagsItems.length === 0}
constrictor={startConstrictor}
/>
<ItemsSelectionView
items={sortedTagsItems}
selectedItemId={selectedTag}
setSelectedItem={setSelectedTag}
handleScroll={handleScroll}
activeScope={activeScope}
onSmallBtnClick={() => {
setActiveScope(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveScope(null);
setIsSelection(false);
}}
/>
</>
) : (
// Табличка
<TagsDetailsView
items={tagsItems}
setActiveScope={setActiveScope}
selectedTags={selectedTags}
setIsSelection={setIsSelection}
handleNextStep={handleNextStep}
handlePrevStep={handlePrevStep}
deleteHC={handleDelete}
/>
)}
</Box>
);
};

@ -0,0 +1,71 @@
import { Box, IconButton, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import Trash from "@icons/trash";
type AnswerItemProps = {
fieldName: string;
fieldValue: string;
deleteHC: () => void;
};
export const AnswerItem: FC<AnswerItemProps> = ({ fieldName, fieldValue, deleteHC }) => {
console.log("AnswerItem")
console.log(fieldName)
const theme = useTheme();
return (
<Box
sx={{
padding: "10px 20px",
height: "140px",
borderBottom: `1px solid ${theme.palette.background.default}`,
display: "flex",
alignItems: "center",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
}}
>
<Typography
sx={{
fontSize: "14px",
fontWeight: 500,
color: theme.palette.grey3.main,
textOverflow: "ellipsis",
overflow: "hidden",
width: "100%",
whiteSpace: "nowrap",
}}
>
{fieldName}
</Typography>
<Typography
sx={{
fontSize: "14px",
fontWeight: 400,
color: theme.palette.grey3.main,
textOverflow: "ellipsis",
overflow: "hidden",
width: "100%",
whiteSpace: "nowrap",
}}
>
{fieldValue}
</Typography>
</Box>
<IconButton
sx={{
m: "auto",
}}
onClick={deleteHC}
>
<Trash />
</IconButton>
</Box>
);
};

@ -1,8 +1,9 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../../components/CustomSelect/CustomSelect";
import { MinifiedData } from "../types";
import { StepButtonsBlock } from "./StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { MinifiedData } from "./types";
import { ModalTitle } from "./ModalTitle";
type Props = {
users: MinifiedData[];
@ -10,6 +11,14 @@ type Props = {
handleNextStep: () => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
titleProps: {
step: number;
title: string;
desc: string;
toSettings: () => void;
}
onScrollUsers: () => void;
};
export const DealPerformers: FC<Props> = ({
@ -18,27 +27,34 @@ export const DealPerformers: FC<Props> = ({
handleNextStep,
selectedDealUser,
setSelectedDealPerformer,
onScrollUsers,
titleProps,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
overflow: "auto",
flexGrow: 1,
}}
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<Box sx={{ width: "100%", zIndex: 3 }}>
<ModalTitle
{...titleProps}
/>
<CustomSelect
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={() => {}}
handleScroll={onScrollUsers}
/>
</Box>
<Box
@ -53,5 +69,6 @@ export const DealPerformers: FC<Props> = ({
/>
</Box>
</Box>
</>
);
};

@ -6,7 +6,7 @@ interface Props {
close: () => void;
}
export const AmoDeleteTagQuestion: FC<Props> = ({ close, deleteItem }) => {
export const DeleteTagQuestion: FC<Props> = ({ close, deleteItem }) => {
const theme = useTheme();
return (

@ -6,87 +6,77 @@ import AccountSetting from "@icons/AccountSetting";
type AmoModalTitleProps = {
step: number;
steps: { title: string; isSettingsAvailable: boolean }[];
isSettingsBlock?: boolean;
setIsSettingsBlock: (value: boolean) => void;
setStep: (value: number) => void;
startRemoveAccount: () => void;
title: string;
desc: string;
toSettings: () => void;
};
export const AmoModalTitle: FC<AmoModalTitleProps> = ({
export const ModalTitle: FC<AmoModalTitleProps> = ({
step,
steps,
setIsSettingsBlock,
isSettingsBlock,
setStep,
startRemoveAccount,
title,
desc,
toSettings,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const handleClick = useCallback(async () => {
if (isSettingsBlock) {
startRemoveAccount();
setIsSettingsBlock(false);
setStep(0);
return;
}
setIsSettingsBlock(true);
}, [isSettingsBlock, setIsSettingsBlock, setStep]);
const btnText = useMemo(() => {
return isSettingsBlock ? "Сменить аккаунт" : "Мои настройки";
}, [isSettingsBlock]);
return (
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
<Box>
<Typography
sx={{
fontSize: isMobile ? "18px" : "24px",
color: theme.palette.grey3.main,
fontWeight: "400",
fontWeight: "500",
lineHeight: "1",
}}
>
{isSettingsBlock ? "Мои настройки" : steps[step].title}
{title}
</Typography>
{isSettingsBlock || (
{
desc &&
<Typography
sx={{
color: theme.palette.grey2.main,
fontWeight: "400",
marginTop: "4px",
fontSize: "14px",
lineHeight: "1",
color: "#4D4D4D",
fontSize: "16px",
m: "5px 0 15px 0",
whiteSpace: "break-spaces",
width: "90%"
}}
>
Шаг {step + 1}
{desc}
</Typography>
)}
}
<Typography
sx={{
color: theme.palette.grey2.main,
fontWeight: "400",
marginTop: "4px",
fontSize: "14px",
lineHeight: "1",
}}
>
{step} шаг
</Typography>
</Box>
{steps[step].isSettingsAvailable && (
<Box>
<Button
variant="outlined"
startIcon={
isSettingsBlock ? (
<AccountSetting
color={theme.palette.brightPurple.main}
height={"20px"}
width={"20px"}
/>
) : (
<GearIcon
color={theme.palette.brightPurple.main}
height={"24px"}
width={"24px"}
/>
)
<GearIcon
color={theme.palette.brightPurple.main}
height={"24px"}
width={"24px"}
/>
}
onClick={handleClick}
onClick={toSettings}
sx={{
padding: isMobile ? "10px" : "10px 20px",
width: "fit-content",
height: "44px",
padding: "0",
width: isMobile ? "44px" : "205px",
minWidth: isMobile ? "44px" : "auto",
backgroundColor: "transparent",
color: theme.palette.brightPurple.main,
"& .MuiButton-startIcon": {
@ -115,9 +105,9 @@ export const AmoModalTitle: FC<AmoModalTitleProps> = ({
},
}}
>
{isMobile ? "" : btnText}
{isMobile ? "" : "Мои настройки"}
</Button>
)}
</Box>
</Box>
);
};

@ -0,0 +1,108 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "./StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup";
import { MinifiedData } from "./types";
import { ModalTitle } from "./ModalTitle";
type Props = {
users: MinifiedData[];
steps: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
selectedStep: string | null;
setSelectedStep: (value: string | null) => void;
titleProps: {
step: number;
title: string;
desc: string;
toSettings: () => void;
}
onScroll: () => void;
onScrollUsers: () => void;
};
export const PipelineSteps: FC<Props> = ({
users,
selectedDealUser,
setSelectedDealPerformer,
steps,
selectedStep,
setSelectedStep,
onScroll,
onScrollUsers,
handlePrevStep,
handleNextStep,
titleProps
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
overflow: "auto",
flexGrow: 1,
}}
>
<Box
sx={{
height: "100%",
overflow: "auto",
zIndex: 3,
width: "100%",
}}>
<Box sx={{ width: "100%", zIndex: 3 }}>
<ModalTitle
{...titleProps}
/>
<CustomSelect
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={onScrollUsers}
/>
</Box>
<Box
sx={{
marginTop: "13px",
flexGrow: 1,
width: "100%",
}}
>
<CustomRadioGroup
items={steps}
selectedItemId={selectedStep}
setSelectedItem={setSelectedStep}
handleScroll={onScroll}
/>
</Box>
</Box>
<Box
sx={{
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
</>
);
};

@ -1,81 +0,0 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../../components/CustomRadioGroup/CustomRadioGroup";
import { MinifiedData } from "../types";
type Props = {
users: MinifiedData[];
steps: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
selectedStep: string | null;
setSelectedStep: (value: string | null) => void;
};
export const PipelineSteps: FC<Props> = ({
users,
selectedDealUser,
setSelectedDealPerformer,
steps,
selectedStep,
setSelectedStep,
handlePrevStep,
handleNextStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={() => {}}
/>
</Box>
<Box
sx={{
marginTop: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
}}
>
<CustomRadioGroup
items={steps}
selectedItemId={selectedStep}
setSelectedItem={setSelectedStep}
handleScroll={() => {}}
/>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
);
};

@ -0,0 +1,105 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "./StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup";
import { MinifiedData } from "./types";
import { ModalTitle } from "./ModalTitle";
type Props = {
pipelines: MinifiedData[];
users: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
selectedPipeline: string | null;
setSelectedPipeline: (value: string | null) => void;
titleProps: {
step: number;
title: string;
desc: string;
toSettings: () => void;
}
onScroll: () => void;
onScrollUsers: () => void;
};
export const Pipelines: FC<Props> = ({
pipelines,
selectedPipeline,
setSelectedPipeline,
titleProps,
users,
selectedDealUser,
setSelectedDealPerformer,
onScroll,
onScrollUsers,
handlePrevStep,
handleNextStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
overflow: "auto",
flexGrow: 1,
}}
>
<Box
sx={{
height: "100%",
overflow: "auto",
zIndex: 3,
width: "100%",
}}>
<Box sx={{ width: "100%", zIndex: 3 }}>
<ModalTitle
{...titleProps}
/>
<CustomSelect
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={onScrollUsers}
/>
</Box>
<Box
sx={{
marginTop: "13px",
flexGrow: 1,
width: "100%",
}}
>
<CustomRadioGroup
items={pipelines}
selectedItemId={selectedPipeline}
setSelectedItem={setSelectedPipeline}
handleScroll={onScroll}
/>
</Box>
</Box>
<Box
sx={{
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
</>
);
};

@ -1,80 +0,0 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../../components/CustomRadioGroup/CustomRadioGroup";
import { MinifiedData } from "../types";
type Props = {
pipelines: MinifiedData[];
users: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
selectedPipeline: string | null;
setSelectedPipeline: (value: string | null) => void;
};
export const Pipelines: FC<Props> = ({
pipelines,
selectedPipeline,
setSelectedPipeline,
users,
selectedDealUser,
setSelectedDealPerformer,
handlePrevStep,
handleNextStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={() => {}}
/>
</Box>
<Box
sx={{
marginTop: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
}}
>
<CustomRadioGroup
items={pipelines}
selectedItemId={selectedPipeline}
setSelectedItem={setSelectedPipeline}
handleScroll={() => {}}
/>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
);
};

@ -4,8 +4,10 @@ import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView";
import { Box } from "@mui/material";
import { MinifiedData, QuestionKeys, SelectedQuestions, TagKeys, TagQuestionHC } from "../types";
import { EntitiesQuestions } from "./EntitiesQuestions";
import { diffArr } from "../AmoCRMModal";
import { diffArr } from "..";
import { DataConstrictor } from "../Components/DataConstrictor";
import { ModalTitle } from "../ModalTitle";
import { StepButtonsBlock } from "../StepButtonsBlock";
type Props = {
selectedCurrentFields: MinifiedData[] | [];
@ -18,6 +20,12 @@ type Props = {
handleNextStep: () => void;
FieldsAllowedFC: MinifiedData[]
setSelectedCurrentFields: (value: MinifiedData[]) => void;
titleProps: {
step: number;
title: string;
toSettings: () => void;
}
onScroll: () => void;
};
export type QuestionPair = {
question: string,
@ -40,7 +48,9 @@ export const AmoQuestions: FC<Props> = ({
handleNextStep,
openDelete,
FieldsAllowedFC,
setSelectedCurrentFields
setSelectedCurrentFields,
onScroll,
titleProps,
}) => {
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<QuestionKeys | null>(null);
@ -109,17 +119,6 @@ export const AmoQuestions: FC<Props> = ({
const [sortedFieldsItems, setSortedFieldsItems] = useState<MinifiedData[]>(fieldsItems);
const [sortedquestionsItems, setSortedquestionsItems] = useState<MinifiedData[]>(questionsItems);
console.log("допущенные ФОРМА КОНТАКТОВ")
console.log(FieldsAllowedFC)
console.log("фильтрованные")
console.log(sortedFieldsAllowedFC)
console.log("")
console.log("допущенные вапросы")
console.log(questionsItems)
console.log("фильтрованные")
console.log(sortedquestionsItems)
const startConstrictor = (substr: string) => {
const a = FieldsAllowedFC.filter((mData) => mData.title.toLowerCase().startsWith(substr.toLowerCase()))
const b = fieldsItems.filter((mData) => mData.title.toLowerCase().startsWith(substr.toLowerCase()))
@ -135,78 +134,93 @@ export const AmoQuestions: FC<Props> = ({
setSortedquestionsItems(questionsItems);
}, [activeScope])
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
<>
<DataConstrictor
isError={false}
constrictor={startConstrictor}
/>
<>
<Box
sx={{
height: "calc( 100% - 70px )",
overflow: "auto"
}}
>
<ModalTitle
{...titleProps}
desc={isSelection && activeScope !== null ? "На этом этапе вы можете соотнести ваше ранее созданное поле с вопросом из квиза или добавить новое поле" : "На этом этапе вы можете добавить в поля соответствующие вопросы"}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
// Здесь выбираем элемент в табличку
<>
<DataConstrictor
isError={false}
constrictor={startConstrictor}
/>
<EntitiesQuestions
FieldsAllowedFC={sortedFieldsAllowedFC}
fieldsItems={sortedFieldsItems}
items={(sortedquestionsItems.length === 0) ? [] : diffArr(sortedquestionsItems, selectedQuestions[activeScope])}
selectedItemId={selectedQuestion}
setSelectedQuestion={setSelectedQuestion}
selectedField={selectedField}
selectedCurrentFields={selectedCurrentFields}
setSelectedField={setSelectedField}
activeScope={activeScope}
setIsCurrentFields={setIsCurrentFields}
isCurrentFields={isCurrentFields}
handleScroll={onScroll}
/>
</>
) : (
// Табличка
<ItemDetailsView
items={[...questionsItems, ...FieldsAllowedFC]}
setActiveScope={setActiveScope}
selectedQuestions={{
Lead: [...selectedQuestions.Lead, ...SCFworld.Lead],
Company: [...selectedQuestions.Company, ...SCFworld.Company],
Customer: [...selectedQuestions.Customer, ...SCFworld.Customer],
Contact: [...selectedQuestions.Contact, ...SCFworld.Contact]
}}
setIsSelection={setIsSelection}
deleteHC={handleDelete}
/>
)}
</Box>
</Box>
{/* табличка */}
<EntitiesQuestions
FieldsAllowedFC={sortedFieldsAllowedFC}
fieldsItems={sortedFieldsItems}
items={(sortedquestionsItems.length === 0) ? [] : diffArr(sortedquestionsItems, selectedQuestions[activeScope])}
selectedItemId={selectedQuestion}
setSelectedQuestion={setSelectedQuestion}
selectedField={selectedField}
selectedCurrentFields={selectedCurrentFields}
setSelectedField={setSelectedField}
onSmallBtnClick={() => {
setActiveScope(null);
setIsSelection(false);
}}
<Box
sx={{
alignSelf: "end",
}}
>
{isSelection && activeScope !== null ?
<StepButtonsBlock
onLargeBtnClick={() => {
handleAdd();
setActiveScope(null);
setIsSelection(false);
}}
activeScope={activeScope}
setIsCurrentFields={setIsCurrentFields}
isCurrentFields={isCurrentFields}
largeBtnText={"Добавить"}
onSmallBtnClick={() => {
setActiveScope(null);
setIsSelection(false);
}}
smallBtnText={"Отменить"}
/>
</>
// Здесь выбираем элемент в табличку
// <ItemsSelectionView
// items={questionsItems}
// selectedItemId={selectedQuestion}
// setSelectedItem={setSelectedQuestion}
// onSmallBtnClick={() => {
// setActiveScope(null);
// setIsSelection(false);
// }}
// onLargeBtnClick={() => {
// handleAdd();
// setActiveScope(null);
// setIsSelection(false);
// }}
// />
) : (
// Табличка
<ItemDetailsView
items={[...questionsItems, ...FieldsAllowedFC]}
setActiveScope={setActiveScope}
selectedQuestions={{
Lead: [...selectedQuestions.Lead, ...SCFworld.Lead],
Company: [...selectedQuestions.Company, ...SCFworld.Company],
Customer: [...selectedQuestions.Customer, ...SCFworld.Customer],
Contact: [...selectedQuestions.Contact, ...SCFworld.Contact]
}}
setIsSelection={setIsSelection}
handleLargeBtn={handleNextStep}
handleSmallBtn={handlePrevStep}
deleteHC={handleDelete}
/>
)}
</Box>
:
<StepButtonsBlock
onSmallBtnClick={handlePrevStep}
onLargeBtnClick={handleNextStep}
largeBtnText={"Сохранить"}
/>
}
</Box>
</>
);
};

@ -1,8 +1,8 @@
import { CustomRadioGroup } from "@/components/CustomRadioGroup/CustomRadioGroup"
import {Box, Typography, useMediaQuery, useTheme} from "@mui/material"
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"
import { MinifiedData } from "../types";
import {CustomSelect} from "@/components/CustomSelect/CustomSelect";
import {CurrentFieldSelect} from "@/pages/IntegrationsPage/IntegrationsModal/Amo/AmoQuestions/CurrentFieldSelectMobile";
import { CustomSelect } from "@/components/CustomSelect/CustomSelect";
import { CurrentFieldSelect } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/Questions/CurrentFieldSelectMobile";
interface Props {
items: MinifiedData[];
@ -11,6 +11,7 @@ interface Props {
currentQuestion: string;
setCurrentField: (value: string) => void;
setCurrentQuestion: (value: string) => void;
handleScroll: () => void;
}
export const CurrentFields = ({
items,
@ -19,6 +20,7 @@ export const CurrentFields = ({
currentQuestion,
setCurrentField,
setCurrentQuestion,
handleScroll,
}: Props) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -53,18 +55,18 @@ export const CurrentFields = ({
{isMobile &&
<CurrentFieldSelect
items={fieldsItems}
selectedItemId={currentField
} setSelectedItem={setCurrentField}
handleScroll={() => { }}/>
selectedItemId={currentField}
setSelectedItem={setCurrentField}
handleScroll={handleScroll} />
}
{!isMobile &&
<CustomRadioGroup
items={fieldsItems}
selectedItemId={currentField}
setSelectedItem={setCurrentField}
handleScroll={() => { }}
activeScope={undefined}
/>
<CustomRadioGroup
items={fieldsItems}
selectedItemId={currentField}
setSelectedItem={setCurrentField}
handleScroll={handleScroll}
activeScope={undefined}
/>
}
</Box>
@ -85,20 +87,20 @@ export const CurrentFields = ({
}}
>Выберите вопрос для этого поля</Typography>
{isMobile &&
<CurrentFieldSelect
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }}/>
<CurrentFieldSelect
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }} />
}
{!isMobile &&
<CustomRadioGroup
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }}
activeScope={undefined}
/>
<CustomRadioGroup
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }}
activeScope={undefined}
/>
}
</Box>

@ -1,11 +1,11 @@
import {Box, Button, useMediaQuery, useTheme} from "@mui/material"
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock"
import { StepButtonsBlock } from "../StepButtonsBlock"
import { FC, useState } from "react";
import { MinifiedData, TagKeys } from "../types";
import { CurrentFields } from "./CurrentFields";
import { NewFields } from "./NewFields";
import { QuestionPair } from "./AmoQuestions";
import { diffArr } from "../AmoCRMModal";
import { diffArr } from "..";
type ItemsSelectionViewProps = {
items: MinifiedData[] | [];
@ -14,9 +14,7 @@ type ItemsSelectionViewProps = {
setSelectedQuestion: (value: string | null) => void;
selectedField?: string | null;
setSelectedField: (value: string | null) => void;
handleScroll?: () => void;
onLargeBtnClick: () => void;
onSmallBtnClick: () => void;
handleScroll: () => void;
activeScope: TagKeys;
FieldsAllowedFC: MinifiedData[];
selectedCurrentFields: MinifiedData[];
@ -31,8 +29,6 @@ export const EntitiesQuestions: FC<ItemsSelectionViewProps> = ({
selectedField,
setSelectedField,
handleScroll,
onLargeBtnClick,
onSmallBtnClick,
activeScope,
FieldsAllowedFC,
selectedCurrentFields,
@ -41,13 +37,6 @@ export const EntitiesQuestions: FC<ItemsSelectionViewProps> = ({
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
console.log("--------------")
console.log("EntitiesQuestions")
console.log("вопросы")
console.log(items)
console.log("ФК")
console.log(FieldsAllowedFC)
console.log("------------------------------")
return (
<Box
@ -105,6 +94,7 @@ export const EntitiesQuestions: FC<ItemsSelectionViewProps> = ({
currentQuestion={selectedItemId}
setCurrentField={setSelectedField}
setCurrentQuestion={setSelectedQuestion}
handleScroll={handleScroll}
/>
:
<NewFields
@ -114,21 +104,6 @@ export const EntitiesQuestions: FC<ItemsSelectionViewProps> = ({
/>
}
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={() => {
onLargeBtnClick()
}}
largeBtnText={"Добавить"}
onSmallBtnClick={onSmallBtnClick}
smallBtnText={"Отменить"}
/>
</Box>
</Box>
)
}

@ -1,6 +1,6 @@
import { Box, useTheme } from "@mui/material";
import { ItemForQuestions } from "../ItemForQuestions";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { StepButtonsBlock } from "../../StepButtonsBlock";
import { FC } from "react";
import { MinifiedData, QuestionKeys, SelectedQuestions } from "../../types";
@ -9,8 +9,6 @@ type TitleKeys = "Contact" | "Company" | "Lead" | "Customer";
type ItemDetailsViewProps = {
items: MinifiedData[];
setIsSelection: (value: boolean) => void;
handleSmallBtn: () => void;
handleLargeBtn: () => void;
selectedQuestions: SelectedQuestions;
setActiveScope: (value: QuestionKeys | null) => void;
deleteHC: (id: string, scope: QuestionKeys) => void;
@ -18,8 +16,6 @@ type ItemDetailsViewProps = {
export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
items,
handleSmallBtn,
handleLargeBtn,
selectedQuestions,
setIsSelection,
setActiveScope,
@ -68,18 +64,6 @@ export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
))}
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={handleSmallBtn}
onLargeBtnClick={handleLargeBtn}
largeBtnText={"Сохранить"}
/>
</Box>
</Box>
);
};

@ -1,6 +1,6 @@
import { Box } from "@mui/material";
import { CustomRadioGroup } from "../../../../../../components/CustomRadioGroup/CustomRadioGroup";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { StepButtonsBlock } from "../../StepButtonsBlock";
import { FC } from "react";
import { MinifiedData, TagKeys } from "../../types";
@ -9,8 +9,6 @@ type ItemsSelectionViewProps = {
selectedItemId?: string | null;
setSelectedItem: (value: string | null) => void;
handleScroll?: () => void;
onLargeBtnClick: () => void;
onSmallBtnClick: () => void;
activeScope: TagKeys;
};
@ -19,8 +17,6 @@ export const ItemsSelectionView: FC<ItemsSelectionViewProps> = ({
selectedItemId,
setSelectedItem,
handleScroll,
onLargeBtnClick,
onSmallBtnClick,
activeScope,
}) => {
return (
@ -49,19 +45,6 @@ export const ItemsSelectionView: FC<ItemsSelectionViewProps> = ({
activeScope={activeScope}
/>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={onLargeBtnClick}
largeBtnText={"Добавить"}
onSmallBtnClick={onSmallBtnClick}
smallBtnText={"Отменить"}
/>
</Box>
</Box>
);
};

@ -10,7 +10,7 @@ interface Props {
}
export const AmoRemoveAccount: FC<Props> = ({
export const RemoveAccount: FC<Props> = ({
stopThisPage,
handleCloseModal,

@ -1,81 +0,0 @@
import { FC } from "react";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { SettingItem } from "./SettingItem/SettingItem";
import { SelectedQuestions, SelectedTags } from "../types";
type AmoSettingsBlockProps = {
stepTitles: string[];
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
selectedFunnel: string | null;
selectedStage: string | null;
selectedDealUser: string | null;
selectedQuestions: SelectedQuestions;
selectedTags: SelectedTags;
};
export const AmoSettingsBlock: FC<AmoSettingsBlockProps> = ({
stepTitles,
setStep,
setIsSettingsBlock,
selectedFunnel,
selectedDealUser,
selectedStage,
selectedQuestions,
selectedTags,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
marginTop: "10px",
width: "100%",
height: "443px",
borderRadius: "10px",
padding: " 0 20px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
overflowY: "auto",
flexGrow: 1,
}}
>
{stepTitles &&
stepTitles.map((title, index) => (
<SettingItem
step={index}
title={title}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedDealUser={selectedDealUser}
selectedFunnel={selectedFunnel}
selectedStage={selectedStage}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
/>
))}
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={() => setIsSettingsBlock(false)}
isLargeBtnMissing={true}
/>
</Box>
</Box>
);
};

@ -9,8 +9,7 @@ import { SelectedQuestions, SelectedTags } from "../../types";
type SettingItemProps = {
step: number;
title: string;
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
setStep: (step: number) => void;
selectedFunnelPerformer: string | null;
selectedFunnel: string | null;
selectedStagePerformer: string | null;
@ -24,7 +23,6 @@ export const SettingItem: FC<SettingItemProps> = ({
step,
title,
setStep,
setIsSettingsBlock,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
@ -34,6 +32,7 @@ export const SettingItem: FC<SettingItemProps> = ({
selectedTags,
}) => {
const theme = useTheme();
console.log(step)
const isMobile = useMediaQuery(theme.breakpoints.down(600));
if (step === 0) {
return;
@ -146,8 +145,7 @@ export const SettingItem: FC<SettingItemProps> = ({
<SettingItemHeader
title={title}
step={step}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
setStep={() => setStep(step)}
/>
<Box>{SettingsContent}</Box>
</Box>

@ -6,23 +6,16 @@ import { FC } from "react";
type SettingItemHeaderProps = {
title: string;
step: number;
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
setStep: () => void;
};
export const SettingItemHeader: FC<SettingItemHeaderProps> = ({
title,
step,
setStep,
setIsSettingsBlock,
}) => {
const theme = useTheme();
const handleClick = () => {
setStep(step);
setIsSettingsBlock(false);
};
return (
<Box>
<Box
@ -41,7 +34,7 @@ export const SettingItemHeader: FC<SettingItemHeaderProps> = ({
>
{step} этап
</Typography>
<IconButton onClick={handleClick}>
<IconButton onClick={setStep}>
<EditPencil
color={theme.palette.brightPurple.main}
width={"18px"}

@ -0,0 +1,92 @@
import { FC } from "react";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../StepButtonsBlock";
import { SettingItem } from "./SettingItem/SettingItem";
import { SelectedQuestions, SelectedTags } from "../types";
type AmoSettingsBlockProps = {
stepTitles: string[];
selectedFunnel: string | null;
selectedStage: string | null;
selectedDealUser: string | null;
selectedQuestions: SelectedQuestions;
selectedTags: SelectedTags;
toBack: () => void
setStep: (step: number) => void
};
export const SettingsBlock: FC<AmoSettingsBlockProps> = ({
stepTitles,
selectedFunnel,
selectedDealUser,
selectedStage,
selectedQuestions,
selectedTags,
toBack,
setStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box sx={{ flexGrow: 1, width: "100%", height: "100%"}}>
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
<Typography
sx={{
fontSize: "24px",
fontWeight: 500,
lineHeight: "28.44px"
}}
>
Мои настройки
</Typography>
<Box
sx={{
marginTop: "20px",
width: "100%",
minHheight: "440px",
maxHeight: "90%",
borderRadius: "10px",
padding: " 0 20px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
overflowY: "auto",
flexGrow: 1,
}}
>
{stepTitles &&
stepTitles.map((title, index) => (
<SettingItem
step={index+1}
title={title}
selectedDealUser={selectedDealUser}
selectedFunnel={selectedFunnel}
selectedStage={selectedStage}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
setStep={setStep}
/>
))}
</Box>
<Box
sx={{
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={toBack}
isLargeBtnMissing={true}
/>
</Box>
</Box>
</Box>
);
};

@ -30,9 +30,10 @@ export const StepButtonsBlock: FC<StepButtonsBlockProps> = ({
<Box
sx={{
display: "flex",
justifyContent: "end",
alignItems: "end",
gap: "10px",
marginTop: "auto",
marginLeft: "auto",
mt: "20px"
}}
>
{isSmallBtnMissing || (

@ -0,0 +1,396 @@
import { useMemo, useState } from "react"
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Skeleton } from "@mui/material";
import { useQuestions } from "@/stores/questions/hooks";
import { redirect } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import CloseIcon from "@mui/icons-material/Close";
import { RemoveAccount } from "./RemoveAccount";
import { DeleteTagQuestion } from "./DeleteTagQuestion";
import { AmoLogin } from "./AmoLogin";
import { Pipelines } from "./Pipelines";
import { PipelineSteps } from "./PipelineSteps";
import { DealPerformers } from "./DealPerformers";
import { AmoTags } from "./Tags/AmoTags";
import { AmoQuestions } from "./Questions/AmoQuestions";
import { ModalTitle } from "./ModalTitle";
import { SettingsBlock } from "./SettingsBlock/SettingsBlock";
import { AccountInfo } from "./AccountInfo";
import { useAmoIntegration } from "./useAmoIntegration";
import { MinifiedData, QuestionKeys, TagKeys, TagQuestionHC } from "./types";
import { Quiz } from "@/model/quiz/quiz";
import { AccountResponse, setIntegrationRules, updateIntegrationRules } from "@/api/integration";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { UntypedQuizQuestion } from "@/model/questionTypes/shared";
const FCTranslate = {
"name": "имя",
"email": "почта",
"phone": "телефон",
"text": "номер",
"address": "адрес",
}
interface Props {
quiz: Quiz;
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
firstRules: boolean;
accountInfo: AccountResponse | null;
arrayOfPipelines: MinifiedData[];
arrayOfPipelinesSteps: MinifiedData[];
arrayOfUsers: MinifiedData[];
arrayOfTags: MinifiedData[];
arrayOfFields: MinifiedData[];
selectedPipeline: string | null;
selectedCurrentFields: MinifiedData[];
selectedPipelineStep: string | null;
selectedDealUser: string | null;
setSelectedPipeline: any;
setSelectedPipelineStep: any;
setSelectedDealPerformer: any;
selectedTags: any;
setSelectedTags: any;
selectedQuestions: any;
setSelectedQuestions: any;
setPageOfPipelines: () => void;
setPageOfPipelinesSteps: () => void;
setPageOfUsers: () => void;
setPageOfTags: () => void;
setPageOfFields: () => void;
setSelectedCurrentFields: any;
handleCloseModal: any;
}
export const SwitchPages = ({
quiz,
questions,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
arrayOfFields,
selectedPipeline,
setSelectedPipeline,
selectedCurrentFields,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
setPageOfFields,
setSelectedCurrentFields,
handleCloseModal,
}: Props) => {
const [step, setStep] = useState(0)
const [specialPage, setSpecialPage] = useState<"deleteCell" | "removeAccount" | "settingsBlock" | "accountInfo" | "amoLogin" | "">(accountInfo ? "accountInfo" : "amoLogin")
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
const startDeleteTagQuestion = (itemForDelete) => {
setOpenDelete(itemForDelete)
setSpecialPage("deleteCell")
}
const minifiedQuestions = useMemo(
() =>
questions
.filter((q) => q.type !== "result" && q.type !== null)
.map(({ backendId, title }) => ({
id: backendId.toString() as string,
title,
})),
[questions]
);
const FieldsAllowedFC = useMemo(
() => {
const list: MinifiedData[] = []
if (quiz.config.showfc) {
const fields = quiz.config.formContact.fields
for (let key in fields) {
if (fields[key].used) list.push({
id: key,
title: FCTranslate[key],
entity: "Contact",
})
}
}
return list;
},
[quiz]
);
const handleAddTagQuestion = (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => {
if (!scope || !id) return;
if (type === "tag") {
setSelectedTags((prevState) => ({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
}));
}
if (type === "question") {
const q = questions.find(e => e.backendId === Number(id))
setSelectedQuestions((prevState) => ({
...prevState,
[scope]: [...prevState[scope as QuestionKeys], {
id,
title: q?.title || "вопрос",
entity: scope,
}],
}));
}
}
const handleDeleteTagQuestion = () => {
if (openDelete === null || !openDelete.scope || !openDelete.id || !openDelete.type) return;
if (openDelete.type === "tag") {
let newArray = selectedTags[openDelete.scope];
const index = newArray.indexOf(openDelete.id);
if (index !== -1) newArray.splice(index, 1);
setSelectedTags((prevState) => ({
...prevState,
[openDelete.scope]: newArray,
}));
}
if (openDelete.type === "question") {
let newArray = selectedQuestions
newArray[openDelete.scope as QuestionKeys] = newArray[openDelete.scope as QuestionKeys].filter(e => e.id !== openDelete.id)
setSelectedQuestions(newArray);
setSelectedCurrentFields(selectedCurrentFields.filter(e => e.id !== openDelete.id));
}
setOpenDelete(null);
closeSpecialPage();
}
const handleNextStep = () => {
setStep((prevState) => prevState + 1);
};
const handlePrevStep = () => {
setStep((prevState) => prevState - 1);
};
const handleSave = () => {
if (quiz?.backendId === undefined) return;
if (selectedPipeline === null) return enqueueSnackbar("Выберите воронку");
if (selectedPipeline === null) return enqueueSnackbar("Выберите этап воронки");
const body = {
PipelineID: Number(selectedPipeline),
StepID: Number(selectedPipelineStep),
PerformerID: Number(selectedDealUser),
// FieldsRule: questionsBackend,
TagsToAdd: selectedTags,
};
const FieldsRule = {
Company: { QuestionID: {} },
Lead: { QuestionID: {} },
Customer: { QuestionID: {} },
Contact: {
QuestionID: {},
ContactRuleMap: {
}
},
};
for (let key in FieldsRule) {
selectedQuestions[key as QuestionKeys].forEach((data) => {
FieldsRule[key as QuestionKeys].QuestionID[data.id] = 0;
});
}
selectedCurrentFields.forEach((data) => {
if (data.entity === "Contact") {
FieldsRule.Contact.ContactRuleMap[data.id] = Number(data.amoId)
} else {
FieldsRule[data.entity].QuestionID[data.id] = Number(data.amoId) || 0
}
})
for (let key in body.TagsToAdd) {
body.TagsToAdd[key as TagKeys] = body.TagsToAdd[key as TagKeys].map((id) => Number(id));
}
body.FieldsRule = FieldsRule;
console.log(body)
if (firstRules) {
setIntegrationRules(quiz.backendId.toString(), body);
} else {
updateIntegrationRules(quiz.backendId.toString(), body);
}
handleCloseModal();
};
const closeSpecialPage = () => setSpecialPage("")
const steps = [
{
isSettingsAvailable: true,
component: (
<Pipelines
users={arrayOfUsers}
pipelines={arrayOfPipelines}
handlePrevStep={() => setSpecialPage("accountInfo")}
handleNextStep={handleNextStep}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
selectedPipeline={selectedPipeline}
setSelectedPipeline={setSelectedPipeline}
titleProps={{
step: step + 2,
title: "Выбор воронки",
desc: "На этом этапе вы можете выбрать нужную воронку и ответственного за сделку",
toSettings: () => setSpecialPage("settingsBlock")
}}
onScroll={setPageOfPipelines}
onScrollUsers={setPageOfUsers}
/>
),
},
{
isSettingsAvailable: true,
component: (
<PipelineSteps
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
selectedStep={selectedPipelineStep}
steps={arrayOfPipelinesSteps}
setSelectedDealPerformer={setSelectedDealPerformer}
setSelectedStep={setSelectedPipelineStep}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
titleProps={{
step: step + 2,
title: "Выбор этапа воронки",
desc: "На этом этапе вы можете выбрать нужный этап и ответственного за сделку",
toSettings: () => setSpecialPage("settingsBlock")
}}
onScroll={setPageOfPipelinesSteps}
onScrollUsers={setPageOfUsers}
/>
),
},
{
isSettingsAvailable: true,
component: (
<DealPerformers
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
titleProps={{
step: step + 2,
title: "Сделка",
desc: "На этом этапе вы можете выбрать ответственного за сделку",
toSettings: () => setSpecialPage("settingsBlock")
}}
onScrollUsers={setPageOfUsers}
/>
),
},
{
isSettingsAvailable: true,
component: (
<AmoTags
tagsItems={arrayOfTags}
selectedTags={selectedTags}
openDelete={startDeleteTagQuestion}
handleAddTag={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
titleProps={{
step: step + 2,
title: "Добавление тегов",
desc: "На этом этапе вы можете добавить теги с результатами",
toSettings: () => setSpecialPage("settingsBlock")
}}
onScroll={setPageOfTags}
/>
),
},
{
isSettingsAvailable: true,
component: (
<AmoQuestions
setSelectedCurrentFields={setSelectedCurrentFields}
fieldsItems={arrayOfFields}
selectedCurrentFields={selectedCurrentFields}
questionsItems={minifiedQuestions}
selectedQuestions={selectedQuestions}
openDelete={startDeleteTagQuestion}
handleAddQuestion={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleSave}
FieldsAllowedFC={FieldsAllowedFC}
titleProps={{
step: step + 2,
title: "Соотнесение вопросов и сущностей",
toSettings: () => setSpecialPage("settingsBlock")
}}
onScroll={setPageOfFields}
/>
),
},
]
const stepTitles = steps.map((step) => step.title);
switch (specialPage) {
case "deleteCell":
return <DeleteTagQuestion
close={closeSpecialPage}
deleteItem={handleDeleteTagQuestion}
/>
case "removeAccount":
return <RemoveAccount
handleCloseModal={handleCloseModal}
stopThisPage={closeSpecialPage}
/>
case "settingsBlock":
return <SettingsBlock
stepTitles={stepTitles}
selectedDealUser={arrayOfUsers.find((u) => u.id === selectedDealUser)?.title || "не указан"}
selectedFunnel={arrayOfPipelines.find((p) => p.id === selectedPipeline)?.title || "нет данных"}
selectedStage={
arrayOfPipelinesSteps.find((s) => s.id === selectedPipelineStep)?.title || "нет данных"
}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
toBack={() => closeSpecialPage()}
setStep={(step: number) => {
closeSpecialPage()
setStep(step - 1)
}}
/>
case "amoLogin": return <AmoLogin handleNextStep={handleNextStep} />
case "accountInfo": return <AccountInfo
handleNextStep={() => closeSpecialPage()}
accountInfo={accountInfo}
toChangeAccount={() => setSpecialPage("removeAccount")}
/>
default: return <Box sx={{
flexGrow: 1,
width: "100%",
height: "100%",
overflow: "auto"
}}>{steps[step].component}</Box>
}
}

@ -0,0 +1,135 @@
import { FC, useState } from "react";
import { Box } from "@mui/material";
import { ItemsSelectionView } from "../Questions/ItemsSelectionView/ItemsSelectionView";
import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView";
import { MinifiedData, QuestionKeys, SelectedTags, TagKeys, TagQuestionHC } from "../types";
import { DataConstrictor } from "../Components/DataConstrictor";
import { ModalTitle } from "../ModalTitle";
import { StepButtonsBlock } from "../StepButtonsBlock";
type Props = {
tagsItems: MinifiedData[] | [];
selectedTags: SelectedTags;
handleAddTag: (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => void;
openDelete: (data: TagQuestionHC) => void;
handlePrevStep: () => void;
handleNextStep: () => void;
titleProps: {
step: number;
title: string;
desc: string;
toSettings: () => void;
}
onScroll: () => void;
};
export const AmoTags: FC<Props> = ({
tagsItems,
selectedTags,
handleAddTag,
openDelete,
handlePrevStep,
handleNextStep,
onScroll,
titleProps,
}) => {
const [sortedTagsItems, setSortedTagsItems] = useState<MinifiedData[] | []>(tagsItems);
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<TagKeys | null>(null);
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const handleAdd = () => {
if (activeScope === null || selectedTag === null) return;
setActiveScope(null);
handleAddTag(activeScope, selectedTag, "tag");
};
const handleDelete = (id: string, scope: TagKeys) => {
openDelete({
id,
scope,
type: "tag",
});
};
const startConstrictor = (substr: string) => {
const a = tagsItems.filter((mData) => mData.title.toLowerCase().startsWith(substr.toLowerCase()))
setSortedTagsItems(a);
}
return (
<>
<Box
sx={{
height: "calc( 100% - 70px )",
overflow: "auto"
}}
>
<ModalTitle
{...titleProps}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
// Здесь выбираем элемент в табличку
<>
<DataConstrictor
isError={sortedTagsItems.length === 0}
constrictor={startConstrictor}
/>
<ItemsSeactiveScopelectionView
items={sortedTagsItems}
selectedItemId={selectedTag}
setSelectedItem={setSelectedTag}
handleScroll={onScroll}
activeScope={activeScope}
/>
</>
) : (
// Табличка
<TagsDetailsView
items={tagsItems}
setActiveScope={setActiveScope}
selectedTags={selectedTags}
setIsSelection={setIsSelection}
deleteHC={handleDelete}
/>
)}
</Box>
</Box>
<Box
sx={{
alignSelf: "end",
}}
>
{
isSelection && activeScope !== null ?
<StepButtonsBlock
onLargeBtnClick={() => {
handleAdd();
setActiveScope(null);
setIsSelection(false);
}}
largeBtnText={"Добавить"}
onSmallBtnClick={() => {
setActiveScope(null);
setIsSelection(false);
}}
smallBtnText={"Отменить"}
/>
:
<StepButtonsBlock
onSmallBtnClick={handlePrevStep}
onLargeBtnClick={handleNextStep}
/>
}
</Box>
</>
);
};

@ -1,14 +1,12 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { StepButtonsBlock } from "../../StepButtonsBlock";
import { FC } from "react";
import { Item } from "../../AmoQuestions/Item/Item";
import { Item } from "../../Questions/Item/Item";
import { MinifiedData, SelectedTags, TagKeys } from "../../types";
type TagsDetailsViewProps = {
items: MinifiedData[];
setIsSelection: (value: boolean) => void;
handlePrevStep: () => void;
handleNextStep: () => void;
setActiveScope: (value: TagKeys | null) => void;
selectedTags: SelectedTags;
deleteHC: (id: string, scope: TagKeys) => void;
@ -19,8 +17,6 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
setActiveScope,
selectedTags,
setIsSelection,
handlePrevStep,
handleNextStep,
deleteHC,
}) => {
const theme = useTheme();
@ -30,7 +26,7 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
return (
<Box
sx={{
marginTop: "20px",
marginTop: "15px",
display: "flex",
flexDirection: "column",
alignItems: "center",
@ -41,7 +37,7 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
<Box
sx={{
width: "100%",
height: "400px",
maxHeight: "380px",
flexGrow: 1,
borderRadius: "10px",
padding: "10px",
@ -88,17 +84,6 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
))}
</Box>
</Box>
<Box
sx={{
marginTop: "20px",
alignSelf: "end",
}}
>
<StepButtonsBlock
onSmallBtnClick={handlePrevStep}
onLargeBtnClick={handleNextStep}
/>
</Box>
</Box>
);
};

@ -0,0 +1,172 @@
import { FC, useState } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Skeleton } from "@mui/material";
import { useQuestions } from "@/stores/questions/hooks";
import { redirect, useNavigate } from "react-router-dom";
import CloseIcon from "@mui/icons-material/Close";
import { useAmoIntegration } from "./useAmoIntegration";
import { MinifiedData } from "./types";
import { Quiz } from "@/model/quiz/quiz";
import { SwitchPages } from "./SwitchPages";
type IntegrationsModalProps = {
isModalOpen: boolean;
handleCloseModal: () => void;
companyName: string | null;
quiz: Quiz;
};
export const AmoCRMModal: FC<IntegrationsModalProps> = ({ isModalOpen, handleCloseModal, companyName, quiz }) => {
//Если нет контекста квиза, то и делать на этой страничке нечего
if (quiz.backendId === undefined) {
redirect("/list");
return null;
}
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const { questions } = useQuestions();
const [isTryRemoveAccount, setIsTryRemoveAccount] = useState<boolean>(false);
const {
isLoadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
arrayOfFields,
selectedPipeline,
setSelectedPipeline,
selectedCurrentFields,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
setPageOfFields,
setSelectedCurrentFields,
} = useAmoIntegration({
quizID: quiz.backendId,
isModalOpen,
isTryRemoveAccount,
questions,
});
return (
<Dialog
open={isModalOpen}
onClose={handleCloseModal}
fullWidth
// fullScreen={isMobile}
PaperProps={{
sx: {
maxWidth: isTablet ? "100%" : "919px",
height: "658px",
borderRadius: "12px",
},
}}
>
<Box>
<Box
sx={{
width: "100%",
height: "68px",
backgroundColor: theme.palette.background.default,
}}
>
<Typography
sx={{
fontSize: isMobile ? "20px" : "24px",
fontWeight: "500",
padding: "20px",
color: theme.palette.grey2.main,
}}
>
Интеграция с {companyName ? companyName : "партнером"}
</Typography>
</Box>
<IconButton
onClick={handleCloseModal}
sx={{
width: "12px",
height: "12px",
position: "absolute",
right: "15px",
top: "15px",
}}
>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
</Box>
<Box
className="родитель"
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
padding: "15px 20px 15px",
flexGrow: 1,
height: "100%",
overflow: "auto"
}}
>
{isLoadingPage ?
<Skeleton
sx={{
width: "100%",
height: "100%",
transform: "none",
}}
/> :
<SwitchPages
quiz={quiz}
questions={questions}
firstRules={firstRules}
accountInfo={accountInfo}
arrayOfPipelines={arrayOfPipelines}
arrayOfPipelinesSteps={arrayOfPipelinesSteps}
arrayOfUsers={arrayOfUsers}
arrayOfTags={arrayOfTags}
arrayOfFields={arrayOfFields}
selectedPipeline={selectedPipeline}
setSelectedPipeline={setSelectedPipeline}
selectedCurrentFields={selectedCurrentFields}
selectedPipelineStep={selectedPipelineStep}
setSelectedPipelineStep={setSelectedPipelineStep}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
selectedQuestions={selectedQuestions}
setSelectedQuestions={setSelectedQuestions}
setPageOfPipelines={setPageOfPipelines}
setPageOfPipelinesSteps={setPageOfPipelinesSteps}
setPageOfUsers={setPageOfUsers}
setPageOfTags={setPageOfTags}
setPageOfFields={setPageOfFields}
setSelectedCurrentFields={setSelectedCurrentFields}
handleCloseModal={handleCloseModal}
/>
}
</Box>
</Dialog>
);
};
export const diffArr = (arr_A: MinifiedData[], arr_B: MinifiedData[]) => {
return arr_A.filter(person_A => !arr_B.some(person_B => person_A.id === person_B.id));
}

@ -13,14 +13,15 @@ import {
getFields,
} from "@/api/integration";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { UntypedQuizQuestion } from "@/model/questionTypes/shared";
const SIZE = 275;
const SIZE = 25;
interface Props {
isModalOpen: boolean;
isTryRemoveAccount: boolean;
quizID: number;
questions: AnyTypedQuizQuestion[]
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[]
}
const FCTranslate = {
@ -30,6 +31,13 @@ const FCTranslate = {
"text": "номер",
"address": "адрес",
}
let isReadyGetPipeline = true;
let isReadyGetPipelineStep = true;
let isReadyGetUsers = true;
let isReadyGetTags = true;
let isReadyGetFields = true;
export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, questions }: Props) => {
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(true);
const [firstRules, setFirstRules] = useState<boolean>(false);
@ -101,15 +109,15 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
if (gottenList !== null) {
Object.keys(gottenList.QuestionID).forEach((qId) => {
const q = questions.find(e=>e.backendId === Number(qId)) || {}
const q = questions.find(e => e.backendId === Number(qId)) || {}
gottenQuestions[key as QuestionKeys].push({
id: qId,
title: q.title,
entity: key,
})
})
})
}
if (key === "Contact") {
@ -153,8 +161,8 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
useEffect(() => {
const transletedQuestions = {}
Object.keys(selectedQuestions).forEach((column) => {
Object.keys(selectedQuestions)?.forEach((column) => {
selectedQuestions[column].forEach((minifiedData) => {
const q = questions.find(e => e.backendId === Number(minifiedData.id)) || {}
transletedQuestions[column] = {
@ -168,115 +176,142 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
}, [questions])
useEffect(() => {
getPipelines({
page: pageOfPipelines,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedPipelines: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedPipelines.push({
id: step.AmoID.toString(),
title: step.Name,
});
});
setArrayOfPipelines((prevItems) => [...prevItems, ...minifiedPipelines]);
setPageOfPipelinesSteps(1);
}
});
}, [pageOfPipelines]);
useEffect(() => {
const oldData = pageOfPipelinesSteps === 1 ? [] : arrayOfPipelinesSteps;
if (selectedPipeline !== null)
getSteps({
page: pageOfPipelinesSteps,
if (isReadyGetPipeline) {
getPipelines({
page: pageOfPipelines,
size: SIZE,
pipelineId: Number(selectedPipeline),
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedSteps: MinifiedData[] = [];
const minifiedPipelines: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedSteps.push({
minifiedPipelines.push({
id: step.AmoID.toString(),
title: step.Name,
});
});
setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]);
setArrayOfPipelines((prevItems) => [...prevItems, ...minifiedPipelines]);
setPageOfPipelinesSteps(1);
} else {
isReadyGetPipeline = false
}
});
}
}, [pageOfPipelines]);
useEffect(() => {
if (isReadyGetPipelineStep) {
const oldData = pageOfPipelinesSteps === 1 ? [] : arrayOfPipelinesSteps;
if (selectedPipeline !== null)
getSteps({
page: pageOfPipelinesSteps,
size: SIZE,
pipelineId: Number(selectedPipeline),
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedSteps: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedSteps.push({
id: step.AmoID.toString(),
title: step.Name,
});
});
setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]);
} else {
isReadyGetPipelineStep = false
}
});
}
}, [selectedPipeline, pageOfPipelinesSteps]);
useEffect(() => {
getUsers({
page: pageOfUsers,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedUsers: MinifiedData[] = [];
if (isReadyGetUsers) {
getUsers({
page: pageOfUsers,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedUsers: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedUsers.push({
id: step.amoUserID.toString(),
title: step.name,
response.items.forEach((step) => {
minifiedUsers.push({
id: step.amoUserID.toString(),
title: step.name,
});
});
});
setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
}
});
setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
} else {
isReadyGetUsers = false
}
});
}
}, [pageOfUsers]);
useEffect(() => {
getTags({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
if (isReadyGetTags) {
getTags({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedTags.push({
id: step.AmoID.toString(),
title: step.Name,
entity:
step.Entity === "leads"
? "Lead"
: step.Entity === "contacts"
? "Contact"
: step.Entity === "companies"
? "Company"
: "Customer",
response.items.forEach((step) => {
minifiedTags.push({
id: step.AmoID.toString(),
title: step.Name,
entity:
step.Entity === "leads"
? "Lead"
: step.Entity === "contacts"
? "Contact"
: step.Entity === "companies"
? "Company"
: "Customer",
});
});
});
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
}
});
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
} else {
isReadyGetTags = false
}
});
}
}, [pageOfTags]);
useEffect(() => {
getFields({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
if (isReadyGetFields) {
getFields({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
response.items.forEach((field) => {
minifiedTags.push({
id: field.AmoID.toString(),
title: field.Name,
entity:
field.Entity === "leads"
? "Lead"
: field.Entity === "contacts"
? "Contact"
: field.Entity === "companies"
? "Company"
: "Customer",
response.items.forEach((field) => {
minifiedTags.push({
id: field.AmoID.toString(),
title: field.Name,
entity:
field.Entity === "leads"
? "Lead"
: field.Entity === "contacts"
? "Contact"
: field.Entity === "companies"
? "Company"
: "Customer",
});
});
});
setArrayOfFields((prevItems) => [...prevItems, ...minifiedTags]);
}
});
setArrayOfFields((prevItems) => [...prevItems, ...minifiedTags]);
}
});
} else {
isReadyGetFields = false
}
}, [pageOfFields]);
useEffect(() => () => {
isReadyGetPipeline = true;
isReadyGetPipelineStep = true;
isReadyGetUsers = true;
isReadyGetTags = true;
isReadyGetFields = true;
}, [])
return {
isLoadingPage,
@ -299,10 +334,11 @@ export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, que
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
setPageOfPipelines: () => setPageOfPipelines(old => old + 1),
setPageOfPipelinesSteps: () => setPageOfPipelinesSteps(old => old + 1),
setPageOfUsers: () => setPageOfUsers(old => old + 1),
setPageOfTags: () => setPageOfTags(old => old + 1),
setPageOfFields: () => setPageOfFields(old => old + 1),
setSelectedCurrentFields,
};
};

@ -16,7 +16,7 @@ const AnalyticsModal = lazy(() =>
);
const AmoCRMModal = lazy(() =>
import("../IntegrationsModal/Amo/AmoCRMModal").then((module) => ({
import("../IntegrationsModal/Amo").then((module) => ({
default: module.AmoCRMModal,
}))
);

10
src/utils/throttle.ts Normal file

@ -0,0 +1,10 @@
export function determineScrollBottom(e: any, callback: any) {
const scrollHeight = e.target.scrollHeight;
const scrollTop = e.target.scrollTop;
const clientHeight = e.target.clientHeight;
const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) >= 0.9;
if (scrolledToBottom) {
callback();
}
}

@ -1513,10 +1513,10 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5"
integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==
"@frontend/kitui@^1.0.84":
version "1.0.84"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.84.tgz#9b655b776433fb8ea6d0840897d941e66530df7f"
integrity sha1-m2Vbd2Qz+46m0IQIl9lB5mUw338=
"@frontend/kitui@^1.0.85":
version "1.0.85"
resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.85.tgz#1a384c9ff3314175c1ba3d35d0979da7026a21ab"
integrity sha1-GjhMn/MxQXXBuj010JedpwJqIas=
dependencies:
immer "^10.0.2"
reconnecting-eventsource "^1.6.2"