This commit is contained in:
Nastya 2025-10-27 20:41:39 +03:00
parent 639929d825
commit 6ceaf7e8bc
43 changed files with 4175 additions and 620 deletions

@ -1,4 +1,4 @@
1.0.14 _ 2025-10-20 _ utm
1.0.14 _ 2025-10-20 _ логика overtime для публички
1.0.13 _ 2025-10-18 _ Визуал utm + логика
1.0.12 _ 2025-10-12 _ ютм с дизайном и беком, но без логики
1.0.11 _ 2025-10-06 _ Merge branch 'staging'

@ -1,577 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QUIZ Service API Documentation</title>
<style>
:root {
--primary-color: #7E2AEA;
--text-color: #333;
--bg-color: #fff;
--border-color: #e0e0e0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: var(--text-color);
margin: 0;
padding: 0;
background: var(--bg-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: var(--primary-color);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
h1, h2, h3 {
color: var(--primary-color);
}
.endpoint {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background: #fafafa;
}
.method {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
color: white;
font-weight: bold;
margin-right: 10px;
}
.get { background: #61affe; }
.post { background: #49cc90; }
.put { background: #fca130; }
.delete { background: #f93e3e; }
.patch { background: #50e3c2; }
.schema {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}
.nav {
position: sticky;
top: 0;
background: white;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.nav a {
color: var(--primary-color);
text-decoration: none;
margin-right: 20px;
}
.nav a:hover {
text-decoration: underline;
}
code {
background: #f1f1f1;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
}
.response {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-left: 4px solid var(--primary-color);
}
.components {
margin: 2rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.security {
background: #fff3cd;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.parameter {
margin: 0.5rem 0;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 4px;
}
.parameter.required {
border-left: 4px solid #dc3545;
}
.enum-values {
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>QUIZ Service API Documentation</h1>
<p>Version 1.0.0</p>
</div>
</header>
<nav class="nav">
<div class="container">
<a href="#components">Components</a>
<a href="#quiz">Quiz Endpoints</a>
<a href="#question">Question Endpoints</a>
<a href="#results">Results Endpoints</a>
<a href="#statistics">Statistics Endpoints</a>
<a href="#account">Account Endpoints</a>
</div>
</nav>
<main class="container">
<section id="components">
<h2>Components</h2>
<div class="components">
<h3>Quiz Model</h3>
<div class="schema">
<pre><code>{
"id": integer, // Id of created quiz
"qid": string, // string id for customers
"deleted": boolean, // true if quiz deleted
"archived": boolean, // true if quiz archived
"fingerprinting": boolean, // set true for save deviceId
"repeatable": boolean, // set true for allow user to repeat quiz
"note_prevented": boolean, // set true for save statistic of incomplete quiz passing
"mail_notifications": boolean, // set true for mail notification for each quiz passing
"unique_answers": boolean, // set true for save statistics only for unique quiz passing
"name": string, // name of quiz. max 280 length
"description": string, // description of quiz
"config": string, // config of quiz. serialized json for rules of quiz flow
"status": string, // status of quiz. allow only '', 'draft', 'template', 'stop', 'start'
"limit": integer, // limit is count of max quiz passing
"due_to": integer, // last time when quiz is valid. timestamp in seconds
"time_of_passing": integer, // seconds to pass quiz
"pausable": boolean, // true if it is allowed for pause quiz
"version": integer, // version of quiz
"version_comment": string, // version comment to version of quiz
"parent_ids": integer[], // array of previous versions of quiz
"created_at": string, // time of creating
"updated_at": string, // time of last updating
"question_cnt": integer, // count of questions
"passed_count": integer, // count passings
"average_time": integer, // average time of passing
"super": boolean, // set true if squiz realize group functionality
"group_id": integer // group of new quiz
}</code></pre>
</div>
</div>
<div class="components">
<h3>Question Model</h3>
<div class="schema">
<pre><code>{
"id": integer, // Id of created question
"quiz_id": integer, // relation to quiz
"title": string, // title of question. max 512 length
"description": string, // description of question
"type": string, // status of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating
"required": boolean, // user must pass this question
"deleted": boolean, // true if question is deleted
"page": integer, // page if question
"content": string, // serialized json of created question
"version": integer, // version of quiz
"parent_ids": integer[], // array of previous versions of quiz
"created_at": string, // time of creating
"updated_at": string // time of last updating
}</code></pre>
</div>
</div>
<div class="components">
<h3>Answer Model</h3>
<div class="schema">
<pre><code>{
"Id": integer, // id ответа
"Content": string, // контент ответа
"QuestionId": integer, // id вопроса к которому ответ
"QuizId": integer, // id опроса к которому ответ
"Fingerprint": string, // fingerprint
"Session": string, // сессия
"Result": boolean, // true or false?
"CreatedAt": string, // таймшап когда ответ создан
"New": boolean, // новый ответ?
"Deleted": boolean // удален?
}</code></pre>
</div>
</div>
<div class="components">
<h3>LeadTarget Model</h3>
<div class="schema">
<pre><code>{
"ID": integer, // primary key
"AccountID": string, // account identifier
"Type": string, // type of target (mail, telegram, whatsapp)
"QuizID": integer, // ID of the quiz
"Target": string, // target address
"InviteLink": string, // invitation link
"Deleted": boolean, // is deleted
"CreatedAt": string // creation timestamp
}</code></pre>
</div>
</div>
<div class="components">
<h3>TgAccount Model</h3>
<div class="schema">
<pre><code>{
"ID": integer, // primary key
"ApiID": integer, // Telegram API ID
"ApiHash": string, // Telegram API Hash
"PhoneNumber": string, // phone number
"Password": string, // account password
"Status": string, // account status (active, inactive, ban)
"Deleted": boolean, // is deleted
"CreatedAt": string // creation timestamp
}</code></pre>
</div>
</div>
</section>
<section id="quiz">
<h2>Quiz Endpoints</h2>
<div class="endpoint">
<h3>Create Quiz</h3>
<span class="method post">POST</span>
<code>/quiz/create</code>
<p>Create a new quiz with specified parameters.</p>
<div class="security">
<h4>Security</h4>
<p>This endpoint requires authentication.</p>
</div>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"fingerprinting": boolean, // set true for save deviceId
"repeatable": boolean, // set true for allow user to repeat quiz
"note_prevented": boolean, // set true for save statistic of incomplete quiz passing
"mail_notifications": boolean, // set true for mail notification for each quiz passing
"unique_answers": boolean, // set true for save statistics only for unique quiz passing
"name": string, // name of quiz. max 280 length
"description": string, // description of quiz
"config": string, // config of quiz. serialized json for rules of quiz flow
"status": string, // status of quiz. allow only '', 'draft', 'template', 'stop', 'start'
"limit": integer, // limit is count of max quiz passing
"due_to": integer, // last time when quiz is valid. timestamp in seconds
"time_of_passing": integer, // seconds to pass quiz
"pausable": boolean, // true if it is allowed for pause quiz
"question_cnt": integer, // count of questions
"super": boolean, // set true if squiz realize group functionality
"group_id": integer // group of new quiz
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>201 Created</h5>
<p>Quiz successfully created. Returns the created quiz object.</p>
<div class="schema">
<pre><code>{
"id": integer,
"qid": string,
"name": string,
"description": string,
// ... other quiz properties
}</code></pre>
</div>
</div>
<div class="response">
<h5>422 Unprocessable Entity</h5>
<p>Name field should have less than 280 characters.</p>
</div>
<div class="response">
<h5>406 Not Acceptable</h5>
<p>Status on creating must be only draft, template, stop, start or due to time must be lesser than now.</p>
<div class="enum-values">
Allowed status values: '', 'draft', 'template', 'stop', 'start'
</div>
</div>
<div class="response">
<h5>409 Conflict</h5>
<p>You can pause quiz only if it has deadline for passing.</p>
</div>
<div class="response">
<h5>500 Internal Server Error</h5>
<p>If you get any content string send it to developer.</p>
</div>
</div>
<div class="endpoint">
<h3>Get Quiz List</h3>
<span class="method post">POST</span>
<code>/quiz/getList</code>
<p>Get paginated list of quizzes with filtering options.</p>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"limit": integer,
"offset": integer,
"from": integer,
"to": integer,
"search": string,
"status": string,
"deleted": boolean,
"archived": boolean,
"super": boolean,
"group_id": integer
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns list of quizzes with total count.</p>
<div class="schema">
<pre><code>{
"count": integer,
"items": [
{
"id": integer,
"qid": string,
// ... other quiz properties
}
]
}</code></pre>
</div>
</div>
<div class="response">
<h5>406 Not Acceptable</h5>
<p>Inappropriate status, allowed only '', 'stop', 'start', 'draft', 'template', 'timeout', 'offlimit'.</p>
</div>
<div class="response">
<h5>500 Internal Server Error</h5>
<p>If you get any content string send it to developer.</p>
</div>
</div>
<!-- Add more quiz endpoints -->
</section>
<section id="question">
<h2>Question Endpoints</h2>
<div class="endpoint">
<h3>Create Question</h3>
<span class="method post">POST</span>
<code>/question/create</code>
<p>Create a new question for a quiz.</p>
</div>
<!-- Add more question endpoints -->
</section>
<section id="results">
<h2>Results Endpoints</h2>
<div class="endpoint">
<h3>Get Quiz Results</h3>
<span class="method post">POST</span>
<code>/results/getResults/{quizId}</code>
<p>Get list of quiz results with pagination.</p>
<h4>Path Parameters:</h4>
<div class="parameter">
<code>quizId</code> - ID of the quiz to get results for
</div>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"to": integer, // таймштамп времени, до которого выбирать статистику. если 0 или не передано - этого ограничения нет
"from": integer, // таймштамп времени, после которого выбирать статистику. если 0 или не передано - этого ограничения нет
"new": boolean, // флаг, по которому вернутся только новые результаты, ещё не просмотренные пользователем
"page": integer, // номер страницы для пагинации
"limit": integer // размер страницы для пагинации
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns paginated list of results.</p>
<div class="schema">
<pre><code>{
"total_count": integer, // общее количество элементов удволетворяющее фильтру
"results": [
{
"content": string, // содержимое ответа
"id": integer, // айдишник ответа
"new": boolean, // статус, был ли просмотрен ответ
"created_at": string // время создания этого результата
}
]
}</code></pre>
</div>
</div>
</div>
<div class="endpoint">
<h3>Export Results</h3>
<span class="method post">POST</span>
<code>/results/{quizID}/export</code>
<p>Export quiz results to CSV format.</p>
<h4>Path Parameters:</h4>
<div class="parameter required">
<code>quizID</code> - ID of the quiz to export results from
</div>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"to": string, // Дата окончания диапазона времени для экспорта результатов
"from": string, // Дата начала временного диапазона для экспорта результатов
"new": boolean // Экспортировать ли только новые результаты?
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns CSV file with quiz results.</p>
<div class="schema">
<pre><code>Content-Type: text/csv</code></pre>
</div>
</div>
</div>
</section>
<section id="telegram">
<h2>Telegram Endpoints</h2>
<div class="endpoint">
<h3>Create Telegram Account</h3>
<span class="method post">POST</span>
<code>/telegram/create</code>
<p>Authorize server in Telegram account.</p>
<h4>Request Body:</h4>
<div class="schema">
<pre><code>{
"ApiID": integer, // Telegram API ID
"ApiHash": string, // Telegram API Hash
"PhoneNumber": string, // Phone number
"Password": string // Account password
}</code></pre>
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns signature for code verification.</p>
<div class="schema">
<pre><code>{
"signature": string // Session identifier for code verification
}</code></pre>
</div>
</div>
<div class="response">
<h5>409 Conflict</h5>
<p>Account already exists and is active.</p>
</div>
</div>
</section>
<section id="audience">
<h2>Audience Endpoints</h2>
<div class="endpoint">
<h3>Create Quiz Audience</h3>
<span class="method post">POST</span>
<code>/quiz/{quizID}/auditory</code>
<p>Create audience for a quiz.</p>
<h4>Path Parameters:</h4>
<div class="parameter required">
<code>quizID</code> - ID of the quiz
</div>
<h4>Responses:</h4>
<div class="response">
<h5>200 OK</h5>
<p>Returns ID of created audience.</p>
<div class="schema">
<pre><code>{
"id": integer // ID of created auditory
}</code></pre>
</div>
</div>
</div>
</section>
<section id="statistics">
<h2>Statistics Endpoints</h2>
<div class="endpoint">
<h3>Get Question Statistics</h3>
<span class="method post">POST</span>
<code>/statistic/{quizID}/questions</code>
<p>Get statistics for specific questions in a quiz.</p>
</div>
<!-- Add more statistics endpoints -->
</section>
<section id="account">
<h2>Account Endpoints</h2>
<div class="endpoint">
<h3>Add Lead Target</h3>
<span class="method post">POST</span>
<code>/account/leadtarget</code>
<p>Add a target destination for lead notifications.</p>
</div>
<!-- Add more account endpoints -->
</section>
</main>
<footer class="container">
<p>© 2024 QUIZ Service API Documentation</p>
</footer>
</body>
</html>

BIN
src.zip

Binary file not shown.

@ -0,0 +1,374 @@
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/Amo/types";
import { makeRequest } from "@frontend/kitui";
import { useToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import useSWR from "swr";
export type PaginationRequest = {
page: number;
size: number;
};
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/bitrix`;
// получение информации об аккаунте
export type AccountResponse = {
id: number;
accountID: string;
amoID: number;
name: string;
deleted: boolean;
createdAt: string;
subdomain: string;
country: string;
driveURL: string;
stale: boolean;
};
export const getAccount = async (): Promise<[AccountResponse | null, string?]> => {
try {
const response = await makeRequest<void, AccountResponse>({
method: "GET",
url: `${API_URL}/account`,
useToken: true,
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, ""];
}
};
export function useBitrixAccount() {
const token = useToken();
return useSWR(token ? "bitrixAccount" : null, () =>
makeRequest<void, AccountResponse>({
method: "GET",
url: `${API_URL}/account`,
useToken: true,
})
);
}
// подключить Bitrix
export const connectBitrix = async (): Promise<[string | null, string?]> => {
try {
const response = await makeRequest<void, { link: string }>({
method: "POST",
url: `${API_URL}/account`,
useToken: true,
withCredentials: true,
});
return [response.link];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось подключить аккаунт. ${error}`];
}
};
// получение токена
export type TokenPair = {
accessToken: string;
refreshToken: string;
};
export const getTokens = async (): Promise<[TokenPair | null, string?]> => {
try {
const response = await makeRequest<void, TokenPair>({
method: "GET",
url: `${API_URL}/webhook/create`,
useToken: true,
});
return [response];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Failed to get tokens. ${error}`];
}
};
//получение списка тегов
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[];
};
export const getTags = async ({ page, size }: PaginationRequest): Promise<[TagsResponse | null, string?]> => {
try {
const tagsResponse = await makeRequest<PaginationRequest, TagsResponse>({
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;
amoID: number;
name: string;
email: string;
role: number;
group: number;
deleted: boolean;
createdAt: string;
amoUserID: number;
};
export type UsersResponse = {
count: number;
items: User[];
};
export const getUsers = async ({ page, size }: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
try {
const usersResponse = await makeRequest<PaginationRequest, UsersResponse>({
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}`];
}
};
//получение списка воронок
export type Pipeline = {
ID: number;
AmoID: number;
AccountID: number;
Name: string;
IsArchive: boolean;
Deleted: boolean;
CreatedAt: number;
};
export type PipelinesResponse = {
count: number;
items: Pipeline[];
};
export const getPipelines = async ({ page, size }: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => {
try {
const pipelinesResponse = await makeRequest<PaginationRequest, PipelinesResponse>({
method: "GET",
url: `${API_URL}/pipelines?page=${page}&size=${size}`,
});
return [pipelinesResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список воронок. ${error}`];
}
};
//получение настроек интеграции
export type QuestionID = Record<string, number>;
export type IntegrationRules = {
PipelineID: number;
StepID: number;
PerformerID?: number;
FieldsRule: FieldsRule;
TagsToAdd: {
Lead: number[] | null;
Contact: number[] | null;
Company: number[] | null;
Customer: number[] | null;
};
};
export type FieldsRule = Record<Partial<QuestionKeys>, null | [{ QuestionID: QuestionID }]>;
export const getIntegrationRules = async (quizID: string): Promise<[IntegrationRules | null, string?]> => {
try {
const settingsResponse = await makeRequest<void, IntegrationRules>({
method: "GET",
url: `${API_URL}/rules/${quizID}`,
});
return [settingsResponse || null];
} catch (nativeError) {
if (nativeError.response?.status === 404) return [null, "first"];
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить настройки интеграции. ${error}`];
}
};
//обновление настроек интеграции
export type IntegrationRulesUpdate = {
PerformerID: number;
PipelineID: number;
StepID: number;
Utms: number[];
FieldsRule: {
Lead: { QuestionID: number }[];
Contact: { ContactRuleMap: string }[];
Company: { QuestionID: number }[];
Customer: { QuestionID: number }[];
};
};
export const setIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
method: "POST",
url: `${API_URL}/rules/${quizID}`,
body: settings,
});
return [updateResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Failed to update integration settings. ${error}`];
}
};
export const updateIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
method: "PATCH",
url: `${API_URL}/rules/${quizID}`,
body: settings,
});
return [updateResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Failed to update integration settings. ${error}`];
}
};
// Получение кастомных полей
export type CustomField = {
ID: number;
AmoID: number;
Code: string;
AccountID: number;
Name: string;
EntityType: string;
Type: string;
Deleted: boolean;
CreatedAt: number;
};
export type Field = {
ID: number;
AmoID: number;
Code: string;
AccountID: number;
Name: string;
Entity: string;
Type: string;
Deleted: boolean;
CreatedAt: number;
};
export type CustomFieldsResponse = {
count: number;
items: CustomField[];
};
export type FieldsResponse = {
count: number;
items: Field[];
};
export const getCustomFields = async (
pagination: PaginationRequest
): Promise<[CustomFieldsResponse | null, string?]> => {
try {
const customFieldsResponse = await makeRequest<PaginationRequest, CustomFieldsResponse>({
method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
});
return [customFieldsResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список кастомных полей. ${error}`];
}
};
export const getFields = async (pagination: PaginationRequest): Promise<[FieldsResponse | null, string?]> => {
try {
const fieldsResponse = await makeRequest<PaginationRequest, FieldsResponse>({
method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
});
return [fieldsResponse, ""];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось получить список полей. ${error}`];
}
};
// Отвязать аккаунт Bitrix от публикации
export const removeBitrixAccount = async (): Promise<[void | null, string?]> => {
try {
await makeRequest<void>({
method: "DELETE",
url: `${API_URL}/account`,
});
return [null, ""];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отвязать аккаунт. ${error}`];
}
};

@ -0,0 +1,21 @@
import { Box, SxProps, Theme } from "@mui/material";
interface Props {
sx?: SxProps<Theme>;
}
export default function Bitrix({ sx }: Props) {
return (
<Box
sx={sx}
>
<svg data-logo="" width="195" height="35" viewBox="0 0 195 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.755 22.17v-7.991h1.921c1.72 0 3.136.2 4.146.81 1.01.605 1.62 1.62 1.62 3.235 0 2.73-1.62 3.946-5.461 3.946H4.755zM.1 26.01h6.776c7.587 0 10.321-3.34 10.321-7.991 0-3.136-1.315-5.26-3.64-6.476-1.82-1.01-4.146-1.315-6.981-1.315h-1.82v-6.07h10.216L16.187.412H0v25.594h.1v.004zm20.233 0h4.45l5.766-8.296c1.115-1.516 1.921-3.035 2.43-3.845h.1c-.1 1.115-.2 2.53-.2 3.945v8.092h4.551V7.703h-4.45l-5.766 8.296c-1.01 1.516-1.921 3.035-2.43 3.845h-.1c.1-1.114.2-2.53.2-3.945V7.807h-4.551V26.02v-.008zm24.888 0h4.655V11.544h5.461L56.552 7.7H39.656v3.845h5.565v14.467zm17.502 9.206v-9.206c.91.305 1.82.405 2.83.405 5.767 0 9.512-3.945 9.512-9.611s-3.44-9.611-10.016-9.611c-2.53 0-4.956.505-6.981 1.114l.1 26.91h4.555zm0-13.151V11.344c.71-.2 1.315-.305 2.125-.305 3.34 0 5.461 1.82 5.461 5.766 0 3.54-1.72 5.766-5.16 5.766-.91 0-1.62-.2-2.43-.506h.004zm15.072 3.945h4.451l5.766-8.296c1.115-1.516 1.92-3.035 2.43-3.845h.1c-.1 1.115-.2 2.53-.2 3.945v8.092h4.55V7.703h-4.45l-5.766 8.296c-1.01 1.516-1.92 3.035-2.43 3.845h-.1c.1-1.114.2-2.53.2-3.945V7.807h-4.55V26.02v-.008zm20.742 0h4.656v-7.586h2.73c.506 0 1.011.505 1.62 1.72l2.326 5.866h4.956l-3.34-6.98c-.606-1.216-1.215-1.921-2.126-2.226v-.1c1.516-.91 1.721-3.54 2.631-4.856.305-.405.71-.606 1.315-.606.305 0 .71 0 1.01.2V7.499c-.505-.2-1.415-.304-1.92-.304-1.62 0-2.631.605-3.34 1.62-1.516 2.225-1.516 6.07-3.745 6.07h-2.126V7.703h-4.655v18.312l.008-.004zm26.605.405c2.53 0 4.855-.81 6.271-1.82l-1.316-3.136c-1.315.71-2.53 1.215-4.25 1.215-3.135 0-5.16-2.025-5.16-5.766 0-3.34 2.025-5.97 5.461-5.97 1.82 0 3.135.505 4.45 1.415V8.41c-1.01-.606-2.63-1.215-4.955-1.215-5.462 0-9.712 4.045-9.712 9.811 0 5.26 3.236 9.407 9.206 9.407l.005.004z" fill="#0BBBEF">
</path><path d="M185.084 19.828c-5.461 0-9.812-4.351-9.916-9.916 0-5.462 4.451-9.916 9.916-9.916 5.465 0 9.916 4.45 9.916 9.916 0 5.465-4.451 9.915-9.916 9.915zm0-17.41c-4.121 0-7.494 3.373-7.494 7.494.092 4.216 3.373 7.494 7.494 7.494 4.121 0 7.494-3.374 7.494-7.494 0-4.121-3.373-7.495-7.494-7.495z" fill="#005893">
</path><path d="M183.827 4.839h1.933v6.521h-1.933V4.84z" fill="#005893">
</path><path d="M190.353 9.427v1.933h-6.521V9.427h6.521zM134.047 26.011h17.807v-3.945h-11.736c1.62-6.476 11.532-7.891 11.532-15.073 0-3.845-2.631-6.676-8.196-6.676-3.441 0-6.476 1.01-8.497 2.025l1.215 3.64c1.821-.91 3.946-1.72 6.576-1.72 2.025 0 3.946.91 3.946 3.236 0 5.261-11.637 5.666-12.647 18.513zm29.74-6.271v6.271h4.551V19.74h3.845v-3.845h-3.845V.317h-3.341l-12.646 16.388v3.035h11.436zm-6.271-3.64 6.475-8.702c0 .71-.2 2.935-.2 4.956v3.64h-3.036c-.91 0-2.63.101-3.235.101l-.004.004z" fill="#005893">
</path>
</svg>
</Box>
)
}

@ -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/bitrixIntegration";
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}/dashboard/`)}
{infoItemLink("Профиль пользователя в amo", `https://${accountInfo?.subdomain}/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>
);
};

@ -0,0 +1,148 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { AmoButton } from "../../../../components/AmoButton/AmoButton";
import { connectBitrix } from "@/api/bitrixIntegration";
type IntegrationStep1Props = {
handleNextStep: () => void;
};
// interface Values {
// login: string;
// password: string;
// }
//
// const initialValues: Values = {
// login: "",
// password: "",
// };
//
// const validationSchema = object({
// login: string().required("Поле обязательно"),
// password: string().required("Поле обязательно").min(8, "Минимум 8 символов"),
// });
export const AmoLogin: FC<IntegrationStep1Props> = ({ handleNextStep }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const onAmoClick = async () => {
const [url, error] = await connectBitrix();
if (url && !error) {
window.open(url, "_blank");
}
};
// const formik = useFormik<Values>({
// initialValues,
// validationSchema,
// onSubmit: async (values, formikHelpers) => {
// const loginTrimmed = values.login.trim();
// const passwordTrimmed = values.password.trim();
// try {
// // Simulate a network request
// await new Promise((resolve) => setTimeout(resolve, 2000));
// handleNextStep();
// } catch (error) {
// formikHelpers.setSubmitting(false);
// if (error instanceof Error) {
// formikHelpers.setErrors({
// login: error.message,
// password: error.message,
// });
// }
// }
// },
// });
return (
<Box
// component="form"
// onSubmit={formik.handleSubmit}
// noValidate
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
{/*<Box*/}
{/* sx={{*/}
{/* marginTop: "68px",*/}
{/* width: isMobile ? "100%" : "500px",*/}
{/* display: "flex",*/}
{/* flexDirection: "column",*/}
{/* alignItems: "center",*/}
{/* gap: "15px",*/}
{/* }}*/}
{/*>*/}
{/* <InputTextfield*/}
{/* TextfieldProps={{*/}
{/* value: formik.values.login,*/}
{/* placeholder: "+7 900 000 00 00 или username@penahaub.com",*/}
{/* onBlur: formik.handleBlur,*/}
{/* error: formik.touched.login && Boolean(formik.errors.login),*/}
{/* helperText: formik.touched.login && formik.errors.login,*/}
{/* "data-cy": "login",*/}
{/* }}*/}
{/* onChange={formik.handleChange}*/}
{/* color={theme.palette.background.default}*/}
{/* id="login"*/}
{/* label="Телефон или E-mail"*/}
{/* gap="10px"*/}
{/* />*/}
{/* <PasswordInput*/}
{/* TextfieldProps={{*/}
{/* value: formik.values.password,*/}
{/* placeholder: "Не менее 8 символов",*/}
{/* onBlur: formik.handleBlur,*/}
{/* error: formik.touched.password && Boolean(formik.errors.password),*/}
{/* helperText: formik.touched.password && formik.errors.password,*/}
{/* type: "password",*/}
{/* "data-cy": "password",*/}
{/* }}*/}
{/* onChange={formik.handleChange}*/}
{/* color={theme.palette.background.default}*/}
{/* id="password"*/}
{/* label="Пароль"*/}
{/* gap="10px"*/}
{/* />*/}
{/*</Box>*/}
<Box sx={{ marginTop: "70px", width: isMobile ? "100%" : "500px" }}>
<Typography
sx={{
fontSize: "16px",
fontWeight: "400",
color: theme.palette.grey2.main,
lineHeight: "1",
}}
>
Инструкция
</Typography>
<Typography
sx={{
marginTop: "12px",
fontSize: "18px",
fontWeight: "400",
color: theme.palette.grey3.main,
lineHeight: "1",
}}
>
После нажатия на кнопку - "Подключить", вас переадресует на страницу подключения интеграции в ваш аккаунт
Bitrix24. Пожалуйста, согласитесь на всё, что мы предлагаем, иначе чуда не случится.
</Typography>
</Box>
<Box sx={{ marginTop: "50px" }}>
<AmoButton onClick={onAmoClick} />
</Box>
{/*<StepButtonsBlock*/}
{/* isSmallBtnDisabled={true}*/}
{/* largeBtnType={"submit"}*/}
{/* // isLargeBtnDisabled={formik.isSubmitting}*/}
{/* largeBtnText={"Войти"}*/}
{/*/>*/}
</Box>
);
};

@ -0,0 +1,115 @@
import { connectBitrix } from "@/api/bitrixIntegration";
import { setTryShowAmoTokenExpiredDialog } from "@/stores/uiTools/actions";
import { useUiTools } from "@/stores/uiTools/store";
import CustomCheckbox from "@/ui_kit/CustomCheckbox";
import { Box, Button, Dialog, Typography, useTheme } from "@mui/material";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
const HIDE_DIALOG_EXPIRATION_PERIOD = 24 * 60 * 60 * 1000;
interface Props {
isAmoTokenExpired: boolean;
}
export default function AmoTokenExpiredDialog({ isAmoTokenExpired }: Props) {
const theme = useTheme();
const tryShowAmoTokenExpiredDialog = useUiTools((state) => state.tryShowAmoTokenExpiredDialog);
const [isHideDialogForADayChecked, setIsHideDialogForADayChecked] = useState<boolean>(false);
// const { hash, pathname, search } = useLocation();
const location = useLocation();
const onAmoClick = async () => {
const [url, error] = await connectBitrix();
if (url && !error) {
window.open(url, "_blank");
}
};
function handleDialogClose() {
if (isHideDialogForADayChecked) {
const expirationDate = Date.now() + HIDE_DIALOG_EXPIRATION_PERIOD;
localStorage.setItem("hideAmoTokenExpiredDialogExpirationTime", expirationDate.toString());
}
setTryShowAmoTokenExpiredDialog(false);
}
useEffect(() => {
setTryShowAmoTokenExpiredDialog(true);
}, [location]);
return (
<Dialog
open={isAmoTokenExpired && tryShowAmoTokenExpiredDialog && location.pathname !== "/"}
onClose={handleDialogClose}
PaperProps={{
sx: {
borderRadius: "12px",
maxWidth: "620px",
},
}}
>
<Box
sx={{
p: "20px",
backgroundColor: "#F2F3F7",
}}
>
<Typography
color="#4D4D4D"
fontSize="24px"
fontWeight="medium"
>
Ваш bitrix-токен не работает
</Typography>
</Box>
<Box
sx={{
p: "20px",
display: "flex",
flexDirection: "column",
gap: "30px",
}}
>
<Typography color="#4D4D4D">
Bitrix отозвал ваш токен. Зайдите заново в свой аккаунт, чтобы вам снова начали приходить сделки.
</Typography>
<Box
sx={{
display: "flex",
gap: "10px",
[theme.breakpoints.down("sm")]: {
flexDirection: "column-reverse",
},
}}
>
<Button
variant="outlined"
onClick={handleDialogClose}
sx={{
flex: "1 0 0",
borderColor: "#9A9AAF",
}}
>
Позже
</Button>
<Button
variant="contained"
onClick={onAmoClick}
sx={{
flex: "1 0 0",
}}
>
Перелогиниться
</Button>
</Box>
<CustomCheckbox
label={"Не показывать сутки"}
checked={isHideDialogForADayChecked}
handleChange={({ target }) => setIsHideDialogForADayChecked(target.checked)}
/>
</Box>
</Dialog>
);
}

@ -0,0 +1,69 @@
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 }) => {
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>
);
};

@ -0,0 +1,62 @@
import { useState } from "react"
import { IconButton, Input, useMediaQuery, useTheme } from "@mui/material"
import Trash from "@/assets/icons/trash"
import { useDebouncedCallback } from "use-debounce"
interface Props {
isError: boolean
constrictor: (text: string) => void
}
export const DataConstrictor = ({
isError,
constrictor,
}: Props) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const [text, setText] = useState("")
const errCalc = isError && text.length > 0
const debouncedTestHC = useDebouncedCallback(
(value: string) => {
constrictor(value)
},
700
);
return <Input
value={text}
placeholder="быстрый поиск"
disableUnderline
sx={{
bgcolor: isError ? "#e6bbbb" : "#f2f3f7",
borderRadius: "10px",
maxWidth: isMobile ? "200px" : "300px",
p: "0 5px 0 15px",
mt: "10px",
"&:hover": {
bgcolor: errCalc ? "#e6bbbb" : "#ececec"
}
}}
endAdornment={
text.length > 0
?
<IconButton
onClick={() => {
setText("")
constrictor("")
}}
>
<Trash />
</IconButton>
:
null
}
onChange={({ target }) => {
setText(target.value)
debouncedTestHC(target.value)
}}
/>
}

@ -0,0 +1,74 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "./StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { MinifiedData } from "./types";
import { ModalTitle } from "./ModalTitle";
type Props = {
users: MinifiedData[];
handlePrevStep: () => void;
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> = ({
users,
handlePrevStep,
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%", zIndex: 3 }}>
<ModalTitle
{...titleProps}
/>
<CustomSelect
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={onScrollUsers}
/>
</Box>
<Box
sx={{
marginTop: "auto",
alignSelf: "end",
}}
>
<StepButtonsBlock
onLargeBtnClick={handleNextStep}
onSmallBtnClick={handlePrevStep}
/>
</Box>
</Box>
</>
);
};

@ -0,0 +1,50 @@
import { FC } from "react";
import { Button, Typography, useTheme, Box } from "@mui/material";
interface Props {
deleteItem: () => void;
close: () => void;
}
export const DeleteTagQuestion: FC<Props> = ({ close, deleteItem }) => {
const theme = useTheme();
return (
<Box
sx={{
mt: "30px",
}}
>
<Typography textAlign="center">Вы хотите удалить элемент?</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-evenly",
flexWrap: "wrap",
margin: "30px auto",
}}
>
<Button
variant="contained"
sx={{
width: "150px",
mb: "15px"
}}
onClick={close}
>
отмена
</Button>
<Button
variant="contained"
sx={{
width: "150px",
mb: "15px"
}}
onClick={deleteItem}
>
удалить
</Button>
</Box>
</Box>
);
};

@ -0,0 +1,113 @@
import Box from "@mui/material/Box";
import { Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import GearIcon from "@icons/GearIcon";
import React, { FC, useCallback, useMemo } from "react";
import AccountSetting from "@icons/AccountSetting";
type AmoModalTitleProps = {
step: number;
title: string;
desc: string;
toSettings: () => void;
};
export const ModalTitle: FC<AmoModalTitleProps> = ({
step,
title,
desc,
toSettings,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box sx={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
<Box>
<Typography
sx={{
fontSize: isMobile ? "18px" : "24px",
color: theme.palette.grey3.main,
fontWeight: "500",
lineHeight: "1",
}}
>
{title}
</Typography>
{
desc &&
<Typography
sx={{
color: "#4D4D4D",
fontSize: "16px",
m: "5px 0 15px 0",
whiteSpace: "break-spaces",
width: "90%"
}}
>
{desc}
</Typography>
}
<Typography
sx={{
color: theme.palette.grey2.main,
fontWeight: "400",
marginTop: "4px",
fontSize: "14px",
lineHeight: "1",
}}
>
{step} шаг
</Typography>
</Box>
<Box>
<Button
variant="outlined"
startIcon={
<GearIcon
color={theme.palette.brightPurple.main}
height={"24px"}
width={"24px"}
/>
}
onClick={toSettings}
sx={{
height: "44px",
padding: "0",
width: isMobile ? "44px" : "205px",
minWidth: isMobile ? "44px" : "auto",
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,
},
},
}}
>
{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>
</>
);
};

@ -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>
</>
);
};

@ -0,0 +1,261 @@
import { FC, useEffect, useMemo, useState } from "react";
import { ItemsSelectionView } from "./ItemsSelectionView/ItemsSelectionView";
import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView";
import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { MinifiedData, QuestionKeys, SelectedQuestions, TagKeys, TagQuestionHC } from "../types";
import { EntitiesQuestions } from "./EntitiesQuestions";
import { diffArr } from "..";
import { DataConstrictor } from "../Components/DataConstrictor";
import { ModalTitle } from "../ModalTitle";
import { StepButtonsBlock } from "../StepButtonsBlock";
import { resetBitrixTagsFields } from "../useAmoIntegration";
type Props = {
selectedCurrentFields: MinifiedData[] | [];
questionsItems: MinifiedData[] | [];
fieldsItems: MinifiedData[] | [];
selectedQuestions: SelectedQuestions;
handleAddQuestion: (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => void;
openDelete: (data: TagQuestionHC) => void;
handlePrevStep: () => void;
handleNextStep: () => void;
FieldsAllowedFC: MinifiedData[]
setSelectedCurrentFields: (value: MinifiedData[]) => void;
titleProps: {
step: number;
title: string;
toSettings: () => void;
}
onScroll: () => void;
};
export type QuestionPair = {
question: string,
field: string
}
const FCTranslate = {
"name": "имя",
"email": "почта",
"phone": "телефон",
"text": "номер",
"address": "адрес",
}
export const AmoQuestions: FC<Props> = ({
selectedCurrentFields,
questionsItems,
fieldsItems,
selectedQuestions = [],
handleAddQuestion,
handlePrevStep,
handleNextStep,
openDelete,
FieldsAllowedFC,
setSelectedCurrentFields,
onScroll,
titleProps,
}) => {
if (!selectedQuestions.hasOwnProperty('Contact')) {
selectedQuestions.Contact = []
}
if (!selectedQuestions.hasOwnProperty('Customer')) {
selectedQuestions.Customer = []
}
if (!selectedQuestions.hasOwnProperty('Company')) {
selectedQuestions.Company = []
}
if (!selectedQuestions.hasOwnProperty('Lead')) {
selectedQuestions.Lead = []
}
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<QuestionKeys | null>(null);
const [selectedQuestion, setSelectedQuestion] = useState<string | null>(null);
const [selectedField, setSelectedField] = useState<string | null>(null);
const [isCurrentFields, setIsCurrentFields] = useState(true);
const handleAddNewField = () => {
if (activeScope === null || selectedQuestion === null) return;
setActiveScope(null);
handleAddQuestion(activeScope, selectedQuestion, "question");
};
const handleAddCurrentField = () => {
if (activeScope === null || selectedField === null || selectedQuestion === null ||
selectedCurrentFields.some(e => (e.id === selectedQuestion) && e.entity === activeScope)
) return;
setActiveScope(null);
//Убедимся что такой ФК не добавлялось
const newArray = selectedCurrentFields
let index = -1
selectedCurrentFields.forEach((e, i) => {
if (e.subTitle === selectedQuestion) index = i
})
if (index !== -1) newArray.splice(index, 1);
newArray.push({
id: selectedQuestion,
title: questionsItems.find(e => e.id === selectedQuestion)?.title || FCTranslate[selectedQuestion],
entity: activeScope,
amoId: selectedField,
})
setSelectedCurrentFields(newArray);
};
const handleAdd = () => {
if (isCurrentFields) {
handleAddCurrentField()
} else {
handleAddNewField()
}
}
const handleDelete = (id: string, scope: QuestionKeys) => {
openDelete({
id,
scope,
type: "question",
});
};
const SCFworld = (() => {
const obj = {
Lead: [],
Company: [],
Customer: [],
Contact: []
}
selectedCurrentFields.forEach((e) => {
if (!obj[e.entity]?.includes(e.id)) {
obj[e.entity].push(e)
}
})
return obj
})()
const [sortedFieldsAllowedFC, setSortedFieldsAllowedFC] = useState<MinifiedData[]>(FieldsAllowedFC);
const [sortedFieldsItems, setSortedFieldsItems] = useState<MinifiedData[]>(fieldsItems);
const [sortedquestionsItems, setSortedquestionsItems] = useState<MinifiedData[]>(questionsItems);
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()))
const c = questionsItems.filter((mData) => mData.title.toLowerCase().startsWith(substr.toLowerCase()))
setSortedFieldsAllowedFC(a);
setSortedFieldsItems(b);
setSortedquestionsItems(c);
}
useEffect(() => {
setSortedFieldsAllowedFC(FieldsAllowedFC);
setSortedFieldsItems(fieldsItems);
setSortedquestionsItems(questionsItems);
}, [activeScope])
const [blockButton, setBlockButton] = useState(false)
return (
<>
<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 ? (
// Здесь выбираем элемент в табличку
<>
<Button
disabled={blockButton}
onClick={() => {
setBlockButton(true)
setTimeout(() => setBlockButton(false), 20000)
resetBitrixTagsFields()
}}
sx={{
width: !isMobile ? "250px" : "auto",
borderRadius: "50px",
p: "8px 20px",
mr: "10px",
fontSize: "16px",
border: "#7E2AEA",
bgcolor: "#7E2AEA1A",
color: "#7E2AEA",
}}
>Обновить теги и сущности</Button>
<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>
<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}
largeBtnText={"Сохранить"}
/>
}
</Box>
</>
);
};

@ -0,0 +1,97 @@
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 "@icons/arrow_down.svg";
import { MinifiedData } from "@/pages/IntegrationsPage/IntegrationsModal/Bitrix/types";
import { CustomRadioGroup } from "@/components/CustomRadioGroup/CustomRadioGroup";
type CustomSelectProps = {
items: MinifiedData[] | [];
selectedItemId: string | null;
setSelectedItem: (value: string | null) => void;
handleScroll: () => void;
};
export const CurrentFieldSelect: FC<CustomSelectProps> = ({ items, selectedItemId, setSelectedItem, handleScroll }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const ref = useRef<HTMLDivElement | null>(null);
const [opened, setOpened] = useState<boolean>(false);
const toggleOpened = useCallback(() => {
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 = items.find((item) => item.id === selectedItemId) || null
return (
<Box>
<Box
sx={{
zIndex: 0,
position: "relative",
width: "100%",
height: "56px",
padding: "5px",
color: "#4D4D4D",
border: `2px solid ${theme.palette.common.white}`,
borderRadius: "12px",
background: "#EFF0F5",
display: "flex",
alignItems: "center",
cursor: "pointer",
}}
onClick={toggleOpened}
>
<Typography
sx={{
marginLeft: isMobile ? "10px" : "20px",
fontWeight: "400",
fontSize: "18px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
flexGrow: 1,
}}
>
{currentItem?.title || "Выберите поле"}
</Typography>
<img
src={arrow_down}
alt="check"
style={{
position: "absolute",
top: "50%",
right: "10px",
transform: `translateY(-50%) rotate(${opened ? "180deg" : "0deg"}`,
height: "36px",
width: "36px",
}}
className={`select-icon ${opened ? "opened" : ""}`}
/>
</Box>
{opened &&
<CustomRadioGroup
items={items}
selectedItemId={selectedItemId}
setSelectedItem={setSelectedItem}
handleScroll={() => { }}
activeScope={undefined}
/>
}
</Box>
);
};

@ -0,0 +1,109 @@
import { CustomRadioGroup } from "@/components/CustomRadioGroup/CustomRadioGroup"
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/Questions/CurrentFieldSelectMobile";
interface Props {
items: MinifiedData[];
fieldsItems: MinifiedData[];
currentField: string;
currentQuestion: string;
setCurrentField: (value: string) => void;
setCurrentQuestion: (value: string) => void;
handleScroll: () => void;
}
export const CurrentFields = ({
items,
fieldsItems,
currentField,
currentQuestion,
setCurrentField,
setCurrentQuestion,
handleScroll,
}: Props) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box
sx={{
display: "inline-flex",
justifyContent: isMobile ? undefined : "space-between",
width: "100%",
height: "326px",
flexDirection: isMobile ? "column" : undefined,
gap: isMobile ? "30px" : undefined
}}
>
<Box
sx={{
mr: "22px",
width: isMobile ? "100%" : "50%",
}}
>
<Typography
sx={{
fontSize: "16px",
fontWeight: 400,
lineHeight: "18.96px",
textAlign: "left",
color: "#9A9AAF",
m: "15px 0 10px",
}}
>Выберите поле</Typography>
{isMobile &&
<CurrentFieldSelect
items={fieldsItems}
selectedItemId={currentField}
setSelectedItem={setCurrentField}
handleScroll={handleScroll} />
}
{!isMobile &&
<CustomRadioGroup
items={fieldsItems}
selectedItemId={currentField}
setSelectedItem={setCurrentField}
handleScroll={handleScroll}
activeScope={undefined}
/>
}
</Box>
<Box
sx={{
width: isMobile ? "100%" : "50%",
}}
>
<Typography
sx={{
fontSize: "16px",
fontWeight: 400,
lineHeight: "18.96px",
textAlign: "left",
color: "#9A9AAF",
m: "15px 0 10px",
}}
>Выберите вопрос для этого поля</Typography>
{isMobile &&
<CurrentFieldSelect
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }} />
}
{!isMobile &&
<CustomRadioGroup
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }}
activeScope={undefined}
/>
}
</Box>
</Box>
)
}

@ -0,0 +1,109 @@
import {Box, Button, useMediaQuery, useTheme} from "@mui/material"
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 "..";
type ItemsSelectionViewProps = {
items: MinifiedData[] | [];
fieldsItems: MinifiedData[] | [];
selectedItemId?: string | null;
setSelectedQuestion: (value: string | null) => void;
selectedField?: string | null;
setSelectedField: (value: string | null) => void;
handleScroll: () => void;
activeScope: TagKeys;
FieldsAllowedFC: MinifiedData[];
selectedCurrentFields: MinifiedData[];
setIsCurrentFields: (a:boolean) => void;
isCurrentFields: boolean
}
export const EntitiesQuestions: FC<ItemsSelectionViewProps> = ({
items,
fieldsItems,
selectedItemId,
setSelectedQuestion,
selectedField,
setSelectedField,
handleScroll,
activeScope,
FieldsAllowedFC,
selectedCurrentFields,
setIsCurrentFields,
isCurrentFields,
}) => {
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: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
overflow: "auto"
}}
>
<Box>
<Button
onClick={() => setIsCurrentFields(old => !old)}
sx={{
width: isMobile ? "146px" : "auto",
borderRadius: "50px",
p: isMobile ? "8px 20px" : "10px 30px",
mr: "10px",
fontSize: "16px",
border: isCurrentFields ? "1px solid #7E2AEA" : "",
bgcolor: isCurrentFields ? "#7E2AEA1A" : "#F2F3F7",
color: isCurrentFields ? "#7E2AEA" : "",
}}
>Ваши готовые поля</Button>
<Button
onClick={() => setIsCurrentFields(old => !old)}
sx={{
width: isMobile ? "140px" : "auto",
borderRadius: "50px",
p: isMobile ? "8px 20px" : "10px 30px",
fontSize: "16px",
mt: isMobile ? "10px" : "0",
border: !isCurrentFields ? "1px solid #7E2AEA" : "",
bgcolor: !isCurrentFields ? "#7E2AEA1A" : "#F2F3F7",
color: !isCurrentFields ? "#7E2AEA" : "",
}}
>Новые поля +</Button>
</Box>
{
isCurrentFields ?
<CurrentFields
items={activeScope === "Contact" ? diffArr([...FieldsAllowedFC, ...items], selectedCurrentFields) : items}
fieldsItems={fieldsItems.filter(e => e.entity === activeScope)}
currentField={selectedField}
currentQuestion={selectedItemId}
setCurrentField={setSelectedField}
setCurrentQuestion={setSelectedQuestion}
handleScroll={handleScroll}
/>
:
<NewFields
items={items}
currentQuestion={selectedItemId}
setCurrentQuestion={setSelectedQuestion}
/>
}
</Box>
</Box>
)
}

@ -0,0 +1,69 @@
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 }) => {
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>
);
};

@ -0,0 +1,46 @@
import { Box, IconButton, useTheme } from "@mui/material";
import AddPlus from "@icons/questionsPage/addPlus";
import { FC } from "react";
type IconBtnAddProps = {
onAddBtnClick: () => void;
};
export const IconBtnAdd: FC<IconBtnAddProps> = ({ onAddBtnClick }) => {
const theme = useTheme();
return (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
borderBottom: `1px solid ${theme.palette.background.default}`,
}}
>
<IconButton
onClick={onAddBtnClick}
sx={{
width: "fit-content",
marginTop: "20px",
marginBottom: "66px",
circle: {
fill: "#EEE4FC",
},
"&:hover": {
circle: {
fill: theme.palette.brightPurple.main,
},
},
"&:active": {
circle: {
fill: "#581CA7",
},
},
}}
>
<AddPlus />
</IconButton>
</Box>
);
};

@ -0,0 +1,62 @@
import { Box, IconButton, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { IconBtnAdd } from "./IconBtnAdd/IconBtnAdd";
import { AnswerItem } from "./AnswerItem/AnswerItem";
import { QuestionKeys, SelectedQuestions, TagKeys, SelectedTags, MinifiedData } from "../../types";
type ItemProps = {
items: MinifiedData[];
title: QuestionKeys | TagKeys;
onAddBtnClick: () => void;
data: SelectedTags | SelectedQuestions;
deleteHC: (id: string, scope: QuestionKeys | TagKeys) => void;
};
export const Item: FC<ItemProps> = ({ items, title, onAddBtnClick, data, deleteHC }) => {
const theme = useTheme();
const titleDictionary = {
Company: "Компания",
Lead: "Сделка",
Contact: "Контакты",
Customer: "Покупатели",
};
const translatedTitle = titleDictionary[title];
const selectedOptions = data[title];
return (
<Box
sx={{
width: "172px",
display: "flex",
flexDirection: "column",
borderRight: `1px solid ${theme.palette.background.default}`,
}}
>
<Box
sx={{
alignSelf: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "12px",
backgroundColor: theme.palette.background.default,
width: "156px",
height: "40px",
}}
>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>{translatedTitle}</Typography>
</Box>
{selectedOptions &&
selectedOptions.map((id, index) => (
<AnswerItem
key={id + index}
fieldValue={"Значение поля"}
fieldName={items.find((e) => e.id === id)?.title || id}
deleteHC={() => deleteHC(selectedOptions[index], title)}
/>
))}
<IconBtnAdd onAddBtnClick={onAddBtnClick} />
</Box>
);
};

@ -0,0 +1,69 @@
import { Box, useTheme } from "@mui/material";
import { ItemForQuestions } from "../ItemForQuestions";
import { StepButtonsBlock } from "../../StepButtonsBlock";
import { FC } from "react";
import { MinifiedData, QuestionKeys, SelectedQuestions } from "../../types";
type TitleKeys = "Contact" | "Company" | "Lead" | "Customer";
type ItemDetailsViewProps = {
items: MinifiedData[];
setIsSelection: (value: boolean) => void;
selectedQuestions: SelectedQuestions;
setActiveScope: (value: QuestionKeys | null) => void;
deleteHC: (id: string, scope: QuestionKeys) => void;
};
export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
items,
selectedQuestions,
setIsSelection,
setActiveScope,
deleteHC,
}) => {
const theme = useTheme();
return (
<Box
sx={{
marginTop: "20px",
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
width: "100%",
height: "400px",
flexGrow: 1,
borderRadius: "10px",
padding: "10px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
display: "flex",
overflowY: "auto",
flexWrap: "wrap",
justifyContent: "start",
}}
>
{selectedQuestions &&
Object.keys(selectedQuestions).map((item) => (
<ItemForQuestions
key={item}
title={item}
onAddBtnClick={() => {
setIsSelection(true);
setActiveScope(item as QuestionKeys);
}}
items={items}
data={selectedQuestions}
deleteHC={deleteHC}
/>
))}
</Box>
</Box>
);
};

@ -0,0 +1,62 @@
import { Box, IconButton, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { IconBtnAdd } from "./Item/IconBtnAdd/IconBtnAdd";
import { AnswerItem } from "./Item/AnswerItem/AnswerItem";
import { QuestionKeys, SelectedQuestions, TagKeys, SelectedTags, MinifiedData } from "../types";
type ItemProps = {
items: MinifiedData[];
title: QuestionKeys | TagKeys;
onAddBtnClick: () => void;
data: MinifiedData[];
deleteHC: (id: string, scope: QuestionKeys | TagKeys) => void;
};
export const ItemForQuestions: FC<ItemProps> = ({ items, title, onAddBtnClick, data, deleteHC }) => {
const theme = useTheme();
const titleDictionary = {
Company: "Компания",
Lead: "Сделка",
Contact: "Контакты",
Customer: "Покупатели",
};
const translatedTitle = titleDictionary[title];
const selectedOptions = data[title];
return (
<Box
sx={{
width: "172px",
display: "flex",
flexDirection: "column",
borderRight: `1px solid ${theme.palette.background.default}`,
}}
>
<Box
sx={{
alignSelf: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "12px",
backgroundColor: theme.palette.background.default,
width: "156px",
height: "40px",
}}
>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>{translatedTitle}</Typography>
</Box>
{selectedOptions &&
selectedOptions.map((minifiedData, index) => (
<AnswerItem
key={minifiedData.id + index}
fieldValue={"Значение поля"}
fieldName={minifiedData.title}
deleteHC={() => deleteHC(selectedOptions[index].id, title)}
/>
))}
<IconBtnAdd onAddBtnClick={onAddBtnClick} />
</Box>
);
};

@ -0,0 +1,50 @@
import { Box } from "@mui/material";
import { CustomRadioGroup } from "../../../../../../components/CustomRadioGroup/CustomRadioGroup";
import { StepButtonsBlock } from "../../StepButtonsBlock";
import { FC } from "react";
import { MinifiedData, TagKeys } from "../../types";
type ItemsSelectionViewProps = {
items: MinifiedData[] | [];
selectedItemId?: string | null;
setSelectedItem: (value: string | null) => void;
handleScroll?: () => void;
activeScope: TagKeys;
};
export const ItemsSelectionView: FC<ItemsSelectionViewProps> = ({
items,
selectedItemId,
setSelectedItem,
handleScroll,
activeScope,
}) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
marginTop: "20px",
flexGrow: 1,
width: "100%",
height: "346px",
}}
>
<CustomRadioGroup
items={items}
selectedItemId={selectedItemId}
setSelectedItem={setSelectedItem}
handleScroll={handleScroll}
activeScope={activeScope}
/>
</Box>
</Box>
);
};

@ -0,0 +1,53 @@
import { CustomRadioGroup } from "@/components/CustomRadioGroup/CustomRadioGroup"
import { Box, Typography } from "@mui/material"
import { MinifiedData } from "../types";
interface Props {
items: MinifiedData[];
fieldsItems: MinifiedData[];
currentField: string;
currentQuestion: string;
setCurrentField: (value: string) => void;
setCurrentQuestion: (value: string) => void;
}
export const NewFields = ({
items,
currentQuestion,
setCurrentQuestion,
}: Props) => {
return (
<Box
sx={{
display: "inline-flex",
justifyContent: "space-between",
width: "100%",
height: "300px"
}}
>
<Box
sx={{
width: "100%"
}}
>
<Typography
sx={{
fontSize: "16px",
fontWeight: 400,
lineHeight: "18.96px",
textAlign: "left",
color: "#9A9AAF",
m: "15px 0 10px",
}}
>Выберите вопрос для поля. Название поля настроится автоматически</Typography>
<CustomRadioGroup
items={items}
selectedItemId={currentQuestion}
setSelectedItem={setCurrentQuestion}
handleScroll={() => { }}
activeScope={undefined}
/>
</Box>
</Box>
)
}

@ -0,0 +1,65 @@
import { FC } from "react"
import { Button, Typography, useTheme, Box } from "@mui/material"
import { removeBitrixAccount } from "@/api/bitrixIntegration";
import { enqueueSnackbar } from "notistack";
interface Props {
stopThisPage: () => void;
handleCloseModal: () => void;
}
export const RemoveAccount: FC<Props> = ({
stopThisPage,
handleCloseModal,
}: Props) => {
const theme = useTheme();
const removeAccount = async () => {
const [, error] = await removeBitrixAccount()
if (error) {
enqueueSnackbar(error)
} else {
handleCloseModal()
}
};
return (
<Box
sx={{
mt: "30px"
}}
>
<Typography textAlign="center">
Вы хотите сменить аккаунт?
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-evenly",
flexWrap: "wrap",
margin: "30px auto",
}}
>
<Button
variant="contained"
sx={{
width: "150px",
mb: "15px"
}}
onClick={stopThisPage}
>отмена</Button>
<Button
variant="contained"
sx={{
width: "150px",
mb: "15px"
}}
onClick={removeAccount}
>сменить</Button>
</Box>
</Box >
)
}

@ -0,0 +1,38 @@
import { Typography, useTheme } from "@mui/material";
import { FC } from "react";
type ResponsiblePersonProps = {
performer: string | null;
};
export const ResponsiblePerson: FC<ResponsiblePersonProps> = ({
performer,
}) => {
const theme = useTheme();
return (
<>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "18px",
fontWeight: 400,
margin: "10px 8px 0 0",
}}
display={"inline-block"}
>
Ответственный за сделку:
</Typography>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 400,
}}
display={"inline"}
>
{performer ? performer : "Не выбран"}
</Typography>
</>
);
};

@ -0,0 +1,27 @@
import { Typography, useTheme } from "@mui/material";
import Box from "@mui/material/Box";
import { FC } from "react";
type SelectedParameterProps = {
parameter: string | null;
};
export const SelectedParameter: FC<SelectedParameterProps> = ({
parameter,
}) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
width: "100%",
padding: "15px 20px",
backgroundColor: theme.palette.background.default,
borderRadius: "12px",
marginTop: "10px",
}}
>
<Typography>{parameter ? parameter : "Не выбрано"}</Typography>
</Box>
);
};

@ -0,0 +1,152 @@
import Box from "@mui/material/Box";
import { FC, useMemo } from "react";
import { Typography, useMediaQuery, useTheme } from "@mui/material";
import { SettingItemHeader } from "./SettingItemHeader/SettingItemHeader";
import { ResponsiblePerson } from "./ResponsiblePerson/ResponsiblePerson";
import { SelectedParameter } from "./SelectedParameter/SelectedParameter";
import { SelectedQuestions, SelectedTags } from "../../types";
type SettingItemProps = {
step: number;
title: string;
setStep: (step: number) => void;
selectedFunnelPerformer: string | null;
selectedFunnel: string | null;
selectedStagePerformer: string | null;
selectedDealUser: string | null;
selectedStage: string | null;
selectedQuestions: SelectedQuestions;
selectedTags: SelectedTags;
};
export const SettingItem: FC<SettingItemProps> = ({
step,
title,
setStep,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealUser,
selectedStage,
selectedQuestions,
selectedTags,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
if (step === 0) {
return;
}
const SettingsContent = useMemo(() => {
if (step === 1) {
return (
<>
<ResponsiblePerson performer={selectedDealUser} />
<SelectedParameter parameter={selectedFunnel} />
</>
);
}
if (step === 2) {
return (
<>
<ResponsiblePerson performer={selectedDealUser} />
<SelectedParameter parameter={selectedStage} />
</>
);
}
if (step === 3) {
return (
<>
<ResponsiblePerson performer={selectedDealUser} />
</>
);
}
if (step === 4) {
const isFilled = Object.values(selectedTags).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
<>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "18px",
fontWeight: 400,
margin: "10px 8px 0 0",
}}
display={"inline-block"}
>
Статус:
</Typography>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 400,
}}
display={"inline"}
>
{status}
</Typography>
</>
);
}
if (step === 5) {
const isFilled = Object.values(selectedQuestions).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
<>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "18px",
fontWeight: 400,
margin: "10px 8px 0 0",
}}
display={"inline-block"}
>
Статус:
</Typography>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 400,
}}
display={"inline"}
>
{status}
</Typography>
</>
);
}
return null;
}, [
step,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealUser,
selectedStage,
selectedQuestions,
selectedTags,
]);
return (
<Box
sx={{
width: "100%",
padding: "20px 0",
borderTop: `1px solid ${theme.palette.background.default}`,
}}
>
<SettingItemHeader
title={title}
step={step}
setStep={() => setStep(step)}
/>
<Box>{SettingsContent}</Box>
</Box>
);
};

@ -0,0 +1,57 @@
import Box from "@mui/material/Box";
import { IconButton, Typography, useTheme } from "@mui/material";
import EditPencil from "@icons/EditPencil";
import { FC } from "react";
type SettingItemHeaderProps = {
title: string;
step: number;
setStep: () => void;
};
export const SettingItemHeader: FC<SettingItemHeaderProps> = ({
title,
step,
setStep,
}) => {
const theme = useTheme();
return (
<Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "14px",
fontWeight: 400,
}}
>
{step} этап
</Typography>
<IconButton onClick={setStep}>
<EditPencil
color={theme.palette.brightPurple.main}
width={"18px"}
height={"18px"}
/>
</IconButton>
</Box>
<Typography
sx={{
color: theme.palette.grey3.main,
fontSize: "18px",
fontWeight: 500,
lineHeight: "1",
}}
>
{title}
</Typography>
</Box>
);
};

@ -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>
);
};

@ -0,0 +1,79 @@
import { Box, Button, useTheme } from "@mui/material";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { FC } from "react";
type StepButtonsBlockProps = {
onSmallBtnClick?: () => void;
onLargeBtnClick?: () => void;
isSmallBtnMissing?: boolean;
isLargeBtnMissing?: boolean;
isSmallBtnDisabled?: boolean;
isLargeBtnDisabled?: boolean;
smallBtnText?: string;
largeBtnText?: string;
largeBtnType?: "button" | "submit" | "reset";
};
export const StepButtonsBlock: FC<StepButtonsBlockProps> = ({
onSmallBtnClick,
onLargeBtnClick,
isSmallBtnMissing,
isLargeBtnMissing,
smallBtnText,
largeBtnText,
isSmallBtnDisabled,
isLargeBtnDisabled,
largeBtnType,
}) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
justifyContent: "end",
alignItems: "end",
gap: "10px",
mt: "20px"
}}
>
{isSmallBtnMissing || (
<Button
variant="outlined"
sx={{
padding: "10px 20px",
borderRadius: "8px",
height: "44px",
color: theme.palette.brightPurple.main,
}}
data-cy="back-button"
disabled={isSmallBtnDisabled}
onClick={onSmallBtnClick}
>
{smallBtnText ? (
smallBtnText
) : (
<ArrowLeft color={theme.palette.brightPurple.main} />
)}
</Button>
)}
{isLargeBtnMissing || (
<Button
data-cy="next-step"
variant="contained"
disabled={isLargeBtnDisabled}
type={largeBtnType ? largeBtnType : "button"}
sx={{
height: "44px",
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
onClick={onLargeBtnClick}
>
{largeBtnText ? largeBtnText : "Далее"}
</Button>
)}
</Box>
);
};

@ -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 { 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) => {
return({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
})});
}
if (type === "question") {
const q = questions.find(e => e.backendId === Number(id))
setSelectedQuestions((prevState) => {
return ({
...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;
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,158 @@
import { FC, useState } from "react";
import { Box, Button, useMediaQuery, useTheme } 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";
import { resetBitrixTagsFields } from "../useAmoIntegration";
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 theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
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);
}
const [blockButton, setBlockButton] = useState(false)
return (
<>
<Box
sx={{
height: "calc( 100% - 70px )",
overflow: "auto"
}}
>
<ModalTitle
{...titleProps}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
// Здесь выбираем элемент в табличку
<>
<Button
disabled={blockButton}
onClick={() => {
setBlockButton(true)
setTimeout(() => setBlockButton(false), 20000)
resetBitrixTagsFields()
}}
sx={{
width: !isMobile ? "250px" : "auto",
borderRadius: "50px",
p: "8px 20px",
mr: "10px",
fontSize: "16px",
border: "#7E2AEA",
bgcolor: "#7E2AEA1A",
color: "#7E2AEA",
}}
>Обновить теги и сущности</Button>
<DataConstrictor
isError={sortedTagsItems.length === 0}
constrictor={startConstrictor}
/>
<ItemsSelectionView
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>
</>
);
};

@ -0,0 +1,89 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../../StepButtonsBlock";
import { FC } from "react";
import { Item } from "../../Questions/Item/Item";
import { MinifiedData, SelectedTags, TagKeys } from "../../types";
type TagsDetailsViewProps = {
items: MinifiedData[];
setIsSelection: (value: boolean) => void;
setActiveScope: (value: TagKeys | null) => void;
selectedTags: SelectedTags;
deleteHC: (id: string, scope: TagKeys) => void;
};
export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
items,
setActiveScope,
selectedTags,
setIsSelection,
deleteHC,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
return (
<Box
sx={{
marginTop: "15px",
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
flexGrow: 1,
}}
>
<Box
sx={{
width: "100%",
maxHeight: "380px",
flexGrow: 1,
borderRadius: "10px",
padding: "10px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
display: "flex",
flexDirection: isMobile ? "column" : "row",
}}
>
<Box
sx={{
p: isMobile ? "0" : "0 40px",
m: isMobile ? "5px auto 20px" : "0",
borderRight: isMobile ? "none" : `1px solid ${theme.palette.background.default}`,
height: isMobile ? "auto" : "100%",
display: "flex",
alignItems: "center",
}}
>
<Typography sx={{ fontSize: "14px", color: theme.palette.grey2.main }}>Результат</Typography>
</Box>
<Box
sx={{
width: "100%",
flexGrow: 1,
display: "flex",
overflowY: "auto",
flexWrap: "wrap",
justifyContent: "start",
}}
>
{selectedTags &&
Object.keys(selectedTags).map((item) => (
<Item
key={item}
items={items}
title={item as TagKeys}
onAddBtnClick={() => {
setIsSelection(true);
setActiveScope(item as TagKeys);
}}
data={selectedTags}
deleteHC={deleteHC}
/>
))}
</Box>
</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 } from "react-router-dom";
import CloseIcon from "@mui/icons-material/Close";
import { useBitrixIntegration } 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 BitrixModal: 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,
} = useBitrixIntegration({
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
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));
}

@ -0,0 +1,19 @@
export type TagKeys = "Company" | "Lead" | "Customer" | "Contact";
export type SelectedTags = Record<TagKeys, number[]>;
export type QuestionKeys = "Company" | "Lead" | "Customer" | "Contact";
export type SelectedQuestions = Record<QuestionKeys, MinifiedData[]>;
export type MinifiedData = {
id: string;
title: string;
subTitle?: string;
entity?: TagKeys;
amoId?: string;
};
export type TagQuestionHC = {
scope: QuestionKeys | TagKeys;
id: string;
type: "question" | "tag";
};

@ -0,0 +1,384 @@
import { useEffect, useState } from "react";
import { enqueueSnackbar } from "notistack";
import type { TagKeys, SelectedTags, QuestionKeys, SelectedQuestions, MinifiedData } from "./types";
import {
AccountResponse,
getIntegrationRules,
getPipelines,
getSteps,
getTags,
getUsers,
getAccount,
FieldsRule,
getFields,
} from "@/api/bitrixIntegration";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { UntypedQuizQuestion } from "@/model/questionTypes/shared";
const SIZE = 25;
interface Props {
isModalOpen: boolean;
isTryRemoveAccount: boolean;
quizID: number;
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[]
}
const FCTranslate = {
"name": "имя",
"email": "почта",
"phone": "телефон",
"text": "номер",
"address": "адрес",
}
let isReadyGetPipeline = true;
let isReadyGetPipelineStep = true;
let isReadyGetUsers = true;
let isReadyGetTags = true;
let isReadyGetFields = true;
export const useBitrixIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, questions }: Props) => {
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(true);
const [firstRules, setFirstRules] = useState<boolean>(false);
const [accountInfo, setAccountInfo] = useState<AccountResponse | null>(null);
const [arrayOfPipelines, setArrayOfPipelines] = useState<MinifiedData[]>([]);
const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState<MinifiedData[]>([]);
const [arrayOfUsers, setArrayOfUsers] = useState<MinifiedData[]>([]);
const [arrayOfTags, setArrayOfTags] = useState<MinifiedData[]>([]);
const [arrayOfFields, setArrayOfFields] = useState<MinifiedData[]>([]);
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [selectedPipelineStep, setSelectedPipelineStep] = useState<string | null>(null);
const [selectedDealUser, setSelectedDealPerformer] = useState<string | null>(null);
const [selectedCurrentFields, setSelectedCurrentFields] = useState<MinifiedData[]>([]);
const [questionsBackend, setQuestionsBackend] = useState<FieldsRule>({} as FieldsRule);
const [selectedTags, setSelectedTags] = useState<SelectedTags>({
Lead: [],
Contact: [],
Company: [],
Customer: [],
});
const [selectedQuestions, setSelectedQuestions] = useState<SelectedQuestions>({
Lead: [],
Company: [],
Customer: [],
Contact: []
});
const [pageOfPipelines, setPageOfPipelines] = useState(1);
const [pageOfPipelinesSteps, setPageOfPipelinesSteps] = useState(1);
const [pageOfUsers, setPageOfUsers] = useState(1);
const [pageOfTags, setPageOfTags] = useState(1);
const [pageOfFields, setPageOfFields] = useState(1);
const selectedPipelineHC = (id:string | null) => {
setSelectedPipeline(id);
isReadyGetPipelineStep = true;
setPageOfPipelinesSteps(1);
}
useEffect(() => {
const fetchAccountRules = async () => {
setIsLoadingPage(true);
const [account, accountError] = await getAccount();
if (accountError) {
if (!accountError.includes("Not Found")) enqueueSnackbar(accountError);
setAccountInfo(null);
}
if (account) {
setAccountInfo(account);
}
const [settingsResponse, rulesError] = await getIntegrationRules(quizID.toString());
if (rulesError) {
if (rulesError === "first") setFirstRules(true);
if (!rulesError.includes("Not Found") && !rulesError.includes("first")) enqueueSnackbar(rulesError);
}
if (settingsResponse) {
if (settingsResponse.PipelineID) selectedPipelineHC(settingsResponse.PipelineID.toString());
if (settingsResponse.StepID) setSelectedPipelineStep(settingsResponse.StepID.toString());
if (settingsResponse.PerformerID) setSelectedDealPerformer(settingsResponse.PerformerID.toString());
if (Boolean(settingsResponse.FieldsRule) && Object.keys(settingsResponse?.FieldsRule).length > 0) {
const gottenQuestions = { ...selectedQuestions };
setQuestionsBackend(settingsResponse.FieldsRule);
for (let key in settingsResponse.FieldsRule) {
if (
settingsResponse.FieldsRule[key as QuestionKeys] !== null
) {
const gottenList = settingsResponse.FieldsRule[key as QuestionKeys];
if (gottenList !== null) {
Object.keys(gottenList.QuestionID).forEach((qId) => {
const q = questions.find(e => e.backendId === Number(qId)) || {}
if (gottenQuestions[key as QuestionKeys] === undefined) gottenQuestions[key as QuestionKeys] = []
gottenQuestions[key as QuestionKeys].push({
id: qId,
title: q.title,
entity: key,
})
})
}
if (key === "Contact") {
const MAP = settingsResponse.FieldsRule[key as QuestionKeys].ContactRuleMap
const list = []
for (let key in MAP) {
list.push({
id: key,
title: FCTranslate[key],
entity: "Contact",
amoId: MAP[key].toString(),
})
}
setSelectedCurrentFields(list)
}
}
}
setSelectedQuestions(gottenQuestions);
}
if (Boolean(settingsResponse.TagsToAdd) && Object.keys(settingsResponse.TagsToAdd).length > 0) {
const gottenTags = { ...selectedTags };
for (let key in settingsResponse.TagsToAdd) {
const gottenList = settingsResponse.TagsToAdd[key as TagKeys];
if (gottenList !== null && Array.isArray(gottenList)) {
gottenTags[key as TagKeys] = gottenList.map((e) => e.toString());
}
}
setSelectedTags(gottenTags);
}
setFirstRules(false);
}
setIsLoadingPage(false);
};
fetchAccountRules();
}, [isModalOpen, isTryRemoveAccount]);
useEffect(() => {
const transletedQuestions = {...selectedQuestions}
Object.keys(selectedQuestions)?.forEach((column) => {
selectedQuestions[column].forEach((minifiedData) => {
const q = questions.find(e => e.backendId === Number(minifiedData.id)) || {};
transletedQuestions[column].push({
...minifiedData,
title: q.title || transletedQuestions[column].title
})
})
})
setSelectedQuestions(transletedQuestions)
}, [questions])
useEffect(() => {
if (isReadyGetPipeline) {
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);
} 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(() => {
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,
});
});
setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
} else {
isReadyGetUsers = false
}
});
}
}, [pageOfUsers]);
useEffect(() => {
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",
});
});
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
} else {
isReadyGetTags = false
}
});
}
}, [pageOfTags]);
useEffect(() => {
if (isReadyGetFields) {
getFields({
page: pageOfFields,
size: 1000,
}).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",
});
});
setArrayOfFields((prevItems) => [...prevItems, ...minifiedTags]);
}
});
} else {
isReadyGetFields = false
}
}, [pageOfFields]);
useEffect(() => () => {
isReadyGetPipeline = true;
isReadyGetPipelineStep = true;
isReadyGetUsers = true;
isReadyGetTags = true;
isReadyGetFields = true;
}, [])
return {
isLoadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
arrayOfFields,
selectedPipeline,
setSelectedPipeline: selectedPipelineHC,
selectedCurrentFields,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
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,
};
};
import { makeRequest } from "@frontend/kitui";
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/bitrix`;
export const resetBitrixTagsFields = async () => {
let success = true
//Fields
try {
await makeRequest({
method: "PATCH",
url: `${API_URL}/fields`,
});
} catch (nativeError) {
success = false
}
//Tags
try {
await makeRequest({
method: "PATCH",
url: `${API_URL}/tags`,
});
} catch (nativeError) {
success = false
}
if (success) {
enqueueSnackbar("Данные обновятся через 5-30 минут")
} else {
enqueueSnackbar("Не удалось обновить данные")
}
}

@ -27,9 +27,8 @@ export const IntegrationsPage = ({
keyof typeof QuizMetricType | null
>(null);
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
const [leadTargetsLoaded, setLeadTargetsLoaded] = useState<boolean>(false);
const [leadTargets, setLeadTargets] = useState<LeadTargetModel[] | null>(null);
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
@ -76,15 +75,7 @@ export const IntegrationsPage = ({
const handleCloseModal = () => {
setIsModalOpen(false);
};
const handleCloseAmoSRMModal = () => {
setIsAmoCrmModalOpen(false);
};
const handleCloseZapierModal = () => {
setIsZapierModalOpen(false);
};
const handleClosePostbackModal = () => {
setIsPostbackModalOpen(false);
};
return (
<>
@ -111,15 +102,6 @@ export const IntegrationsPage = ({
setCompanyName={setCompanyName}
isModalOpen={isModalOpen}
handleCloseModal={handleCloseModal}
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
isAmoCrmModalOpen={isAmoCrmModalOpen}
handleCloseAmoSRMModal={handleCloseAmoSRMModal}
setIsZapierModalOpen={setIsZapierModalOpen}
isZapierModalOpen={isZapierModalOpen}
handleCloseZapierModal={handleCloseZapierModal}
setIsPostbackModalOpen={setIsPostbackModalOpen}
isPostbackModalOpen={isPostbackModalOpen}
handleClosePostbackModal={handleClosePostbackModal}
zapierTarget={zapierTarget}
postbackTarget={postbackTarget}
/>

@ -1,5 +1,5 @@
import { Box, Typography, useTheme } from "@mui/material";
import React, { FC, lazy, Suspense } from "react";
import React, { FC, lazy, Suspense, useState } from "react";
import { ServiceButton } from "./buttons/ServiceButton";
import { ZapierButton } from "./buttons/ZapierButton";
import { PostbackButton } from "./buttons/PostbackButton";
@ -21,6 +21,11 @@ const AmoCRMModal = lazy(() =>
default: module.AmoCRMModal,
}))
);
const BitrixModal = lazy(() =>
import("../IntegrationsModal/Bitrix").then((module) => ({
default: module.BitrixModal,
}))
);
const ZapierModal = lazy(() =>
import("../IntegrationsModal/Zapier").then((module) => ({
@ -40,15 +45,6 @@ type PartnersBoardProps = {
setCompanyName: (value: keyof typeof QuizMetricType) => void;
isModalOpen: boolean;
handleCloseModal: () => void;
setIsAmoCrmModalOpen: (value: boolean) => void;
isAmoCrmModalOpen: boolean;
handleCloseAmoSRMModal: () => void;
setIsZapierModalOpen: (value: boolean) => void;
isZapierModalOpen: boolean;
handleCloseZapierModal: () => void;
setIsPostbackModalOpen: (value: boolean) => void;
isPostbackModalOpen: boolean;
handleClosePostbackModal: () => void;
zapierTarget?: LeadTargetModel | null;
postbackTarget?: LeadTargetModel | null;
};
@ -59,21 +55,22 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
handleCloseModal,
companyName,
setCompanyName,
setIsAmoCrmModalOpen,
isAmoCrmModalOpen,
handleCloseAmoSRMModal,
setIsZapierModalOpen,
isZapierModalOpen,
handleCloseZapierModal,
setIsPostbackModalOpen,
isPostbackModalOpen,
handleClosePostbackModal,
zapierTarget,
postbackTarget,
}) => {
const theme = useTheme();
const quiz = useCurrentQuiz();
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
const handleCloseAmoSRMModal = () => setIsAmoCrmModalOpen(false);
const [isBitrixModalOpen, setIsBitrixModalOpen] = useState<boolean>(false);
const handleCloseBirixModal = () => setIsBitrixModalOpen(false);
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
const handleCloseZapierModal = () => setIsZapierModalOpen(false);
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
const handleClosePostbackModal = () => setIsPostbackModalOpen(false);
const sectionTitleStyles = {
textAlign: { xs: "start", sm: "start", md: "start" } as const,
lineHeight: "1",
@ -116,6 +113,12 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
setCompanyName={setCompanyName}
name={"amoCRM"}
/>
<ServiceButton
logo={"Bitrix"}
setIsModalOpen={setIsBitrixModalOpen}
setCompanyName={setCompanyName}
name={"Bitrix"}
/>
</Box>
<Typography variant="h6" sx={sectionTitleStyles}>
@ -171,6 +174,16 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
/>
</Suspense>
)}
{companyName && isBitrixModalOpen && (
<Suspense>
<BitrixModal
isModalOpen={isBitrixModalOpen}
handleCloseModal={handleCloseBirixModal}
companyName={companyName}
quiz={quiz!}
/>
</Suspense>
)}
{companyName && isZapierModalOpen && (
<Suspense>
<ZapierModal

@ -6,7 +6,7 @@ import { IntegrationButton } from "./IntegrationButton";
type PartnerItemProps = {
setIsModalOpen: (value: boolean) => void;
setCompanyName: (value: keyof typeof QuizMetricType) => void;
logo?: JSX.Element;
logo?: JSX.Element | string;
title?: string;
name: string;
};

@ -113,7 +113,6 @@ const OverTime = () => {
if (rafId) cancelAnimationFrame(rafId);
if (timerId) clearInterval(timerId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled, (quiz as any)?.config?.overTime?.endsAt]);
// Синхронизация, если config.overTime обновился извне