Автоматическое создание заданий для внешнего продвижения с выбором товаров и настройками
Size
46.9 KB
Version
1.1.139
Created
Mar 13, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Ozon Массовое создание заданий
3// @description Автоматическое создание заданий для внешнего продвижения с выбором товаров и настройками
4// @version 1.1.139
5// @match https://*.seller.ozon.ru/*
6// @icon https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.xmlHttpRequest
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 console.log('🚀 Ozon Массовое создание заданий - запущен');
15
16 // ============================================
17 // АРХИТЕКТУРА РАСШИРЕНИЯ - ВАЖНО!
18 // ============================================
19 /*
20 * ОСНОВНЫЕ ПРИНЦИПЫ РАБОТЫ:
21 *
22 * 1. ОДНА РАБОЧАЯ ВКЛАДКА:
23 * - ГЛАВНАЯ вкладка - страница списка заданий
24 * * Показывает модальное окно с настройками
25 * * При запуске переходит на страницу создания задания
26 * * После создания задания возвращается на страницу списка
27 * * Цикл повторяется до тех пор, пока не закончатся товары
28 *
29 * 2. ОБМЕН ДАННЫМИ через GM.getValue/GM.setValue:
30 * - ozon_current_task: настройки и счётчик текущего процесса
31 * - ozon_task_result: результат создания задания (success/error)
32 * - ozon_processed_skus: список обработанных SKU (не создавать дубли)
33 *
34 * 3. ПАГИНАЦИЯ товаров:
35 * - Последняя кнопка с классом t0c110-a1.table-500 - это стрелка "вперёд"
36 * - Если все товары на странице обработаны - переходим на следующую
37 * - Рекурсивно ищем необработанный товар на всех страницах
38 *
39 * 4. ЛОГИКА РАБОТЫ:
40 * - Пользователь нажимает "Запустить" → переход на страницу создания
41 * - Создаётся задание → переход на страницу списка
42 * - Проверяется результат → переход на страницу создания для следующего задания
43 * - Цикл продолжается в одной вкладке
44 */
45
46 // ============================================
47 // КОНСТАНТЫ И КОНФИГУРАЦИЯ
48 // ============================================
49
50 const CONFIG = {
51 PAGES: {
52 LIST: 'https://seller.ozon.ru/app/advertisement/product/external-promo',
53 CREATE: 'https://seller.ozon.ru/app/advertisement/product/external-promo/create'
54 },
55 SELECTORS: {
56 // Селекторы для страницы списка заданий
57 LIST_PAGE: {
58 CREATE_BUTTON: 'button[data-context-help-target="external_promo_create_task"]',
59 HEADER: 'h1.c7r110-a'
60 },
61 // Селекторы для страницы создания задания
62 CREATE_PAGE: {
63 POST_TYPE_CLIPS: 'input[type="radio"][name="postType"][value="TASK_POST_TYPE_CLIP"]',
64 POST_TYPE_POSTS: 'input[type="radio"][name="postType"][value="TASK_POST_TYPE_POST"]',
65 BID_INPUT: 'div[data-context-help-target="external_promo_create_task_add_bid"] input',
66 SAMPLES_INPUT: 'div[data-context-help-target="external_promo_create_task_add_sample"] input',
67 ADD_PRODUCT_BUTTON: 'button.c9r110-a.c9r110-a4.body-500-true.c9r110-b4.c9r110-b9.c9r110-b0',
68 SAVE_BUTTON: 'button[type="submit"]'
69 },
70 // Селекторы для модального окна с товарами
71 MODAL: {
72 DIALOG: '.sc1110-a.sc1110-a0',
73 PRODUCT_ROWS: 'tbody tr.ct5110-c',
74 PRODUCT_ADD_BUTTON: 'td button[type="submit"].c9r110-a.c9r110-a4',
75 NEXT_PAGE_BUTTON: 'ul.t0c110-a button.t0c110-a1.table-500:last-child',
76 PAGINATION: 'ul.t0c110-a'
77 }
78 },
79 STORAGE_KEYS: {
80 PROCESSED_SKUS: 'ozon_processed_skus',
81 SETTINGS: 'ozon_bulk_settings',
82 IS_RUNNING: 'ozon_is_running'
83 },
84 DELAYS: {
85 AFTER_CLICK: 1000,
86 AFTER_PAGE_LOAD: 2000,
87 BETWEEN_ACTIONS: 500
88 }
89 };
90
91 // ============================================
92 // УТИЛИТЫ
93 // ============================================
94
95 // Функция задержки
96 const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
97
98 // Функция для безопасного получения элемента с ожиданием
99 async function waitForElement(selector, timeout = 10000) {
100 const startTime = Date.now();
101 while (Date.now() - startTime < timeout) {
102 const element = document.querySelector(selector);
103 if (element) {
104 console.log(`✅ Элемент найден: ${selector}`);
105 return element;
106 }
107 await delay(100);
108 }
109 console.error(`❌ Элемент не найден за ${timeout}мс: ${selector}`);
110 return null;
111 }
112
113 // Функция для клика с задержкой
114 async function clickElement(element, description = '') {
115 if (!element) {
116 console.error(`❌ Не могу кликнуть: элемент не найден ${description}`);
117 return false;
118 }
119 console.log(`🖱️ Клик: ${description}`);
120 element.click();
121 await delay(CONFIG.DELAYS.AFTER_CLICK);
122 return true;
123 }
124
125 // ============================================
126 // ХРАНИЛИЩЕ ДАННЫХ
127 // ============================================
128
129 class Storage {
130 // Получить обработанные SKU
131 static async getProcessedSkus() {
132 const data = await GM.getValue(CONFIG.STORAGE_KEYS.PROCESSED_SKUS, '[]');
133 return JSON.parse(data);
134 }
135
136 // Добавить обработанный SKU
137 static async addProcessedSku(sku) {
138 const skus = await this.getProcessedSkus();
139 if (!skus.includes(sku)) {
140 skus.push(sku);
141 await GM.setValue(CONFIG.STORAGE_KEYS.PROCESSED_SKUS, JSON.stringify(skus));
142 console.log(`💾 SKU сохранён: ${sku}`);
143 }
144 }
145
146 // Проверить, обработан ли SKU
147 static async isSkuProcessed(sku) {
148 const skus = await this.getProcessedSkus();
149 return skus.includes(sku);
150 }
151
152 // Получить настройки
153 static async getSettings() {
154 const data = await GM.getValue(CONFIG.STORAGE_KEYS.SETTINGS, null);
155 return data ? JSON.parse(data) : null;
156 }
157
158 // Сохранить настройки
159 static async saveSettings(settings) {
160 await GM.setValue(CONFIG.STORAGE_KEYS.SETTINGS, JSON.stringify(settings));
161 console.log('💾 Настройки сохранены:', settings);
162 }
163
164 // Проверить, запущен ли процесс
165 static async isRunning() {
166 return await GM.getValue(CONFIG.STORAGE_KEYS.IS_RUNNING, false);
167 }
168
169 // Установить статус запуска
170 static async setRunning(status) {
171 await GM.setValue(CONFIG.STORAGE_KEYS.IS_RUNNING, status);
172 }
173 }
174
175 // ============================================
176 // UI КОМПОНЕНТЫ
177 // ============================================
178
179 class UI {
180 // Создать кнопку "Массовое создание заданий"
181 static createBulkButton() {
182 const button = document.createElement('button');
183 button.id = 'ozon-bulk-create-btn';
184 button.className = 'c9r110-a c9r110-a5 body-500-true c9r110-b2 c9r110-b9 c9r110-b0';
185 button.style.marginLeft = '10px';
186 button.innerHTML = `
187 <div class="c9r110-a0">
188 <div class="c9r110-a1 c9r110-a2">
189 <span class="c9r110-a2">⚡ Массовое создание заданий</span>
190 </div>
191 </div>
192 `;
193 return button;
194 }
195
196 // Создать модальное окно настроек
197 static createSettingsModal() {
198 const modal = document.createElement('div');
199 modal.id = 'ozon-bulk-settings-modal';
200 modal.style.cssText = `
201 position: fixed;
202 top: 0;
203 left: 0;
204 width: 100%;
205 height: 100%;
206 background: rgba(0, 0, 0, 0.5);
207 display: flex;
208 align-items: center;
209 justify-content: center;
210 z-index: 100000;
211 `;
212
213 modal.innerHTML = `
214 <div style="
215 background: white;
216 border-radius: 12px;
217 padding: 30px;
218 width: 500px;
219 max-width: 90%;
220 box-shadow: 0 4px 20px rgba(0,0,0,0.3);
221 ">
222 <h2 style="margin: 0 0 20px 0; font-size: 24px; color: #333;">
223 ⚙️ Настройки массового создания
224 </h2>
225
226 <div style="margin-bottom: 20px;">
227 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #555;">
228 Формат публикаций:
229 </label>
230 <select id="ozon-post-type" style="
231 width: 100%;
232 padding: 10px;
233 border: 1px solid #ddd;
234 border-radius: 6px;
235 font-size: 14px;
236 ">
237 <option value="TASK_POST_TYPE_CLIP">Клипы</option>
238 <option value="TASK_POST_TYPE_POST">Посты</option>
239 </select>
240 </div>
241
242 <div style="margin-bottom: 20px;">
243 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #555;">
244 Ставка за заказ (%):
245 </label>
246 <input type="number" id="ozon-bid" min="10" step="1" placeholder="Минимум 10%" style="
247 width: 100%;
248 padding: 10px;
249 border: 1px solid #ddd;
250 border-radius: 6px;
251 font-size: 14px;
252 box-sizing: border-box;
253 ">
254 <small style="color: #888; font-size: 12px;">Минимальная ставка — 10%</small>
255 </div>
256
257 <div style="margin-bottom: 25px;">
258 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: #555;">
259 Количество образцов:
260 </label>
261 <input type="number" id="ozon-samples" min="0" step="1" placeholder="0" style="
262 width: 100%;
263 padding: 10px;
264 border: 1px solid #ddd;
265 border-radius: 6px;
266 font-size: 14px;
267 box-sizing: border-box;
268 ">
269 <small style="color: #888; font-size: 12px;">Можно использовать для товаров дешевле 5 000 ₽</small>
270 </div>
271
272 <div style="margin-bottom: 25px; padding: 15px; background: #fff3cd; border-radius: 6px; border: 1px solid #ffc107;">
273 <div style="margin-bottom: 10px;">
274 <strong style="color: #856404;">⚠️ Обработанные товары</strong>
275 </div>
276 <p style="margin: 0 0 10px 0; font-size: 13px; color: #856404;">
277 Расширение запоминает товары, для которых уже создали задания, и пропускает их при повторном запуске.
278 </p>
279 <button id="ozon-reset-btn" style="
280 padding: 8px 16px;
281 border: 1px solid #ffc107;
282 background: white;
283 color: #856404;
284 border-radius: 6px;
285 cursor: pointer;
286 font-size: 13px;
287 font-weight: 500;
288 ">
289 🔄 Сбросить список обработанных товаров
290 </button>
291 </div>
292
293 <div id="ozon-progress-container" style="display: none; margin-bottom: 20px;">
294 <div style="background: #f0f0f0; border-radius: 6px; padding: 15px;">
295 <div style="margin-bottom: 10px;">
296 <strong>Прогресс:</strong> <span id="ozon-progress-text">0 / 0</span>
297 </div>
298 <div style="background: #ddd; height: 8px; border-radius: 4px; overflow: hidden;">
299 <div id="ozon-progress-bar" style="
300 background: linear-gradient(90deg, #4CAF50, #45a049);
301 height: 100%;
302 width: 0%;
303 transition: width 0.3s ease;
304 "></div>
305 </div>
306 <div style="margin-top: 10px; font-size: 12px; color: #666;">
307 <span id="ozon-current-action">Ожидание...</span>
308 </div>
309 </div>
310 </div>
311
312 <div style="display: flex; gap: 10px; justify-content: flex-end;">
313 <button id="ozon-cancel-btn" style="
314 padding: 10px 20px;
315 border: 1px solid #ddd;
316 background: white;
317 border-radius: 6px;
318 cursor: pointer;
319 font-size: 14px;
320 font-weight: 500;
321 ">
322 Отмена
323 </button>
324 <button id="ozon-start-btn" style="
325 padding: 10px 20px;
326 border: none;
327 background: #005bff;
328 color: white;
329 border-radius: 6px;
330 cursor: pointer;
331 font-size: 14px;
332 font-weight: 500;
333 ">
334 🚀 Запустить
335 </button>
336 <button id="ozon-stop-btn" style="
337 padding: 10px 20px;
338 border: none;
339 background: #ff4444;
340 color: white;
341 border-radius: 6px;
342 cursor: pointer;
343 font-size: 14px;
344 font-weight: 500;
345 display: none;
346 ">
347 ⏹️ Остановить
348 </button>
349 </div>
350 </div>
351 `;
352
353 return modal;
354 }
355
356 // Показать модальное окно
357 static showModal(modal) {
358 document.body.appendChild(modal);
359 }
360
361 // Скрыть модальное окно
362 static hideModal(modal) {
363 if (modal && modal.parentNode) {
364 modal.parentNode.removeChild(modal);
365 }
366 }
367
368 // Обновить прогресс
369 static updateProgress(current, total, action = '') {
370 const progressContainer = document.getElementById('ozon-progress-container');
371 const progressText = document.getElementById('ozon-progress-text');
372 const progressBar = document.getElementById('ozon-progress-bar');
373 const currentAction = document.getElementById('ozon-current-action');
374
375 if (progressContainer) {
376 progressContainer.style.display = 'block';
377 progressText.textContent = `${current} / ${total}`;
378 const percentage = total > 0 ? (current / total) * 100 : 0;
379 progressBar.style.width = `${percentage}%`;
380 if (currentAction && action) {
381 currentAction.textContent = action;
382 }
383 }
384 }
385
386 // Показать кнопку остановки
387 static showStopButton() {
388 const startBtn = document.getElementById('ozon-start-btn');
389 const stopBtn = document.getElementById('ozon-stop-btn');
390 if (startBtn) startBtn.style.display = 'none';
391 if (stopBtn) stopBtn.style.display = 'block';
392 }
393
394 // Показать кнопку запуска
395 static showStartButton() {
396 const startBtn = document.getElementById('ozon-start-btn');
397 const stopBtn = document.getElementById('ozon-stop-btn');
398 if (startBtn) startBtn.style.display = 'block';
399 if (stopBtn) stopBtn.style.display = 'none';
400 }
401 }
402
403 // ============================================
404 // ОСНОВНАЯ ЛОГИКА АВТОМАТИЗАЦИИ
405 // ============================================
406
407 class BulkTaskCreator {
408 constructor(settings) {
409 this.settings = settings;
410 this.currentPage = 1;
411 this.processedCount = 0;
412 this.totalProducts = 0;
413 this.shouldStop = false;
414 }
415
416 // Остановить процесс
417 stop() {
418 this.shouldStop = true;
419 console.log('⏹️ Остановка процесса...');
420 }
421
422 // Главная функция запуска
423 async start() {
424 console.log('🚀 Начало массового создания заданий');
425 console.log('⚙️ Настройки:', this.settings);
426
427 await Storage.setRunning(true);
428
429 try {
430 // Открываем страницу создания задания в новой вкладке
431 await this.createTasksLoop();
432 } catch (error) {
433 console.error('❌ Ошибка при создании заданий:', error);
434 alert('Произошла ошибка: ' + error.message);
435 } finally {
436 await Storage.setRunning(false);
437 UI.showStartButton();
438 }
439 }
440
441 // Цикл создания заданий
442 async createTasksLoop() {
443 console.log(`\n📄 Создание задания ${this.processedCount + 1}`);
444
445 // Сохраняем текущее состояние
446 await GM.setValue('ozon_current_task', JSON.stringify({
447 settings: this.settings,
448 processedCount: this.processedCount,
449 shouldContinue: true
450 }));
451
452 // Очищаем предыдущий результат
453 await GM.setValue('ozon_task_result', null);
454
455 // Открываем страницу создания в НОВОЙ вкладке (только первый раз)
456 console.log('🔗 Открываем новую вкладку для создания заданий...');
457 await GM.openInTab(CONFIG.PAGES.CREATE, false);
458
459 // Главная вкладка остаётся открытой и отслеживает прогресс
460 await this.monitorProgress();
461 }
462
463 // Отслеживание прогресса в главной вкладке
464 async monitorProgress() {
465 console.log('👀 Отслеживаем прогресс в главной вкладке...');
466
467 while (!this.shouldStop) {
468 await delay(1000);
469
470 // Проверяем результат
471 const result = await GM.getValue('ozon_task_result', null);
472 if (result) {
473 const resultData = JSON.parse(result);
474
475 if (resultData.success) {
476 this.processedCount++;
477 UI.updateProgress(this.processedCount, this.totalProducts, `Создано заданий: ${this.processedCount}`);
478 console.log(`✅ Задание ${this.processedCount} создано успешно`);
479
480 // Очищаем результат
481 await GM.setValue('ozon_task_result', null);
482
483 // Обновляем счётчик для рабочей вкладки
484 await GM.setValue('ozon_current_task', JSON.stringify({
485 settings: this.settings,
486 processedCount: this.processedCount,
487 shouldContinue: true
488 }));
489
490 } else if (resultData.noMoreProducts) {
491 console.log('✅ Все товары обработаны');
492 alert('Готово! Создано заданий: ' + this.processedCount);
493 break;
494 } else if (resultData.error) {
495 console.error('❌ Ошибка:', resultData.error);
496 alert('Ошибка: ' + resultData.error);
497 break;
498 }
499 }
500 }
501
502 if (this.shouldStop) {
503 console.log('⏹️ Процесс остановлен пользователем');
504 // Сообщаем рабочей вкладке об остановке
505 await GM.setValue('ozon_current_task', JSON.stringify({
506 settings: this.settings,
507 processedCount: this.processedCount,
508 shouldContinue: false
509 }));
510 alert('Процесс остановлен. Создано заданий: ' + this.processedCount);
511 }
512 }
513 }
514
515 // ============================================
516 // ЛОГИКА ДЛЯ СТРАНИЦЫ СОЗДАНИЯ ЗАДАНИЯ
517 // ============================================
518
519 class TaskCreationHandler {
520 constructor(taskData) {
521 this.settings = taskData.settings;
522 this.processedCount = taskData.processedCount;
523 }
524
525 // Выполнить создание задания
526 async execute() {
527 console.log('📝 Начинаем создание задания на странице create...');
528
529 try {
530 // Ждём загрузки страницы
531 await delay(CONFIG.DELAYS.AFTER_PAGE_LOAD);
532
533 // Ждём появления кнопки "Добавить"
534 const addButton = await waitForElement(CONFIG.SELECTORS.CREATE_PAGE.ADD_PRODUCT_BUTTON, 15000);
535 if (!addButton) {
536 throw new Error('Не найдена кнопка "Добавить"');
537 }
538
539 // Заполняем форму
540 await this.fillForm();
541
542 // Открываем модальное окно с товарами
543 await clickElement(addButton, 'Кнопка "Добавить товар"');
544 await delay(CONFIG.DELAYS.AFTER_CLICK);
545
546 // Выбираем товар
547 const productSelected = await this.selectProduct();
548 if (!productSelected) {
549 // Все товары обработаны
550 await GM.setValue('ozon_task_result', JSON.stringify({
551 success: false,
552 noMoreProducts: true
553 }));
554 // Переходим на страницу списка вместо закрытия
555 window.location.href = CONFIG.PAGES.LIST;
556 return;
557 }
558
559 // Сохраняем задание
560 await this.saveTask();
561
562 // Сообщаем об успехе
563 await GM.setValue('ozon_task_result', JSON.stringify({
564 success: true
565 }));
566
567 console.log('✅ Задание создано, переходим на страницу списка...');
568
569 // Переходим на страницу списка вместо закрытия вкладки
570 await delay(2000); // Ждём сохранения задания
571 window.location.href = CONFIG.PAGES.LIST;
572
573 } catch (error) {
574 console.error('❌ Ошибка при создании задания:', error);
575 await GM.setValue('ozon_task_result', JSON.stringify({
576 success: false,
577 error: error.message
578 }));
579 // Переходим на страницу списка
580 window.location.href = CONFIG.PAGES.LIST;
581 }
582 }
583
584 // Заполнить форму создания задания
585 async fillForm() {
586 console.log('📝 Заполнение формы...');
587
588 // Выбираем формат публикации
589 const postTypeSelector = this.settings.postType === 'TASK_POST_TYPE_CLIP'
590 ? CONFIG.SELECTORS.CREATE_PAGE.POST_TYPE_CLIPS
591 : CONFIG.SELECTORS.CREATE_PAGE.POST_TYPE_POSTS;
592
593 const postTypeRadio = await waitForElement(postTypeSelector);
594 if (postTypeRadio && !postTypeRadio.checked) {
595 await clickElement(postTypeRadio, `Формат: ${this.settings.postType === 'TASK_POST_TYPE_CLIP' ? 'Клипы' : 'Посты'}`);
596 }
597
598 // Заполняем ставку
599 const bidInput = await waitForElement(CONFIG.SELECTORS.CREATE_PAGE.BID_INPUT);
600 if (bidInput) {
601 bidInput.value = this.settings.bid;
602 bidInput.dispatchEvent(new Event('input', { bubbles: true }));
603 bidInput.dispatchEvent(new Event('change', { bubbles: true }));
604 console.log(`💰 Ставка установлена: ${this.settings.bid}%`);
605 }
606
607 // Заполняем количество образцов
608 const samplesInput = await waitForElement(CONFIG.SELECTORS.CREATE_PAGE.SAMPLES_INPUT);
609 if (samplesInput) {
610 samplesInput.value = this.settings.samples;
611 samplesInput.dispatchEvent(new Event('input', { bubbles: true }));
612 samplesInput.dispatchEvent(new Event('change', { bubbles: true }));
613 console.log(`📦 Образцов установлено: ${this.settings.samples}`);
614 }
615
616 await delay(CONFIG.DELAYS.BETWEEN_ACTIONS);
617 }
618
619 // Выбрать товар из модального окна
620 async selectProduct() {
621 console.log('🔍 Поиск товара...');
622
623 // Ждём появления модального окна
624 const modal = await waitForElement(CONFIG.SELECTORS.MODAL.DIALOG, 10000);
625 if (!modal) {
626 console.error('❌ Модальное окно не открылось');
627 // Попробуем найти модальное окно другими способами
628 const allModals = document.querySelectorAll('[role="dialog"], .modal, [class*="modal"], [class*="Modal"]');
629 console.log(`🔍 Найдено потенциальных модальных окон: ${allModals.length}`);
630 if (allModals.length > 0) {
631 console.log('📋 Классы найденных элементов:', Array.from(allModals).map(m => m.className));
632 }
633 return false;
634 }
635
636 await delay(1000);
637
638 // Ищем строки с товарами
639 const productRows = modal.querySelectorAll(CONFIG.SELECTORS.MODAL.PRODUCT_ROWS);
640 console.log(`📦 Найдено строк товаров: ${productRows.length}`);
641
642 for (const row of productRows) {
643 // Извлекаем SKU из строки
644 const sku = this.extractSkuFromRow(row);
645 if (!sku) continue;
646
647 // Проверяем, не обработан ли уже этот товар
648 const isProcessed = await Storage.isSkuProcessed(sku);
649 if (isProcessed) {
650 console.log(`⏭️ SKU ${sku} уже обработан, пропускаем`);
651 continue;
652 }
653
654 // Находим кнопку "Добавить" в этой строке
655 const addButton = row.querySelector(CONFIG.SELECTORS.MODAL.PRODUCT_ADD_BUTTON);
656 if (addButton) {
657 await clickElement(addButton, `Добавление товара SKU: ${sku}`);
658 await Storage.addProcessedSku(sku);
659 return true;
660 }
661 }
662
663 // Если на текущей странице не нашли товар, переходим на следующую
664 // Ищем контейнер пагинации
665 const pagination = modal.querySelector(CONFIG.SELECTORS.MODAL.PAGINATION);
666 if (pagination) {
667 // Получаем все кнопки пагинации
668 const allButtons = pagination.querySelectorAll('button.t0c110-a1.table-500');
669 console.log(`🔍 Найдено кнопок пагинации: ${allButtons.length}`);
670
671 if (allButtons.length > 0) {
672 // Последняя кнопка - это стрелка "вперёд"
673 const nextPageButton = allButtons[allButtons.length - 1];
674 console.log(`🔍 Последняя кнопка: текст="${nextPageButton.textContent.trim()}", disabled=${nextPageButton.disabled}`);
675
676 if (!nextPageButton.disabled) {
677 console.log('➡️ Переход на следующую страницу товаров');
678 await clickElement(nextPageButton, 'Следующая страница');
679 await delay(CONFIG.DELAYS.AFTER_CLICK);
680 return await this.selectProduct(); // Рекурсивно ищем на следующей странице
681 } else {
682 console.log('⚠️ Кнопка следующей страницы отключена - достигнут конец списка');
683 }
684 }
685 }
686
687 console.log('❌ Не найдено необработанных товаров');
688 return false;
689 }
690
691 // Извлечь SKU из строки таблицы
692 extractSkuFromRow(row) {
693 // Ищем атрибут data-sku
694 const skuCell = row.querySelector('[data-sku]');
695 if (skuCell) {
696 return skuCell.getAttribute('data-sku');
697 }
698
699 // Альтернативный способ - поиск по тексту
700 const cells = row.querySelectorAll('td');
701 for (const cell of cells) {
702 const text = cell.textContent.trim();
703 // SKU обычно числовой
704 if (/^\d+$/.test(text) && text.length > 5) {
705 return text;
706 }
707 }
708
709 return null;
710 }
711
712 // Сохранить задание
713 async saveTask() {
714 console.log('💾 Сохранение задания...');
715
716 // Ищем кнопку "Запустить" (не "Сохранить"!)
717 // Кнопка "Запустить" находится внизу формы
718 const buttons = document.querySelectorAll('button[type="submit"]');
719 console.log(`🔍 Найдено кнопок submit: ${buttons.length}`);
720
721 // Ищем кнопку с текстом "Запустить"
722 let launchButton = null;
723 for (const button of buttons) {
724 const text = button.textContent.trim();
725 console.log(`📋 Текст кнопки: "${text}"`);
726 if (text.includes('Запустить') || text.includes('Создать')) {
727 launchButton = button;
728 break;
729 }
730 }
731
732 if (launchButton) {
733 console.log('🖱️ Клик: Запуск задания');
734 launchButton.click();
735 // НЕ закрываем вкладку сразу - ждём перехода на страницу списка
736 console.log('⏳ Ожидаем сохранения задания...');
737 } else {
738 console.error('❌ Не найдена кнопка "Запустить"');
739 throw new Error('Не найдена кнопка "Запустить"');
740 }
741 }
742 }
743
744 // ============================================
745 // ИНИЦИАЛИЗАЦИЯ
746 // ============================================
747
748 async function init() {
749 console.log('🔧 Инициализация расширения...');
750
751 // Проверяем, на какой странице мы находимся
752 const currentUrl = window.location.href;
753
754 if (currentUrl.includes('/app/advertisement/product/external-promo')) {
755 // Мы на странице списка заданий или создания
756
757 if (currentUrl.includes('/external-promo/create')) {
758 // Страница создания - проверяем, есть ли задача для выполнения
759 const taskData = await GM.getValue('ozon_current_task', null);
760 if (taskData) {
761 const task = JSON.parse(taskData);
762 if (task.shouldContinue) {
763 console.log('⚙️ Выполняем задачу создания задания...');
764 const handler = new TaskCreationHandler(task);
765 await handler.execute();
766 }
767 }
768 } else {
769 // Страница списка - добавляем кнопку
770 await initListPage();
771 }
772 }
773 }
774
775 // Инициализация на странице списка
776 async function initListPage() {
777 console.log('📄 Инициализация страницы списка заданий');
778
779 // Проверяем, есть ли активная задача (вернулись после создания задания)
780 const taskData = await GM.getValue('ozon_current_task', null);
781
782 if (taskData) {
783 const task = JSON.parse(taskData);
784 if (task.shouldContinue) {
785 console.log('🔄 Возврат после создания задания, продолжаем процесс...');
786
787 // Показываем индикатор прогресса
788 const progressIndicator = document.createElement('div');
789 progressIndicator.style.cssText = `
790 position: fixed;
791 top: 20px;
792 right: 20px;
793 background: white;
794 padding: 20px;
795 border-radius: 8px;
796 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
797 z-index: 100000;
798 min-width: 300px;
799 `;
800 progressIndicator.innerHTML = `
801 <div style="margin-bottom: 10px; font-weight: 600; font-size: 16px;">
802 ⚡ Массовое создание заданий
803 </div>
804 <div style="margin-bottom: 10px;">
805 <strong>Создано заданий:</strong> <span id="ozon-task-count">${task.processedCount}</span>
806 </div>
807 <div style="margin-bottom: 15px; color: #666; font-size: 14px;">
808 Обработка товаров...
809 </div>
810 <button id="ozon-stop-process-btn" style="
811 width: 100%;
812 padding: 10px;
813 background: #ff4444;
814 color: white;
815 border: none;
816 border-radius: 6px;
817 cursor: pointer;
818 font-weight: 600;
819 ">
820 ⏹️ Остановить процесс
821 </button>
822 `;
823 document.body.appendChild(progressIndicator);
824
825 // Обработчик кнопки остановки
826 document.getElementById('ozon-stop-process-btn').addEventListener('click', async () => {
827 console.log('⏹️ Пользователь остановил процесс');
828 await GM.setValue('ozon_current_task', null);
829 await Storage.setRunning(false);
830 alert('Процесс остановлен. Создано заданий: ' + task.processedCount);
831 window.location.reload();
832 });
833
834 // Проверяем результат последнего задания
835 const result = await GM.getValue('ozon_task_result', null);
836 if (result) {
837 const resultData = JSON.parse(result);
838
839 if (resultData.success) {
840 task.processedCount++;
841 console.log(`✅ Задание ${task.processedCount} создано успешно`);
842 document.getElementById('ozon-task-count').textContent = task.processedCount;
843 } else if (resultData.noMoreProducts) {
844 console.log('✅ Все товары обработаны');
845 await GM.setValue('ozon_current_task', null);
846 await Storage.setRunning(false);
847 progressIndicator.remove();
848 alert('Готово! Создано заданий: ' + task.processedCount);
849 return;
850 } else {
851 console.error('❌ Ошибка при создании задания:', resultData.error);
852 await GM.setValue('ozon_current_task', null);
853 await Storage.setRunning(false);
854 progressIndicator.remove();
855 alert('Ошибка: ' + resultData.error);
856 return;
857 }
858
859 // Очищаем результат
860 await GM.setValue('ozon_task_result', null);
861 }
862
863 // Продолжаем процесс - создаём следующее задание
864 await delay(CONFIG.DELAYS.BETWEEN_ACTIONS);
865
866 // Обновляем задачу с новым счётчиком
867 await GM.setValue('ozon_current_task', JSON.stringify({
868 settings: task.settings,
869 processedCount: task.processedCount,
870 shouldContinue: true
871 }));
872
873 console.log(`\n📄 Создание задания ${task.processedCount + 1}`);
874 console.log('🔗 Переходим на страницу создания задания...');
875 window.location.href = CONFIG.PAGES.CREATE;
876 return;
877 }
878 }
879
880 // Если нет активной задачи - очищаем старые данные
881 console.log('🧹 Очищаем старые данные задач');
882 await GM.setValue('ozon_current_task', null);
883
884 // Ждём появления кнопки "Создать задание" с увеличенным таймаутом
885 const createButton = await waitForElement(CONFIG.SELECTORS.LIST_PAGE.CREATE_BUTTON, 20000);
886 if (!createButton) {
887 console.error('❌ Не найдена кнопка "Создать задание"');
888 return;
889 }
890
891 // Проверяем, не добавлена ли уже наша кнопка
892 const existingButton = document.getElementById('ozon-bulk-create-btn');
893 if (existingButton) {
894 console.log('✅ Кнопка уже добавлена, пропускаем инициализацию');
895 return;
896 }
897
898 // Создаём и добавляем нашу кнопку
899 const bulkButton = UI.createBulkButton();
900 createButton.parentElement.appendChild(bulkButton);
901 console.log('✅ Кнопка "Массовое создание заданий" добавлена');
902
903 // Обработчик клика на кнопку
904 bulkButton.addEventListener('click', async () => {
905 console.log('🖱️ Клик на "Массовое создание заданий"');
906
907 // Создаём и показываем модальное окно
908 const modal = UI.createSettingsModal();
909 UI.showModal(modal);
910
911 // Загружаем сохранённые настройки
912 const savedSettings = await Storage.getSettings();
913 if (savedSettings) {
914 document.getElementById('ozon-post-type').value = savedSettings.postType;
915 document.getElementById('ozon-bid').value = savedSettings.bid;
916 document.getElementById('ozon-samples').value = savedSettings.samples;
917 }
918
919 // Обработчик кнопки "Отмена"
920 document.getElementById('ozon-cancel-btn').addEventListener('click', () => {
921 UI.hideModal(modal);
922 });
923
924 // Обработчик кнопки "Сбросить список"
925 document.getElementById('ozon-reset-btn').addEventListener('click', async () => {
926 const confirmed = confirm('Вы уверены, что хотите сбросить список обработанных товаров? После этого можно будет создать задания для всех товаров заново.');
927 if (confirmed) {
928 await GM.setValue(CONFIG.STORAGE_KEYS.PROCESSED_SKUS, '[]');
929 console.log('🔄 Список обработанных товаров сброшен');
930 alert('Список обработанных товаров успешно сброшен!');
931 }
932 });
933
934 // Обработчик кнопки "Запустить"
935 let taskCreator = null;
936 document.getElementById('ozon-start-btn').addEventListener('click', async () => {
937 // Получаем настройки из формы
938 const settings = {
939 postType: document.getElementById('ozon-post-type').value,
940 bid: document.getElementById('ozon-bid').value,
941 samples: document.getElementById('ozon-samples').value
942 };
943
944 // Валидация
945 if (!settings.bid || settings.bid < 10) {
946 alert('Пожалуйста, укажите ставку (минимум 10%)');
947 return;
948 }
949
950 if (!settings.samples || settings.samples < 0) {
951 alert('Пожалуйста, укажите количество образцов (минимум 0)');
952 return;
953 }
954
955 // Сохраняем настройки
956 await Storage.saveSettings(settings);
957
958 // Показываем кнопку остановки
959 UI.showStopButton();
960
961 // Запускаем процесс
962 taskCreator = new BulkTaskCreator(settings);
963 await taskCreator.start();
964 });
965
966 // Обработчик кнопки "Остановить"
967 document.getElementById('ozon-stop-btn').addEventListener('click', () => {
968 if (taskCreator) {
969 taskCreator.stop();
970 }
971 });
972
973 // Закрытие по клику вне модального окна
974 modal.addEventListener('click', (e) => {
975 if (e.target === modal) {
976 UI.hideModal(modal);
977 }
978 });
979 });
980 }
981
982 // Запускаем инициализацию после загрузки страницы
983 if (document.readyState === 'loading') {
984 document.addEventListener('DOMContentLoaded', init);
985 } else {
986 init();
987 }
988
989})();