@andrey4emk/npm-app-front-b24 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Subscriptions.vue CHANGED
@@ -1,142 +1,214 @@
1
1
  <template>
2
- <div class="Subscriptions">
2
+ <div class="Subscriptions subscriptions">
3
3
  <h1>Страница подписок</h1>
4
- <button @click="selectTab('tab1')">Сущности</button>
5
- <button @click="selectTab('tab2')">События</button>
6
- <br /><br /><br />
7
-
8
- <!-- Вкладка 1 -->
9
- <div v-if="tabs.tab1">
10
- <div>
11
- <button @click="loadOfflineEntitiesList" :disabled="loading">Обновить</button>
12
- <!-- Чекбокс для запуска автообновления сущностей. При наведении выходит сообщение "автообновление каждые 3 секунды" -->
13
- <label class="switch tooltipParent" style="top: 12px">
14
- <input type="checkbox" v-model="autoRefresh" @change="autoRefreshEntities" />
15
- <span class="slider"></span>
16
- <span class="tooltip">Автообновление каждые 3 секунды</span>
17
- </label>
18
- </div>
19
4
 
20
- <div v-if="entities.list.length > 0">
21
- <p><button @click="deleteOfflineEntitiesAll">Удалить все</button><button @click="deleteOfflineEntitiesAllEmpty">Удалить все пустые</button> Всего измененных сущностей {{ entities.total }}</p>
22
- <table class="main" v-if="entities.list.length">
23
- <thead>
24
- <tr>
25
- <th>#</th>
26
- <th>Удалить</th>
27
- <th>Обновить</th>
28
- <th>Дата</th>
29
- <th>PROCESS_ID</th>
30
- <th>EVENT_NAME</th>
31
- <th>Ссылка</th>
32
- <th>EVENT_DATA</th>
33
- <th>who_update</th>
34
- <th>entity</th>
35
- </tr>
36
- </thead>
37
- <tbody>
38
- <tr v-for="(entity, index) in entities.list" :key="entity.PROCESS_ID + '-' + entity.ID">
39
- <td>{{ index + 1 }}</td>
40
- <!-- Удалить -->
41
- <td>
42
- <button @click="deleteOfflineEntities(entity)">Удалить</button>
43
- </td>
44
- <!-- Изменить -->
45
- <td>
46
- <button v-if="entity.EVENT_DATA.FIELDS?.ID && (entity.EVENT_NAME == 'ONCRMDEALUPDATE' || entity.EVENT_NAME.includes('ONCRMDYNAMICITEMUPDATE_'))" @click="entityUpdate(entity)">Изменить</button>
47
- </td>
48
- <td>{{ formatDate(entity.TIMESTAMP_X) }}</td>
49
- <td>{{ entity.PROCESS_ID }}</td>
50
- <td>{{ entity.EVENT_NAME }}</td>
51
- <td>
52
- <a v-if="entity.EVENT_DATA.FIELDS?.ID && entity.EVENT_NAME == 'ONCRMDEALUPDATE'" :href="'https://repacheb.bitrix24.ru/crm/deal/details/' + entity.EVENT_DATA.FIELDS?.ID + '/'" target="_blank" rel="noopener noreferrer"> {{ entity.EVENT_DATA.FIELDS?.ID }} </a>
53
- </td>
54
- <td>
55
- {{ entity.EVENT_DATA }}
56
- </td>
57
- <td>{{ entity.EVENT_ADDITIONAL.user_id }}</td>
58
- <td class="tooltipParent" style="">
59
- <span>Подробней</span>
60
- <span class="tooltip"> {{ entity }}</span>
61
- </td>
62
- </tr>
63
- </tbody>
64
- </table>
65
- </div>
66
- <div v-else>Список сущностей пуст.</div>
67
- </div>
68
-
69
- <!-- Вкладка 2 -->
70
- <div v-if="tabs.tab2">
71
- <div>
72
- <h3>Подписаны на события:</h3>
73
- <table class="main" v-if="subscribedEvents.length">
74
- <thead>
75
- <tr>
76
- <th>Событие</th>
77
- <th>connector_id</th>
78
- <th>offline</th>
79
- <th>Отписаться</th>
80
- </tr>
81
- </thead>
82
- <tbody>
83
- <tr v-for="event in subscribedEvents" :key="event.id">
84
- <td>
85
- {{ event.event }}
86
- </td>
87
- <td>
88
- {{ event.connector_id }}
89
- </td>
90
- <td>
91
- {{ event.offline }}
92
- </td>
93
- <td>
94
- <button @click="unbindSubscribedEvents(event)">Отписаться от событий</button>
95
- </td>
96
- </tr>
97
- </tbody>
98
- </table>
99
- <p v-else>Подписок на события нет.</p>
5
+ <!-- Без клиента Bitrix24 компонент не работает, но и не падает -->
6
+ <p v-if="!b24Client" class="subscriptions__empty">Клиент Bitrix24 не инициализирован</p>
7
+
8
+ <template v-else>
9
+ <div class="subscriptions__tabs">
10
+ <button @click="selectTab('tab1')" :class="{ 'subscriptions__tab--active': tabs.tab1 }">Сущности</button>
11
+ <button @click="selectTab('tab2')" :class="{ 'subscriptions__tab--active': tabs.tab2 }">События</button>
100
12
  </div>
101
13
 
102
- <div>
103
- <h3>На какие события можно подписаться:</h3>
104
- <table class="main" v-if="events.length">
105
- <thead>
106
- <tr>
107
- <th>Событие</th>
108
- <th>Подписаться</th>
109
- </tr>
110
- </thead>
111
- <tbody>
112
- <tr v-for="event in events" :key="event">
113
- <td>{{ event }}</td>
114
- <!-- <td><button @click="subscribeToEvents(event)">Подписаться</button></td> -->
115
- <td><button @click="subscribeToEvents(event)" :disabled="isSubscribed(event)" :class="{ 'disabled-button': isSubscribed(event) }">Подписаться</button></td>
116
- </tr>
117
- </tbody>
118
- </table>
119
- <p v-else>Список событий пуст.</p>
14
+ <!-- Вкладка 1 -->
15
+ <div v-if="tabs.tab1">
16
+ <div>
17
+ <button @click="loadOfflineEntitiesList" :disabled="loading">Обновить</button>
18
+ <!-- Тумблер автообновления сущностей. При наведении показывается подсказка -->
19
+ <label class="subscriptions__switch subscriptions__tooltip-parent switch tooltipParent">
20
+ <input type="checkbox" v-model="autoRefresh" @change="autoRefreshEntities" />
21
+ <span class="subscriptions__slider slider"></span>
22
+ <span class="subscriptions__tooltip tooltip">Автообновление каждые 3 секунды</span>
23
+ </label>
24
+ </div>
25
+
26
+ <!-- Обратная связь показывается и при пустом списке: иначе ошибка загрузки не видна -->
27
+ <div v-if="error" class="subscriptions__message subscriptions__message--error">{{ error }}</div>
28
+ <div v-else-if="message" class="subscriptions__message subscriptions__message--success">{{ message }}</div>
29
+
30
+ <div v-if="entities.list.length > 0">
31
+ <div class="subscriptions__actions">
32
+ <button @click="deleteOfflineEntitiesAll" :disabled="busy">{{ confirmAllLabel }}</button>
33
+ <button @click="deleteOfflineEntitiesAllEmpty" :disabled="busy">{{ confirmEmptyLabel }}</button>
34
+ <span>Всего измененных сущностей {{ entities.total }}</span>
35
+ </div>
36
+
37
+ <table class="main" v-if="entities.list.length">
38
+ <thead>
39
+ <tr>
40
+ <th>#</th>
41
+ <th>Удалить</th>
42
+ <th>Обновить</th>
43
+ <th>Дата</th>
44
+ <th>PROCESS_ID</th>
45
+ <th>EVENT_NAME</th>
46
+ <th>Ссылка</th>
47
+ <th>EVENT_DATA</th>
48
+ <th>who_update</th>
49
+ <th>entity</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ <tr v-for="(entity, index) in entities.list" :key="entity.PROCESS_ID + '-' + entity.ID">
54
+ <td>{{ index + 1 }}</td>
55
+ <!-- Удалить -->
56
+ <td>
57
+ <button @click="deleteOfflineEntities(entity)" :disabled="busy">Удалить</button>
58
+ </td>
59
+ <!-- Изменить -->
60
+ <td>
61
+ <button v-if="entity.EVENT_DATA?.FIELDS?.ID && (entity.EVENT_NAME === 'ONCRMDEALUPDATE' || String(entity.EVENT_NAME || '').includes('ONCRMDYNAMICITEMUPDATE_'))" @click="entityUpdate(entity)" :disabled="busy">Изменить</button>
62
+ </td>
63
+ <td>{{ formatDate(entity.TIMESTAMP_X) }}</td>
64
+ <td>{{ entity.PROCESS_ID }}</td>
65
+ <td>{{ entity.EVENT_NAME }}</td>
66
+ <td>
67
+ <a v-if="entityLink(entity)" :href="entityLink(entity)" target="_blank" rel="noopener noreferrer">{{ entity.EVENT_DATA?.FIELDS?.ID }}</a>
68
+ <span v-else>{{ entity.EVENT_DATA?.FIELDS?.ID }}</span>
69
+ </td>
70
+ <td>
71
+ {{ entity.EVENT_DATA }}
72
+ </td>
73
+ <td>{{ entity.EVENT_ADDITIONAL?.user_id }}</td>
74
+ <td class="subscriptions__tooltip-parent tooltipParent">
75
+ <span>Подробней</span>
76
+ <span class="subscriptions__tooltip tooltip"> {{ entity }}</span>
77
+ </td>
78
+ </tr>
79
+ </tbody>
80
+ </table>
81
+ </div>
82
+ <div v-else>Список сущностей пуст.</div>
120
83
  </div>
121
- </div>
122
84
 
123
- <!-- Вкладка 3 -->
124
- <div v-if="tabs.tab3"></div>
85
+ <!-- Вкладка 2 -->
86
+ <div v-if="tabs.tab2">
87
+ <div v-if="error" class="subscriptions__message subscriptions__message--error">{{ error }}</div>
88
+ <div v-else-if="message" class="subscriptions__message subscriptions__message--success">{{ message }}</div>
89
+
90
+ <div>
91
+ <h3>Подписаны на события:</h3>
92
+ <table class="main" v-if="subscribedEvents.length">
93
+ <thead>
94
+ <tr>
95
+ <th>Событие</th>
96
+ <th>connector_id</th>
97
+ <th>offline</th>
98
+ <th>Отписаться</th>
99
+ </tr>
100
+ </thead>
101
+ <tbody>
102
+ <!-- event.get отдаёт { event, connector_id, offline } без id — ключ собираем из пары событие + коннектор -->
103
+ <tr v-for="event in subscribedEvents" :key="event.event + '-' + event.connector_id">
104
+ <td>
105
+ {{ event.event }}
106
+ </td>
107
+ <td>
108
+ {{ event.connector_id }}
109
+ </td>
110
+ <td>
111
+ {{ event.offline }}
112
+ </td>
113
+ <td>
114
+ <button @click="unbindSubscribedEvents(event)" :disabled="busy">Отписаться от событий</button>
115
+ </td>
116
+ </tr>
117
+ </tbody>
118
+ </table>
119
+ <p v-else>Подписок на события нет.</p>
120
+ </div>
121
+
122
+ <div>
123
+ <h3>На какие события можно подписаться:</h3>
124
+ <table class="main" v-if="events.length">
125
+ <thead>
126
+ <tr>
127
+ <th>Событие</th>
128
+ <th>Подписаться</th>
129
+ </tr>
130
+ </thead>
131
+ <tbody>
132
+ <tr v-for="event in events" :key="event">
133
+ <td>{{ event }}</td>
134
+ <td><button @click="subscribeToEvents(event)" :disabled="busy || isSubscribed(event)" :class="{ 'disabled-button': isSubscribed(event) }">Подписаться</button></td>
135
+ </tr>
136
+ </tbody>
137
+ </table>
138
+ <p v-else>Список событий пуст.</p>
139
+ </div>
140
+ </div>
141
+ </template>
125
142
  </div>
126
143
  </template>
127
144
 
128
145
  <script>
129
- import { DateTime } from "luxon";
146
+ // Интервал автообновления списка сущностей, мс
147
+ const AUTO_REFRESH_MS = 3000;
148
+ // Сколько кнопка удерживает состояние «Точно удалить?», мс
149
+ const CONFIRM_TIMEOUT_MS = 4000;
150
+ // Потолок команд в одном batch — ограничение платформы Bitrix24
151
+ const BATCH_CHUNK_SIZE = 50;
152
+ // Потолок итераций цикла очистки очереди — страховка от вечного цикла
153
+ const MAX_CLEAR_ITERATIONS = 100;
154
+
155
+ // Формат даты создаётся один раз: Intl.DateTimeFormat дорого конструировать на каждую строку таблицы.
156
+ // Указан только hourCycle: при hour12: false часть движков отдаёт 24 вместо 00 для полуночи.
157
+ const DATE_FORMATTER = new Intl.DateTimeFormat("ru-RU", {
158
+ day: "2-digit",
159
+ month: "2-digit",
160
+ hour: "2-digit",
161
+ minute: "2-digit",
162
+ second: "2-digit",
163
+ hourCycle: "h23",
164
+ });
165
+
166
+ /**
167
+ * Приводит метку времени к виду dd.MM HH:mm:ss.
168
+ * Склейка через formatToParts: обычный format() в ru-RU ставит запятую между датой и временем.
169
+ */
170
+ function formatTimestamp(value) {
171
+ if (!value) return "";
172
+ const date = new Date(value);
173
+ if (Number.isNaN(date.getTime())) return String(value);
174
+ const parts = {};
175
+ for (const part of DATE_FORMATTER.formatToParts(date)) {
176
+ parts[part.type] = part.value;
177
+ }
178
+ return `${parts.day}.${parts.month} ${parts.hour}:${parts.minute}:${parts.second}`;
179
+ }
180
+
181
+ /**
182
+ * Режет массив на куски заданного размера.
183
+ */
184
+ function chunkArray(items, size) {
185
+ const chunks = [];
186
+ for (let index = 0; index < items.length; index += size) {
187
+ chunks.push(items.slice(index, index + size));
188
+ }
189
+ return chunks;
190
+ }
130
191
 
131
192
  export default {
132
193
  name: "Subscriptions",
133
- // props: {
134
- // b24: { type: Object, default: null },
135
- // initialAuthConnector: { type: String, default: "" },
136
- // autoStart: { type: Boolean, default: true },
137
- // },
194
+ props: {
195
+ // Клиент Bitrix24 (B24Frame). Приоритетный источник
196
+ b24: { type: Object, default: null },
197
+ // Значение auth_connector для методов работы с событиями
198
+ authConnector: { type: String, default: "" },
199
+ // Переопределение адреса портала: принимается и https://portal.bitrix24.ru, и голый portal.bitrix24.ru
200
+ portalDomain: { type: String, default: "" },
201
+ },
202
+ inject: {
203
+ // Тот же ключ, под которым потребители кладут клиент через app.provide
204
+ injectedB24: { from: "$b24", default: null },
205
+ },
138
206
  data() {
139
207
  return {
208
+ tabs: {
209
+ tab1: true,
210
+ tab2: false,
211
+ },
140
212
  events: [],
141
213
  subscribedEvents: [],
142
214
  entities: {
@@ -144,262 +216,743 @@ export default {
144
216
  total: 0,
145
217
  },
146
218
  autoRefresh: true,
147
- auth_connector: "",
219
+ loading: false,
220
+ busy: false,
221
+ error: "",
222
+ message: "",
223
+ refreshTimerId: null,
224
+ isUnmounted: false,
225
+ // Какая кнопка ждёт второго клика: "" | "all" | "empty"
226
+ confirmAction: "",
227
+ confirmTimerId: null,
148
228
  };
149
229
  },
150
- computed: {},
230
+ computed: {
231
+ // Клиент Bitrix24: prop → inject → глобальный window.$b24
232
+ b24Client() {
233
+ if (this.b24) return this.b24;
234
+ if (this.injectedB24) return this.injectedB24;
235
+ if (typeof window !== "undefined" && window.$b24) return window.$b24;
236
+ return null;
237
+ },
238
+ // Адрес портала без завершающего слэша, "" — определить не удалось
239
+ portalOrigin() {
240
+ try {
241
+ if (this.portalDomain) {
242
+ const domain = String(this.portalDomain).replace(/\/+$/, "");
243
+ return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`;
244
+ }
245
+ const client = this.b24Client;
246
+ if (!client) return "";
247
+ if (typeof client.getTargetOrigin === "function") {
248
+ const origin = client.getTargetOrigin();
249
+ if (origin) return String(origin).replace(/\/+$/, "");
250
+ }
251
+ // getAuthData() отдаёт false, когда токен истёк
252
+ const authData = client.auth && typeof client.auth.getAuthData === "function" ? client.auth.getAuthData() : null;
253
+ if (authData && authData.domain) {
254
+ const domain = String(authData.domain).replace(/\/+$/, "");
255
+ return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`;
256
+ }
257
+ return "";
258
+ } catch (originError) {
259
+ console.error("Не удалось определить адрес портала:", originError);
260
+ return "";
261
+ }
262
+ },
263
+ confirmAllLabel() {
264
+ return this.confirmAction === "all" ? "Точно удалить?" : "Удалить все";
265
+ },
266
+ confirmEmptyLabel() {
267
+ return this.confirmAction === "empty" ? "Точно удалить?" : "Удалить все пустые";
268
+ },
269
+ },
151
270
  methods: {
271
+ selectTab(name) {
272
+ for (const key of Object.keys(this.tabs)) {
273
+ this.tabs[key] = key === name;
274
+ }
275
+ },
152
276
  formatDate(timestamp) {
153
- if (!timestamp) return "";
154
- return DateTime.fromISO(timestamp).toFormat("dd.MM HH:mm:ss");
277
+ return formatTimestamp(timestamp);
155
278
  },
156
279
  pause(ms) {
157
280
  return new Promise((resolve) => setTimeout(resolve, ms));
158
281
  },
282
+
283
+ // --- Обратная связь пользователю ---
284
+
285
+ clearFeedback() {
286
+ this.error = "";
287
+ this.message = "";
288
+ },
289
+ showError(text) {
290
+ this.error = String(text);
291
+ this.message = "";
292
+ },
293
+ showMessage(text) {
294
+ this.message = String(text);
295
+ this.error = "";
296
+ },
297
+ // Текст исключения
298
+ errorText(err) {
299
+ if (err instanceof Error) return err.message;
300
+ if (typeof err === "string") return err;
301
+ return String(err);
302
+ },
303
+ // Текст ошибок ответа SDK (Result / AjaxResult)
304
+ resultErrors(result) {
305
+ let texts = [];
306
+ try {
307
+ if (result && typeof result.getErrorMessagesByKey === "function") {
308
+ // SDK >= 2.0.0: видно, какая команда batch упала
309
+ const byKey = result.getErrorMessagesByKey() || {};
310
+ texts = Object.entries(byKey).map(([key, messages]) => `${key}: ${[].concat(messages).join(", ")}`);
311
+ } else if (result && typeof result.getErrorMessages === "function") {
312
+ texts = result.getErrorMessages() || [];
313
+ }
314
+ } catch (readError) {
315
+ console.error("Не удалось прочитать текст ошибки ответа:", readError);
316
+ }
317
+ const text = texts.filter((item) => item).join("; ");
318
+ return text || "портал не вернул текст ошибки";
319
+ },
320
+
321
+ // --- Подтверждение вторым кликом вместо confirm() ---
322
+
323
+ requestConfirm(action) {
324
+ if (this.confirmAction !== action) {
325
+ this.resetConfirm();
326
+ this.confirmAction = action;
327
+ this.confirmTimerId = setTimeout(() => this.resetConfirm(), CONFIRM_TIMEOUT_MS);
328
+ return false;
329
+ }
330
+ this.resetConfirm();
331
+ return true;
332
+ },
333
+ resetConfirm() {
334
+ if (this.confirmTimerId !== null) {
335
+ clearTimeout(this.confirmTimerId);
336
+ this.confirmTimerId = null;
337
+ }
338
+ this.confirmAction = "";
339
+ },
340
+
341
+ /**
342
+ * Определяет ENTITY_TYPE_ID смарт-процесса: сначала из FIELDS, затем из имени события.
343
+ * Общий разбор для ссылки и для обновления — иначе событие без ENTITY_TYPE_ID в FIELDS
344
+ * уходит в ветку сделки. Возвращает "" когда тип определить не удалось.
345
+ */
346
+ resolveEntityTypeId(entity) {
347
+ const entityTypeId = entity?.EVENT_DATA?.FIELDS?.ENTITY_TYPE_ID;
348
+ if (Number(entityTypeId) > 0) return String(entityTypeId);
349
+ // В части событий FIELDS приходит без ENTITY_TYPE_ID — берём из имени события
350
+ const matched = String(entity?.EVENT_NAME || "").match(/^ONCRMDYNAMICITEMUPDATE_(\d+)$/);
351
+ if (matched && Number(matched[1]) > 0) return matched[1];
352
+ return "";
353
+ },
354
+
355
+ // Ссылка на сущность на текущем портале, "" — построить не удалось
356
+ entityLink(entity) {
357
+ const origin = this.portalOrigin;
358
+ if (!origin) return "";
359
+ const id = entity?.EVENT_DATA?.FIELDS?.ID;
360
+ if (!id) return "";
361
+ const entityTypeId = this.resolveEntityTypeId(entity);
362
+ if (entityTypeId) return `${origin}/crm/type/${entityTypeId}/details/${id}/`;
363
+ if (entity?.EVENT_NAME === "ONCRMDEALUPDATE") return `${origin}/crm/deal/details/${id}/`;
364
+ return "";
365
+ },
366
+
367
+ // --- Работа с событиями Bitrix24 ---
368
+
159
369
  async eventsList() {
160
- console.log("Запускаем eventsList");
370
+ const b24 = this.b24Client;
371
+ if (!b24) {
372
+ this.showError("Клиент Bitrix24 не инициализирован");
373
+ return;
374
+ }
161
375
  try {
162
- let result = await $b24.callMethod("events", {
163
- // auth: access_token,
164
- // scope: "crm",
165
- });
166
- this.events = result.getData().result;
167
- } catch (error) {
168
- console.error("Ошибка при получении списка событий:", error);
376
+ const result = await b24.callMethod("events", {});
377
+ // Сравнение с false намеренно явное и повторяется во всех вызовах ниже:
378
+ // потребители подставляют шим-клиенты, у которых ответ без поля isSuccess.
379
+ // undefined тогда трактуется как отказ, хотя запрос прошёл.
380
+ // У настоящего AjaxResult геттер всегда булев, поэтому === false ловит те же отказы.
381
+ if (result.isSuccess === false) {
382
+ this.showError(`Не удалось получить список событий: ${this.resultErrors(result)}`);
383
+ return;
384
+ }
385
+ this.events = result.getData()?.result || [];
386
+ } catch (callError) {
387
+ console.error("Ошибка при получении списка событий:", callError);
388
+ this.showError(`Ошибка при получении списка событий: ${this.errorText(callError)}`);
169
389
  }
170
390
  },
391
+
171
392
  async subscribeToEvents(event) {
393
+ const b24 = this.b24Client;
394
+ if (!b24) {
395
+ this.showError("Клиент Bitrix24 не инициализирован");
396
+ return;
397
+ }
398
+ this.clearFeedback();
399
+ this.busy = true;
172
400
  try {
173
- await $b24.callMethod("event.bind", {
401
+ // handler обязателен даже для оффлайн-событий: портал его не вызывает,
402
+ // но без параметра event.bind отвечает ошибкой
403
+ const handler = typeof window !== "undefined" && window.location ? window.location.origin : "";
404
+ const result = await b24.callMethod("event.bind", {
174
405
  event: event,
175
406
  event_type: "offline",
176
- // auth_type: "138",
177
- auth_connector: this.auth_connector,
407
+ handler: handler,
408
+ auth_connector: this.authConnector,
178
409
  });
179
- console.log(`Подписались на событие ${event}.`);
180
- } catch (error) {
181
- console.error("Ошибка при подписке на события:", error);
410
+ if (result.isSuccess === false) {
411
+ this.showError(`Не удалось подписаться на событие ${event}: ${this.resultErrors(result)}`);
412
+ return;
413
+ }
414
+ this.showMessage(`Подписались на событие ${event}`);
415
+ await this.pause(1000);
416
+ await this.getSubscribedEvents();
417
+ } catch (callError) {
418
+ console.error("Ошибка при подписке на события:", callError);
419
+ this.showError(`Ошибка при подписке на событие ${event}: ${this.errorText(callError)}`);
420
+ } finally {
421
+ this.busy = false;
182
422
  }
183
- await this.pause(1000);
184
- await this.getSubscribedEvents();
185
423
  },
186
424
 
187
425
  async getSubscribedEvents() {
426
+ const b24 = this.b24Client;
427
+ if (!b24) {
428
+ this.showError("Клиент Bitrix24 не инициализирован");
429
+ return;
430
+ }
188
431
  try {
189
- let result = await $b24.callMethod("event.get", {});
190
- this.subscribedEvents = result.getData().result;
191
- console.log("Подписанные события:", this.subscribedEvents);
192
- } catch (error) {
193
- console.error("Ошибка при получении подписанных событий:", error);
432
+ const result = await b24.callMethod("event.get", {});
433
+ if (result.isSuccess === false) {
434
+ this.showError(`Не удалось получить подписанные события: ${this.resultErrors(result)}`);
435
+ return;
436
+ }
437
+ this.subscribedEvents = result.getData()?.result || [];
438
+ } catch (callError) {
439
+ console.error("Ошибка при получении подписанных событий:", callError);
440
+ this.showError(`Ошибка при получении подписанных событий: ${this.errorText(callError)}`);
194
441
  }
195
442
  },
196
443
 
197
444
  async unbindSubscribedEvents(event) {
445
+ const b24 = this.b24Client;
446
+ if (!b24) {
447
+ this.showError("Клиент Bitrix24 не инициализирован");
448
+ return;
449
+ }
450
+ this.clearFeedback();
451
+ this.busy = true;
198
452
  try {
199
- await $b24.callMethod("event.unbind", {
453
+ const result = await b24.callMethod("event.unbind", {
200
454
  event: event.event,
201
455
  event_type: event.offline == 1 ? "offline" : "online",
202
456
  auth_connector: event.connector_id,
203
457
  });
458
+ if (result.isSuccess === false) {
459
+ this.showError(`Не удалось отписаться от события ${event.event}: ${this.resultErrors(result)}`);
460
+ return;
461
+ }
462
+ this.showMessage(`Отписались от события ${event.event}`);
204
463
  await this.pause(1000);
205
464
  await this.getSubscribedEvents();
206
- } catch (error) {
207
- console.error("Ошибка при отписке от событий:", error);
465
+ } catch (callError) {
466
+ console.error("Ошибка при отписке от событий:", callError);
467
+ this.showError(`Ошибка при отписке от события ${event.event}: ${this.errorText(callError)}`);
468
+ } finally {
469
+ this.busy = false;
208
470
  }
209
471
  },
210
472
 
211
473
  async loadOfflineEntitiesList() {
212
- // console.log(`Запустили loadOfflineEntitiesList`);
213
- let start = 0;
214
- let result = await $b24.callListMethod(
215
- "event.offline.list",
216
- {
217
- start: start,
218
- auth_connector: this.auth_connector,
219
- // clear: 0,
474
+ const b24 = this.b24Client;
475
+ if (!b24) {
476
+ this.showError("Клиент Bitrix24 не инициализирован");
477
+ return;
478
+ }
479
+ this.loading = true;
480
+ try {
481
+ // callListMethod отдаёт плоский массив, а не { result }
482
+ const result = await b24.callListMethod("event.offline.list", {
483
+ start: 0,
484
+ auth_connector: this.authConnector,
485
+ });
486
+ if (result.isSuccess === false) {
487
+ // Старый список полезнее пустого — entities.list не обнуляем
488
+ this.showError(`Не удалось получить список сущностей: ${this.resultErrors(result)}`);
489
+ return;
220
490
  }
221
- // (progress) => {
222
- // console.log(`Загрузка: ${progress.toFixed(0)}%`);
223
- // }
224
- );
225
-
226
- // console.log("Всего сущностей:", result.getData().length);
227
- // console.log("Список сущностей:", result.getData());
228
- this.entities.list = result.getData();
229
- this.entities.total = result.getData().length;
491
+ const rows = result.getData() || [];
492
+ this.entities.list = rows;
493
+ this.entities.total = rows.length;
494
+ } catch (callError) {
495
+ console.error("Ошибка при получении списка оффлайн-сущностей:", callError);
496
+ this.showError(`Ошибка при получении списка сущностей: ${this.errorText(callError)}`);
497
+ } finally {
498
+ this.loading = false;
499
+ }
230
500
  },
231
501
 
232
- async deleteOfflineEntities(entities) {
233
- let result = await $b24.callMethod("event.offline.clear", {
234
- process_id: entities.PROCESS_ID,
235
- id: [entities.ID],
236
- auth_connector: this.auth_connector,
237
- });
238
- console.log("Удалили сущности из оффлан событий:", result.getData());
239
- await this.pause(1000);
240
- await this.loadOfflineEntitiesList();
502
+ /**
503
+ * Снимает одну запись из оффлайн-очереди. Возвращает признак успеха.
504
+ */
505
+ async deleteOfflineEntities(entity) {
506
+ const b24 = this.b24Client;
507
+ if (!b24) {
508
+ this.showError("Клиент Bitrix24 не инициализирован");
509
+ return false;
510
+ }
511
+ // event.offline.clear работает только по process_id: для свободной записи вызов бессмысленный
512
+ if (!entity?.PROCESS_ID) {
513
+ this.showError("У записи пустой PROCESS_ID — снимается кнопкой «Удалить все пустые»");
514
+ return false;
515
+ }
516
+ this.clearFeedback();
517
+ this.busy = true;
518
+ try {
519
+ const result = await b24.callMethod("event.offline.clear", {
520
+ process_id: entity.PROCESS_ID,
521
+ id: [entity.ID],
522
+ auth_connector: this.authConnector,
523
+ });
524
+ if (result.isSuccess === false) {
525
+ this.showError(`Не удалось удалить запись из очереди: ${this.resultErrors(result)}`);
526
+ return false;
527
+ }
528
+ this.showMessage(`Запись ${entity.ID} удалена из очереди`);
529
+ // Портал отдаёт очередь с задержкой
530
+ await this.pause(1000);
531
+ await this.loadOfflineEntitiesList();
532
+ return true;
533
+ } catch (callError) {
534
+ console.error("Ошибка при удалении записи из оффлайн-очереди:", callError);
535
+ this.showError(`Ошибка при удалении записи из очереди: ${this.errorText(callError)}`);
536
+ return false;
537
+ } finally {
538
+ this.busy = false;
539
+ }
241
540
  },
242
541
 
243
542
  async deleteOfflineEntitiesAll() {
244
- if (!confirm("Вы уверены, что хотите удалить все НЕ обработанные сущности? Это действие необратимо!")) {
543
+ if (!this.requestConfirm("all")) return;
544
+ const b24 = this.b24Client;
545
+ if (!b24) {
546
+ this.showError("Клиент Bitrix24 не инициализирован");
245
547
  return;
246
548
  }
549
+ this.clearFeedback();
550
+ this.busy = true;
551
+ // Фоновое обновление конкурирует с удалением и мигает таблицей
552
+ const wasAutoRefresh = this.autoRefresh;
553
+ this.stopAutoRefresh();
247
554
 
248
- let pageAll = this.entities.total / 50 + 1;
249
- console.log("Всего страниц для удаления:", pageAll);
250
- // Цикл по всем страницам для удаления всех сущностей
251
- for (let page = 1; page <= pageAll; page++) {
252
- // Получаем первые сущности
253
- let resAll = await $b24.callMethod("event.offline.list", {
254
- start: 0,
255
- });
256
- // Получаем массив PROCESS_ID
257
- let processIds = resAll
258
- .getData()
259
- .result.map((entity) => entity.PROCESS_ID)
260
- .filter((id) => id);
261
- // Добавляем в массив пустой PROCESS_ID для удаления не обработанных сущностей
262
- // processIds.push("");
263
- console.log("PROCESS_ID для удаления:", processIds);
264
- // Формируем batch запрос для удаления всех сущностей с указанными PROCESS_ID одним запросов
265
- let batchCommands = {};
266
- processIds.forEach((processId, index) => {
267
- batchCommands[`clear_${index}`] = {
268
- method: "event.offline.clear",
269
- params: {
270
- process_id: processId,
271
- },
272
- };
273
- });
274
- console.log("batchCommands:", batchCommands);
275
- let batchResult = await $b24.callBatch(batchCommands);
276
- console.log("Результат batch удаления сущностей:", batchResult.getData());
555
+ const failures = [];
556
+ // Пачки, снятые подтверждённым batch: размер множества — честный счётчик для отчёта
557
+ const clearedProcessIds = new Set();
558
+ let reachedLimit = false;
559
+ let stoppedOnEmpty = "";
560
+ let stoppedOnRepeat = "";
561
+ let iteration = 0;
562
+ try {
563
+ for (; iteration < MAX_CLEAR_ITERATIONS; iteration++) {
564
+ // callListMethod проходит очередь целиком: страница из 50 записей с пустым
565
+ // PROCESS_ID иначе закрывает собой зарезервированные пачки и цикл встаёт
566
+ const pageResult = await b24.callListMethod("event.offline.list", {
567
+ start: 0,
568
+ auth_connector: this.authConnector,
569
+ });
570
+ if (pageResult.isSuccess === false) {
571
+ failures.push(this.resultErrors(pageResult));
572
+ break;
573
+ }
574
+ const rows = pageResult.getData() || [];
575
+ if (!rows.length) break;
576
+
577
+ // Дедупликация обязательна: очередь обычно принадлежит нескольким пачкам
578
+ const processIds = [...new Set(rows.map((row) => row.PROCESS_ID).filter((id) => id))];
579
+ if (!processIds.length) {
580
+ // Свободные записи этим методом не снимаются — иначе цикл крутится до потолка итераций
581
+ stoppedOnEmpty = `Остались записи с пустым PROCESS_ID (${rows.length}) — снимаются кнопкой «Удалить все пустые»`;
582
+ break;
583
+ }
584
+
585
+ // Пачки, снятые на прошлых итерациях, портал может отдавать повторно
586
+ const freshProcessIds = processIds.filter((processId) => !clearedProcessIds.has(processId));
587
+ if (!freshProcessIds.length) {
588
+ stoppedOnRepeat = "портал продолжает отдавать уже снятые пачки";
589
+ break;
590
+ }
591
+
592
+ // Резка по 50: в один batch больше команд платформа не принимает
593
+ for (const chunk of chunkArray(freshProcessIds, BATCH_CHUNK_SIZE)) {
594
+ const commands = {};
595
+ chunk.forEach((processId, index) => {
596
+ commands[`clear_${index}`] = {
597
+ method: "event.offline.clear",
598
+ params: {
599
+ process_id: processId,
600
+ auth_connector: this.authConnector,
601
+ },
602
+ };
603
+ });
604
+ // isHaltOnError: false — одна протухшая пачка не должна валить остальные
605
+ const batchResult = await b24.callBatch(commands, false);
606
+ if (batchResult.isSuccess === false) {
607
+ failures.push(this.resultErrors(batchResult));
608
+ } else {
609
+ // Снятой пачка считается только после подтверждённого batch
610
+ for (const processId of chunk) clearedProcessIds.add(processId);
611
+ }
612
+ }
613
+ // Если пачка не удаляется, следующая итерация увидит те же PROCESS_ID
614
+ if (failures.length) break;
615
+ // Портал отдаёт очередь с задержкой — перечитывать сразу бессмысленно
616
+ await this.pause(1000);
617
+ }
618
+ reachedLimit = iteration >= MAX_CLEAR_ITERATIONS;
619
+ } catch (callError) {
620
+ console.error("Ошибка при массовом удалении сущностей:", callError);
621
+ failures.push(this.errorText(callError));
622
+ } finally {
623
+ this.busy = false;
624
+ if (wasAutoRefresh) this.startAutoRefresh();
625
+ }
626
+
627
+ if (failures.length) {
628
+ this.showError(`Очищено пачек: ${clearedProcessIds.size}. Ошибки: ${failures.join("; ")}`);
629
+ } else if (reachedLimit) {
630
+ this.showError(`Достигнут потолок в ${MAX_CLEAR_ITERATIONS} итераций, очередь очищена не полностью. Очищено пачек: ${clearedProcessIds.size}`);
631
+ } else if (stoppedOnRepeat) {
632
+ this.showError(`Очищено пачек: ${clearedProcessIds.size}. Остановка: ${stoppedOnRepeat}`);
633
+ } else if (stoppedOnEmpty) {
634
+ this.showMessage(`Очищено пачек: ${clearedProcessIds.size}. ${stoppedOnEmpty}`);
635
+ } else {
636
+ this.showMessage(`Очищено пачек: ${clearedProcessIds.size}`);
277
637
  }
638
+
278
639
  await this.pause(500);
640
+ // Размонтирование могло случиться, пока шла очистка
641
+ if (this.isUnmounted) return;
279
642
  await this.loadOfflineEntitiesList();
280
643
  },
281
644
 
645
+ // Сколько записей в текущем списке без PROCESS_ID
646
+ countEmptyProcessId() {
647
+ return this.entities.list.filter((row) => !row?.PROCESS_ID).length;
648
+ },
649
+
282
650
  async deleteOfflineEntitiesAllEmpty() {
283
- if (!confirm("Вы уверены, что хотите удалить все пустые НЕ обработанные сущности? Это действие необратимо!")) {
651
+ if (!this.requestConfirm("empty")) return;
652
+ const b24 = this.b24Client;
653
+ if (!b24) {
654
+ this.showError("Клиент Bitrix24 не инициализирован");
284
655
  return;
285
656
  }
286
- // Удаляем последний раз, чтобы удалить сущности с пустым PROCESS_ID
287
- let finalResult = await $b24.callMethod("event.offline.clear", {
288
- process_id: "",
289
- });
290
- console.log("Результат удаления сущностей с пустым PROCESS_ID:", finalResult.getData());
657
+ this.clearFeedback();
658
+ this.busy = true;
659
+ try {
660
+ // Считаем свободные записи до вызова: портал отвечает успехом, даже когда ничего не снял
661
+ const emptyBefore = this.countEmptyProcessId();
662
+ // Снимаем записи с пустым PROCESS_ID — по process_id их не удалить
663
+ const result = await b24.callMethod("event.offline.clear", {
664
+ process_id: "",
665
+ auth_connector: this.authConnector,
666
+ });
667
+ if (result.isSuccess === false) {
668
+ this.showError(`Не удалось удалить записи с пустым PROCESS_ID: ${this.resultErrors(result)}`);
669
+ return;
670
+ }
671
+ await this.pause(500);
672
+ await this.loadOfflineEntitiesList();
673
+ // Ошибку перезагрузки не затираем: без свежего списка разницу считать не по чему
674
+ if (this.error) return;
675
+ const removedCount = emptyBefore - this.countEmptyProcessId();
676
+ if (removedCount > 0) {
677
+ this.showMessage(`Снято записей: ${removedCount}`);
678
+ } else {
679
+ this.showMessage("Портал не снял ни одной записи с пустым PROCESS_ID");
680
+ }
681
+ } catch (callError) {
682
+ console.error("Ошибка при удалении сущностей с пустым PROCESS_ID:", callError);
683
+ this.showError(`Ошибка при удалении записей с пустым PROCESS_ID: ${this.errorText(callError)}`);
684
+ } finally {
685
+ this.busy = false;
686
+ }
291
687
  },
292
688
 
293
689
  isSubscribed(event) {
294
- // console.log(event);
295
- // console.log(this.subscribedEvents);
296
690
  return this.subscribedEvents.some((subEvent) => subEvent.event == event);
297
691
  },
298
692
 
693
+ // --- Автообновление ---
694
+
695
+ startAutoRefresh() {
696
+ // Защита от двойного таймера
697
+ this.stopAutoRefresh();
698
+ if (!this.autoRefresh || this.isUnmounted) return;
699
+ this.refreshTimerId = setTimeout(async () => {
700
+ this.refreshTimerId = null;
701
+ if (this.isUnmounted) return;
702
+ await this.loadOfflineEntitiesList();
703
+ // Размонтирование могло случиться, пока висел запрос
704
+ if (this.isUnmounted) return;
705
+ this.startAutoRefresh();
706
+ }, AUTO_REFRESH_MS);
707
+ },
708
+
709
+ stopAutoRefresh() {
710
+ if (this.refreshTimerId !== null) {
711
+ clearTimeout(this.refreshTimerId);
712
+ this.refreshTimerId = null;
713
+ }
714
+ },
715
+
716
+ // Обработчик @change тумблера: выключение отменяет уже отложенный вызов
299
717
  autoRefreshEntities() {
300
- // console.log("Запущена функция автообновления сущностей autoRefreshEntities, проверяем состояние autoRefresh");
301
718
  if (this.autoRefresh) {
302
- // console.log("Автообновление включено. Обновляем список сущностей через 3 секунд.");
303
- setTimeout(async () => {
304
- await this.loadOfflineEntitiesList();
305
- this.autoRefreshEntities();
306
- }, 3000);
719
+ this.startAutoRefresh();
307
720
  } else {
308
- // console.log("Автообновление autoRefreshEntities выключено.");
721
+ this.stopAutoRefresh();
309
722
  }
310
723
  },
311
724
 
725
+ /**
726
+ * Перезаписывает сущность теми же данными и снимает её из оффлайн-очереди.
727
+ */
312
728
  async entityUpdate(eventEntity) {
313
- let error = false;
314
- console.log("Запущена функция entityUpdate для изменения сущности с ID:", eventEntity.EVENT_DATA.FIELDS?.ID);
729
+ const b24 = this.b24Client;
730
+ if (!b24) {
731
+ this.showError("Клиент Bitrix24 не инициализирован");
732
+ return;
733
+ }
734
+ const fields = eventEntity?.EVENT_DATA?.FIELDS || {};
735
+ const entityId = fields.ID;
736
+ if (!entityId) {
737
+ this.showError("У события нет ID сущности");
738
+ return;
739
+ }
740
+
741
+ // Тип берётся тем же разбором, что и для ссылки: FIELDS плюс имя события
742
+ const entityTypeId = this.resolveEntityTypeId(eventEntity);
743
+
744
+ let failed = false;
745
+ this.clearFeedback();
746
+ this.busy = true;
315
747
  try {
316
- // Если смарт процесс
317
- if (eventEntity.EVENT_DATA.FIELDS?.ENTITY_TYPE_ID && eventEntity.EVENT_DATA.FIELDS?.ENTITY_TYPE_ID > 0) {
318
- // Обработка смарт процесса
319
- let entity = await $b24.callMethod("crm.item.get", {
320
- entityTypeId: eventEntity.EVENT_DATA.FIELDS.ENTITY_TYPE_ID,
321
- id: eventEntity.EVENT_DATA.FIELDS.ID,
748
+ if (entityTypeId) {
749
+ // Смарт-процесс
750
+ const getResult = await b24.callMethod("crm.item.get", {
751
+ entityTypeId: entityTypeId,
752
+ id: entityId,
322
753
  });
323
- entity = entity.getData().result.item;
324
- console.log("Данные смарт процесса до изменения:", entity);
325
- if (entity.id == eventEntity.EVENT_DATA.FIELDS?.ID) {
326
- // обновляем смарт процесс через метод crm.item.update. Меняем в ней TITLE на то же значение что и было
327
- let updateResult = await $b24.callMethod("crm.item.update", {
328
- entityTypeId: eventEntity.EVENT_DATA.FIELDS.ENTITY_TYPE_ID,
329
- id: eventEntity.EVENT_DATA.FIELDS.ID,
754
+ const item = getResult.getData()?.result?.item;
755
+ // Портал отдаёт идентификаторы то строкой, то числом
756
+ if (getResult.isSuccess === false || !item || String(item.id) !== String(entityId)) {
757
+ failed = true;
758
+ const reason = getResult.isSuccess === false ? this.resultErrors(getResult) : "портал вернул другую сущность или пустой ответ";
759
+ this.showError(`Не удалось получить смарт-процесс ${entityId}: ${reason}`);
760
+ } else {
761
+ const updateResult = await b24.callMethod("crm.item.update", {
762
+ entityTypeId: entityTypeId,
763
+ id: entityId,
330
764
  fields: {
331
- title: entity.title + "123",
765
+ title: item.title,
332
766
  },
333
767
  });
334
- console.log("Результат обновления смарт процесса:", updateResult.getData());
335
- if (updateResult.id === entity.id) {
336
- console.log("Сущность успешно обновлена.");
337
- error = true;
338
- } else {
339
- console.error("Ошибка при обновлении сущности.");
340
- error = true;
768
+ const updatedId = updateResult.getData()?.result?.item?.id;
769
+ if (updateResult.isSuccess === false || String(updatedId) !== String(entityId)) {
770
+ failed = true;
771
+ const reason = updateResult.isSuccess === false ? this.resultErrors(updateResult) : "портал вернул другой идентификатор";
772
+ this.showError(`Не удалось обновить смарт-процесс ${entityId}: ${reason}`);
341
773
  }
342
- } else {
343
- error = true;
344
774
  }
345
- } else {
346
- // Если сделка, то получаем значение сделки используя метод crm.deal.get
347
- let deal = await $b24.callMethod("crm.deal.get", {
348
- id: eventEntity.EVENT_DATA.FIELDS?.ID,
775
+ } else if (eventEntity?.EVENT_NAME === "ONCRMDEALUPDATE") {
776
+ // Сделка
777
+ const getResult = await b24.callMethod("crm.deal.get", {
778
+ id: entityId,
349
779
  });
350
- deal = deal.getData().result;
351
- console.log("Данные сделки до изменения:", deal);
352
-
353
- if (deal.ID == eventEntity.EVENT_DATA.FIELDS?.ID) {
354
- // обновляем сделку через метод crm.deal.update. Меняем в ней title на то же значение что и было
355
- let updateResult = await $b24.callMethod("crm.deal.update", {
356
- id: eventEntity.EVENT_DATA.FIELDS?.ID,
780
+ const deal = getResult.getData()?.result;
781
+ if (getResult.isSuccess === false || !deal || String(deal.ID) !== String(entityId)) {
782
+ failed = true;
783
+ const reason = getResult.isSuccess === false ? this.resultErrors(getResult) : "портал вернул другую сделку или пустой ответ";
784
+ this.showError(`Не удалось получить сделку ${entityId}: ${reason}`);
785
+ } else {
786
+ const updateResult = await b24.callMethod("crm.deal.update", {
787
+ id: entityId,
357
788
  fields: {
358
789
  TITLE: deal.TITLE,
359
790
  },
360
791
  });
361
- console.log("Результат обновления сделки:", updateResult.getData());
362
- updateResult = updateResult.getData().result;
363
- if (updateResult === true) {
364
- console.log("Сущность успешно обновлена.");
365
- } else {
366
- console.error("Ошибка при обновлении сущности.");
367
- error = true;
792
+ // crm.deal.update возвращает { result: true }
793
+ if (updateResult.getData()?.result !== true) {
794
+ failed = true;
795
+ const reason = updateResult.isSuccess === false ? this.resultErrors(updateResult) : "портал не подтвердил обновление";
796
+ this.showError(`Не удалось обновить сделку ${entityId}: ${reason}`);
368
797
  }
369
- } else {
370
- error = true;
371
798
  }
799
+ } else {
800
+ // Ни ENTITY_TYPE_ID, ни имя события не говорят, что это за сущность —
801
+ // вслепую дёргать crm.deal.get нельзя: снимется чужая запись с зелёным успехом
802
+ failed = true;
803
+ this.showError(`Не удалось определить тип сущности для события ${eventEntity?.EVENT_NAME || "без имени"}`);
372
804
  }
373
- } catch (error) {
374
- console.error("Ошибка при обновлении сущности:", error);
375
- error = true;
805
+ } catch (callError) {
806
+ failed = true;
807
+ console.error("Ошибка при обновлении сущности:", callError);
808
+ this.showError(`Ошибка при обновлении сущности ${entityId}: ${this.errorText(callError)}`);
809
+ } finally {
810
+ this.busy = false;
376
811
  }
377
- if (!error) {
378
- // Удаляем сущность из оффлайн очереди
379
- await this.deleteOfflineEntities(eventEntity);
380
- } else {
812
+
813
+ if (!failed) {
814
+ const removed = await this.deleteOfflineEntities(eventEntity);
815
+ if (removed) {
816
+ this.showMessage(`Сущность ${entityId} обновлена и удалена из очереди`);
817
+ } else {
818
+ // Оба факта в одной строке: иначе на экране остаётся только претензия
819
+ // к записи очереди, и кажется, что сущность не обновилась
820
+ const reason = this.error || "портал не подтвердил снятие записи";
821
+ this.showError(`Сущность ${entityId} обновлена, но запись не снята из очереди: ${reason}`);
822
+ }
823
+ } else if (!this.error) {
824
+ this.showError("Сущность не обновлена, из очереди не удалена");
381
825
  }
382
826
  },
383
827
  },
384
- watch: {},
385
828
  mounted() {
829
+ // Три загрузки идут параллельно — await здесь намеренно не ставится
386
830
  this.getSubscribedEvents();
387
831
  this.eventsList();
388
832
  this.loadOfflineEntitiesList();
389
833
  this.autoRefreshEntities();
390
834
  },
835
+ beforeUnmount() {
836
+ this.isUnmounted = true;
837
+ this.stopAutoRefresh();
838
+ this.resetConfirm();
839
+ },
391
840
  };
392
841
  </script>
393
842
 
394
843
  <style>
844
+ /* Цвета вынесены в кастомные свойства: потребитель перекрашивает компонент без борьбы за специфичность */
845
+ .subscriptions {
846
+ --subscriptions-switch-off: #ccc;
847
+ --subscriptions-switch-on: #4caf50;
848
+ --subscriptions-switch-knob: #fff;
849
+ --subscriptions-tooltip-bg: #555;
850
+ --subscriptions-tooltip-ink: #fff;
851
+ --subscriptions-error-bg: #ffebee;
852
+ --subscriptions-error-ink: #c62828;
853
+ --subscriptions-success-bg: #e8f5e9;
854
+ --subscriptions-success-ink: #2e7d32;
855
+ }
856
+
857
+ .subscriptions__tabs {
858
+ margin-bottom: 24px;
859
+ }
860
+
861
+ .subscriptions__tab--active {
862
+ font-weight: 600;
863
+ }
864
+
865
+ .subscriptions__empty {
866
+ padding: 12px;
867
+ color: var(--subscriptions-error-ink, #c62828);
868
+ }
869
+
870
+ .subscriptions__actions {
871
+ display: flex;
872
+ align-items: center;
873
+ gap: 8px;
874
+ margin: 12px 0;
875
+ }
876
+
877
+ .subscriptions__message {
878
+ margin: 12px 0;
879
+ padding: 8px 12px;
880
+ border-radius: 4px;
881
+ font-size: 14px;
882
+ }
883
+
884
+ .subscriptions__message--success {
885
+ background: var(--subscriptions-success-bg, #e8f5e9);
886
+ color: var(--subscriptions-success-ink, #2e7d32);
887
+ border: 1px solid #a5d6a7;
888
+ }
889
+
890
+ .subscriptions__message--error {
891
+ background: var(--subscriptions-error-bg, #ffebee);
892
+ color: var(--subscriptions-error-ink, #c62828);
893
+ border: 1px solid #ef9a9a;
894
+ }
895
+
896
+ /* Тумблер автообновления */
897
+ .subscriptions__switch {
898
+ position: relative;
899
+ display: inline-block;
900
+ width: 44px;
901
+ height: 24px;
902
+ vertical-align: middle;
903
+ }
904
+
905
+ .subscriptions__switch input {
906
+ opacity: 0;
907
+ width: 0;
908
+ height: 0;
909
+ }
910
+
911
+ .subscriptions__slider {
912
+ position: absolute;
913
+ cursor: pointer;
914
+ top: 0;
915
+ left: 0;
916
+ right: 0;
917
+ bottom: 0;
918
+ background-color: var(--subscriptions-switch-off, #ccc);
919
+ transition: 0.3s;
920
+ border-radius: 24px;
921
+ }
922
+
923
+ .subscriptions__slider::before {
924
+ content: "";
925
+ position: absolute;
926
+ height: 18px;
927
+ width: 18px;
928
+ left: 3px;
929
+ bottom: 3px;
930
+ background-color: var(--subscriptions-switch-knob, #fff);
931
+ transition: 0.3s;
932
+ border-radius: 50%;
933
+ }
934
+
935
+ input:checked + .subscriptions__slider {
936
+ background-color: var(--subscriptions-switch-on, #4caf50);
937
+ }
938
+
939
+ input:checked + .subscriptions__slider::before {
940
+ transform: translateX(20px);
941
+ }
942
+
943
+ /* Подсказки. Старые имена .tooltipParent и .tooltip — алиасы для мостов совместимости
944
+ у потребителей (b24_change_entity, b24_desktop), снять в следующей minor после их обновления */
945
+ .subscriptions__tooltip-parent,
395
946
  .tooltipParent {
396
947
  position: relative; /* Чтобы подсказка позиционировалась относительно родителя */
397
948
  cursor: pointer;
398
949
  }
950
+
951
+ .subscriptions__tooltip,
399
952
  .tooltip {
400
953
  visibility: hidden;
401
- background-color: #555;
402
- color: #fff;
954
+ background-color: var(--subscriptions-tooltip-bg, #555);
955
+ color: var(--subscriptions-tooltip-ink, #fff);
403
956
  text-align: center;
404
957
  border-radius: 5px;
405
958
  padding: 5px;
@@ -413,10 +966,9 @@ export default {
413
966
  max-width: 400px;
414
967
  min-width: 200px;
415
968
  word-wrap: break-word; /* Перенос слов при необходимости */
416
- /* white-space: nowrap; Запретить перенос слов */
417
969
  }
418
970
 
419
- /* .switch:hover .tooltip, */
971
+ .subscriptions__tooltip-parent:hover .subscriptions__tooltip,
420
972
  .tooltipParent:hover .tooltip {
421
973
  visibility: visible;
422
974
  opacity: 1;