bitrix
This commit is contained in:
parent
639929d825
commit
6ceaf7e8bc
@ -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.13 _ 2025-10-18 _ Визуал utm + логика
|
||||||
1.0.12 _ 2025-10-12 _ ютм с дизайном и беком, но без логики
|
1.0.12 _ 2025-10-12 _ ютм с дизайном и беком, но без логики
|
||||||
1.0.11 _ 2025-10-06 _ Merge branch 'staging'
|
1.0.11 _ 2025-10-06 _ Merge branch 'staging'
|
||||||
|
|||||||
577
api-docs.html
577
api-docs.html
@ -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
BIN
src.zip
Binary file not shown.
374
src/api/bitrixIntegration.ts
Normal file
374
src/api/bitrixIntegration.ts
Normal file
@ -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}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
21
src/assets/icons/Bitrix.tsx
Normal file
21
src/assets/icons/Bitrix.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
148
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/AmoLogin.tsx
Normal file
148
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/AmoLogin.tsx
Normal file
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
97
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/CurrentFieldSelectMobile.tsx
Normal file
97
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/CurrentFieldSelectMobile.tsx
Normal file
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
69
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/Item/AnswerItem/AnswerItem.tsx
Normal file
69
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/Item/AnswerItem/AnswerItem.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/Item/IconBtnAdd/IconBtnAdd.tsx
Normal file
46
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/Item/IconBtnAdd/IconBtnAdd.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/ItemDetailsView/ItemDetailsView.tsx
Normal file
69
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/ItemDetailsView/ItemDetailsView.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
50
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/ItemsSelectionView/ItemsSelectionView.tsx
Normal file
50
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Questions/ItemsSelectionView/ItemsSelectionView.tsx
Normal file
@ -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 >
|
||||||
|
)
|
||||||
|
}
|
||||||
38
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/ResponsiblePerson/ResponsiblePerson.tsx
Normal file
38
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/ResponsiblePerson/ResponsiblePerson.tsx
Normal file
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
27
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/SelectedParameter/SelectedParameter.tsx
Normal file
27
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/SelectedParameter/SelectedParameter.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
152
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/SettingItem.tsx
Normal file
152
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/SettingItem.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
57
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/SettingItemHeader/SettingItemHeader.tsx
Normal file
57
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/SettingsBlock/SettingItem/SettingItemHeader/SettingItemHeader.tsx
Normal file
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
89
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Tags/TagsDetailsView/TagsDetailsView.tsx
Normal file
89
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/Tags/TagsDetailsView/TagsDetailsView.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
172
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/index.tsx
Normal file
172
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/index.tsx
Normal file
@ -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));
|
||||||
|
}
|
||||||
19
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/types.ts
Normal file
19
src/pages/IntegrationsPage/IntegrationsModal/Bitrix/types.ts
Normal file
@ -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
|
keyof typeof QuizMetricType | null
|
||||||
>(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 [leadTargetsLoaded, setLeadTargetsLoaded] = useState<boolean>(false);
|
||||||
const [leadTargets, setLeadTargets] = useState<LeadTargetModel[] | null>(null);
|
const [leadTargets, setLeadTargets] = useState<LeadTargetModel[] | null>(null);
|
||||||
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
|
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
|
||||||
@ -76,15 +75,7 @@ export const IntegrationsPage = ({
|
|||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
const handleCloseAmoSRMModal = () => {
|
|
||||||
setIsAmoCrmModalOpen(false);
|
|
||||||
};
|
|
||||||
const handleCloseZapierModal = () => {
|
|
||||||
setIsZapierModalOpen(false);
|
|
||||||
};
|
|
||||||
const handleClosePostbackModal = () => {
|
|
||||||
setIsPostbackModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -111,15 +102,6 @@ export const IntegrationsPage = ({
|
|||||||
setCompanyName={setCompanyName}
|
setCompanyName={setCompanyName}
|
||||||
isModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
handleCloseModal={handleCloseModal}
|
handleCloseModal={handleCloseModal}
|
||||||
setIsAmoCrmModalOpen={setIsAmoCrmModalOpen}
|
|
||||||
isAmoCrmModalOpen={isAmoCrmModalOpen}
|
|
||||||
handleCloseAmoSRMModal={handleCloseAmoSRMModal}
|
|
||||||
setIsZapierModalOpen={setIsZapierModalOpen}
|
|
||||||
isZapierModalOpen={isZapierModalOpen}
|
|
||||||
handleCloseZapierModal={handleCloseZapierModal}
|
|
||||||
setIsPostbackModalOpen={setIsPostbackModalOpen}
|
|
||||||
isPostbackModalOpen={isPostbackModalOpen}
|
|
||||||
handleClosePostbackModal={handleClosePostbackModal}
|
|
||||||
zapierTarget={zapierTarget}
|
zapierTarget={zapierTarget}
|
||||||
postbackTarget={postbackTarget}
|
postbackTarget={postbackTarget}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Box, Typography, useTheme } from "@mui/material";
|
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 { ServiceButton } from "./buttons/ServiceButton";
|
||||||
import { ZapierButton } from "./buttons/ZapierButton";
|
import { ZapierButton } from "./buttons/ZapierButton";
|
||||||
import { PostbackButton } from "./buttons/PostbackButton";
|
import { PostbackButton } from "./buttons/PostbackButton";
|
||||||
@ -21,6 +21,11 @@ const AmoCRMModal = lazy(() =>
|
|||||||
default: module.AmoCRMModal,
|
default: module.AmoCRMModal,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
const BitrixModal = lazy(() =>
|
||||||
|
import("../IntegrationsModal/Bitrix").then((module) => ({
|
||||||
|
default: module.BitrixModal,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const ZapierModal = lazy(() =>
|
const ZapierModal = lazy(() =>
|
||||||
import("../IntegrationsModal/Zapier").then((module) => ({
|
import("../IntegrationsModal/Zapier").then((module) => ({
|
||||||
@ -40,15 +45,6 @@ type PartnersBoardProps = {
|
|||||||
setCompanyName: (value: keyof typeof QuizMetricType) => void;
|
setCompanyName: (value: keyof typeof QuizMetricType) => void;
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
handleCloseModal: () => void;
|
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;
|
zapierTarget?: LeadTargetModel | null;
|
||||||
postbackTarget?: LeadTargetModel | null;
|
postbackTarget?: LeadTargetModel | null;
|
||||||
};
|
};
|
||||||
@ -59,21 +55,22 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
handleCloseModal,
|
handleCloseModal,
|
||||||
companyName,
|
companyName,
|
||||||
setCompanyName,
|
setCompanyName,
|
||||||
setIsAmoCrmModalOpen,
|
|
||||||
isAmoCrmModalOpen,
|
|
||||||
handleCloseAmoSRMModal,
|
|
||||||
setIsZapierModalOpen,
|
|
||||||
isZapierModalOpen,
|
|
||||||
handleCloseZapierModal,
|
|
||||||
setIsPostbackModalOpen,
|
|
||||||
isPostbackModalOpen,
|
|
||||||
handleClosePostbackModal,
|
|
||||||
zapierTarget,
|
zapierTarget,
|
||||||
postbackTarget,
|
postbackTarget,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const quiz = useCurrentQuiz();
|
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 = {
|
const sectionTitleStyles = {
|
||||||
textAlign: { xs: "start", sm: "start", md: "start" } as const,
|
textAlign: { xs: "start", sm: "start", md: "start" } as const,
|
||||||
lineHeight: "1",
|
lineHeight: "1",
|
||||||
@ -116,6 +113,12 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
setCompanyName={setCompanyName}
|
setCompanyName={setCompanyName}
|
||||||
name={"amoCRM"}
|
name={"amoCRM"}
|
||||||
/>
|
/>
|
||||||
|
<ServiceButton
|
||||||
|
logo={"Bitrix"}
|
||||||
|
setIsModalOpen={setIsBitrixModalOpen}
|
||||||
|
setCompanyName={setCompanyName}
|
||||||
|
name={"Bitrix"}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h6" sx={sectionTitleStyles}>
|
<Typography variant="h6" sx={sectionTitleStyles}>
|
||||||
@ -171,6 +174,16 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
{companyName && isBitrixModalOpen && (
|
||||||
|
<Suspense>
|
||||||
|
<BitrixModal
|
||||||
|
isModalOpen={isBitrixModalOpen}
|
||||||
|
handleCloseModal={handleCloseBirixModal}
|
||||||
|
companyName={companyName}
|
||||||
|
quiz={quiz!}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
{companyName && isZapierModalOpen && (
|
{companyName && isZapierModalOpen && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<ZapierModal
|
<ZapierModal
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { IntegrationButton } from "./IntegrationButton";
|
|||||||
type PartnerItemProps = {
|
type PartnerItemProps = {
|
||||||
setIsModalOpen: (value: boolean) => void;
|
setIsModalOpen: (value: boolean) => void;
|
||||||
setCompanyName: (value: keyof typeof QuizMetricType) => void;
|
setCompanyName: (value: keyof typeof QuizMetricType) => void;
|
||||||
logo?: JSX.Element;
|
logo?: JSX.Element | string;
|
||||||
title?: string;
|
title?: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -113,7 +113,6 @@ const OverTime = () => {
|
|||||||
if (rafId) cancelAnimationFrame(rafId);
|
if (rafId) cancelAnimationFrame(rafId);
|
||||||
if (timerId) clearInterval(timerId);
|
if (timerId) clearInterval(timerId);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [enabled, (quiz as any)?.config?.overTime?.endsAt]);
|
}, [enabled, (quiz as any)?.config?.overTime?.endsAt]);
|
||||||
|
|
||||||
// Синхронизация, если config.overTime обновился извне
|
// Синхронизация, если config.overTime обновился извне
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user