Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
Size
83.0 KB
Version
1.1.26
Created
Dec 5, 2025
Updated
7 days ago
1// ==UserScript==
2// @name Ozon AI Analyzer 2.0
3// @description Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
4// @version 1.1.26
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 AI Аналитик Продаж запущен');
15
16 // Утилита для задержки
17 const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
18
19 // Парсинг числовых значений
20 function parseNumber(str) {
21 if (!str || str === '—' || str === '') return null;
22 // Извлекаем первое число ДО знака процента
23 const match = str.match(/^([\d\s,.]+)/);
24 if (!match) return null;
25 // Убираем пробелы (разделители тысяч), потом парсим
26 const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
27 const num = parseFloat(cleaned);
28 return isNaN(num) ? null : num;
29 }
30
31 // Парсинг процентов
32 function parsePercent(str) {
33 if (!str || str === '—' || str === '') return null;
34 const match = str.match(/([+-]?\d+(?:\.\d+)?)\s*%/);
35 return match ? parseFloat(match[1]) : null;
36 }
37
38 // Парсинг цены (убираем пробелы между цифрами)
39 function parsePrice(str) {
40 if (!str || str === '—' || str === '') return null;
41 // Извлекаем первое число ДО знака процента
42 const match = str.match(/^([\d\s,.]+)/);
43 if (!match) return null;
44 // Убираем пробелы, затем все кроме цифр и точек
45 const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
46 const num = parseFloat(cleaned);
47 return isNaN(num) ? null : num;
48 }
49
50 // Таблица с данными для расчета прибыли
51 const PRODUCT_COST_DATA = {
52 '72252': { cost: 158.4, commission: 0.27, delivery: 90 },
53 '71613': { cost: 108, commission: 0.27, delivery: 90 },
54 '73716': { cost: 126, commission: 0.27, delivery: 90 },
55 '80036': { cost: 103.2, commission: 0.27, delivery: 90 },
56 '73365': { cost: 166.8, commission: 0.27, delivery: 90 },
57 '74881': { cost: 135.6, commission: 0.27, delivery: 90 },
58 '73266': { cost: 708, commission: 0.27, delivery: 90 },
59 '73655': { cost: 219.6, commission: 0.27, delivery: 90 },
60 '75222': { cost: 103.2, commission: 0.27, delivery: 90 },
61 '73358': { cost: 163.2, commission: 0.27, delivery: 90 },
62 '73723': { cost: 116.4, commission: 0.27, delivery: 90 },
63 '74119': { cost: 110.4, commission: 0.27, delivery: 90 },
64 '72573': { cost: 146.4, commission: 0.27, delivery: 90 },
65 '72221': { cost: 90, commission: 0.27, delivery: 90 },
66 '75345': { cost: 111.6, commission: 0.27, delivery: 90 },
67 '73334': { cost: 104.4, commission: 0.27, delivery: 90 },
68 '72184': { cost: 177.6, commission: 0.27, delivery: 90 },
69 '75291': { cost: 100.8, commission: 0.27, delivery: 90 },
70 '73617': { cost: 163.2, commission: 0.27, delivery: 90 },
71 '73976': { cost: 170.4, commission: 0.27, delivery: 90 },
72 '75710': { cost: 172.8, commission: 0.27, delivery: 90 },
73 '76113': { cost: 115.2, commission: 0.27, delivery: 90 },
74 '75499': { cost: 96, commission: 0.27, delivery: 90 },
75 '71569': { cost: 117.6, commission: 0.27, delivery: 90 },
76 '72276': { cost: 286.8, commission: 0.27, delivery: 90 },
77 '75338': { cost: 90, commission: 0.27, delivery: 90 },
78 '75536': { cost: 87.6, commission: 0.27, delivery: 90 },
79 '76014': { cost: 135.6, commission: 0.27, delivery: 90 },
80 '73730': { cost: 121.2, commission: 0.27, delivery: 90 },
81 '75628': { cost: 124.8, commission: 0.27, delivery: 90 },
82 '74249': { cost: 312, commission: 0.27, delivery: 90 },
83 '75468': { cost: 138, commission: 0.27, delivery: 90 },
84 '73495': { cost: 120, commission: 0.27, delivery: 90 },
85 '74393': { cost: 126, commission: 0.27, delivery: 90 },
86 '74188': { cost: 174, commission: 0.27, delivery: 90 },
87 '73907': { cost: 155.376, commission: 0.27, delivery: 90 },
88 '73396': { cost: 112.8, commission: 0.27, delivery: 90 },
89 '71668': { cost: 96, commission: 0.27, delivery: 90 },
90 '73235': { cost: 114, commission: 0.27, delivery: 90 },
91 '75093': { cost: 96, commission: 0.27, delivery: 90 },
92 '73891': { cost: 100.8, commission: 0.27, delivery: 90 },
93 '75505': { cost: 91.2, commission: 0.27, delivery: 90 },
94 '71590': { cost: 100.8, commission: 0.27, delivery: 90 },
95 '73488': { cost: 150, commission: 0.27, delivery: 90 },
96 '75413': { cost: 128.4, commission: 0.27, delivery: 90 },
97 '76403': { cost: 352.8, commission: 0.27, delivery: 90 },
98 '74799': { cost: 162, commission: 0.27, delivery: 90 },
99 '75406': { cost: 117.6, commission: 0.27, delivery: 90 },
100 '75154': { cost: 123.6, commission: 0.27, delivery: 90 },
101 '75383': { cost: 82.8, commission: 0.27, delivery: 90 },
102 '80029': { cost: 67.416, commission: 0.27, delivery: 90 },
103 '76120': { cost: 264, commission: 0.27, delivery: 90 },
104 '72306': { cost: 186, commission: 0.27, delivery: 90 },
105 '75246': { cost: 88.8, commission: 0.27, delivery: 90 },
106 '73228': { cost: 133.476, commission: 0.27, delivery: 90 },
107 '73419': { cost: 165.6, commission: 0.27, delivery: 90 },
108 '74379': { cost: 175.2, commission: 0.27, delivery: 90 },
109 '83356': { cost: 229.2, commission: 0.27, delivery: 90 },
110 '75444': { cost: 123.6, commission: 0.27, delivery: 90 },
111 '79992': { cost: 127.2, commission: 0.27, delivery: 90 },
112 '73709': { cost: 218.4, commission: 0.27, delivery: 90 },
113 '73778': { cost: 144, commission: 0.27, delivery: 90 },
114 '72269': { cost: 194.4, commission: 0.27, delivery: 90 },
115 '73440': { cost: 118.8, commission: 0.27, delivery: 90 },
116 '74669': { cost: 176.4, commission: 0.27, delivery: 90 },
117 '77660': { cost: 0, commission: 0.27, delivery: 90 },
118 '77578': { cost: 0, commission: 0.27, delivery: 90 },
119 '71545': { cost: 124.8, commission: 0.27, delivery: 90 },
120 '75673': { cost: 97.2, commission: 0.27, delivery: 90 },
121 '76168': { cost: 80.4, commission: 0.27, delivery: 90 },
122 '75277': { cost: 217.2, commission: 0.27, delivery: 90 },
123 '75390': { cost: 108, commission: 0.27, delivery: 90 },
124 '74263': { cost: 160.8, commission: 0.27, delivery: 90 },
125 '74676': { cost: 176.4, commission: 0.27, delivery: 90 },
126 '75727': { cost: 121.2, commission: 0.27, delivery: 90 },
127 '74126': { cost: 111.6, commission: 0.27, delivery: 90 },
128 '74294': { cost: 145.2, commission: 0.27, delivery: 90 },
129 '76069': { cost: 220.8, commission: 0.27, delivery: 90 },
130 '71361': { cost: 204, commission: 0.27, delivery: 90 },
131 '73501': { cost: 114, commission: 0.27, delivery: 90 },
132 '72238': { cost: 102, commission: 0.27, delivery: 90 },
133 '75482': { cost: 218.4, commission: 0.27, delivery: 90 },
134 '76489': { cost: 216, commission: 0.27, delivery: 90 },
135 '76076': { cost: 103.2, commission: 0.27, delivery: 90 },
136 '75437': { cost: 69.6, commission: 0.27, delivery: 90 },
137 '75352': { cost: 72, commission: 0.27, delivery: 90 },
138 '75550': { cost: 112.8, commission: 0.27, delivery: 90 },
139 '75529': { cost: 114, commission: 0.27, delivery: 90 },
140 '76021': { cost: 243.6, commission: 0.27, delivery: 90 },
141 '73969': { cost: 91.2, commission: 0.27, delivery: 90 },
142 '73242': { cost: 133.488, commission: 0.27, delivery: 90 },
143 '80111': { cost: 58.8, commission: 0.27, delivery: 90 },
144 '73693': { cost: 159.6, commission: 0.27, delivery: 90 },
145 '75703': { cost: 208.8, commission: 0.27, delivery: 90 },
146 '74980': { cost: 158.4, commission: 0.27, delivery: 90 },
147 '76380': { cost: 145.6, commission: 0.27, delivery: 90 },
148 '77677': { cost: 129.948, commission: 0.27, delivery: 90 },
149 '75369': { cost: 103.2, commission: 0.27, delivery: 90 },
150 '74713': { cost: 180, commission: 0.27, delivery: 90 },
151 '75024': { cost: 114, commission: 0.27, delivery: 90 },
152 '77615': { cost: 0, commission: 0.27, delivery: 90 },
153 '73389': { cost: 136.8, commission: 0.27, delivery: 90 },
154 '74850': { cost: 169.2, commission: 0.27, delivery: 90 },
155 '75192': { cost: 180, commission: 0.27, delivery: 90 },
156 '73310': { cost: 140.4, commission: 0.27, delivery: 90 },
157 '73280': { cost: 81.6, commission: 0.27, delivery: 90 },
158 '75048': { cost: 122.4, commission: 0.27, delivery: 90 },
159 '74874': { cost: 140.4, commission: 0.27, delivery: 90 },
160 '71675': { cost: 105.6, commission: 0.27, delivery: 90 },
161 '74225': { cost: 153.6, commission: 0.27, delivery: 90 },
162 '74768': { cost: 117.6, commission: 0.27, delivery: 90 },
163 '73136': { cost: 163.2, commission: 0.27, delivery: 90 },
164 '74300': { cost: 134.4, commission: 0.27, delivery: 90 },
165 '76410': { cost: 328.8, commission: 0.27, delivery: 90 },
166 '74898': { cost: 139.2, commission: 0.27, delivery: 90 },
167 '73129': { cost: 159.6, commission: 0.27, delivery: 90 },
168 '75253': { cost: 117.6, commission: 0.27, delivery: 90 },
169 '75666': { cost: 92.4, commission: 0.27, delivery: 90 },
170 '73839': { cost: 112.8, commission: 0.27, delivery: 90 },
171 '75475': { cost: 115.2, commission: 0.27, delivery: 90 },
172 '76397': { cost: 0, commission: 0.27, delivery: 90 },
173 '76083': { cost: 103.2, commission: 0.27, delivery: 90 },
174 '72207': { cost: 123.6, commission: 0.27, delivery: 90 },
175 '76151': { cost: 340.8, commission: 0.27, delivery: 90 },
176 '74911': { cost: 127.2, commission: 0.27, delivery: 90 },
177 '74775': { cost: 141.6, commission: 0.27, delivery: 90 },
178 '74027': { cost: 182.4, commission: 0.27, delivery: 90 },
179 '72245': { cost: 94.8, commission: 0.27, delivery: 90 },
180 '71705': { cost: 112.8, commission: 0.27, delivery: 90 },
181 '75109': { cost: 124.8, commission: 0.27, delivery: 90 },
182 '75260': { cost: 144, commission: 0.27, delivery: 90 },
183 '74584': { cost: 141.6, commission: 0.27, delivery: 90 },
184 '74331': { cost: 128.4, commission: 0.27, delivery: 90 },
185 '75307': { cost: 224.4, commission: 0.27, delivery: 90 },
186 '72542': { cost: 104.4, commission: 0.27, delivery: 90 },
187 '75642': { cost: 144.54, commission: 0.27, delivery: 90 },
188 '75512': { cost: 88.8, commission: 0.27, delivery: 90 },
189 '70999': { cost: 164.4, commission: 0.27, delivery: 90 },
190 '76137': { cost: 103.788, commission: 0.27, delivery: 90 },
191 '74072': { cost: 148.8, commission: 0.27, delivery: 90 },
192 '73297': { cost: 85.2, commission: 0.27, delivery: 90 },
193 '76465': { cost: 301.452, commission: 0.27, delivery: 90 },
194 '71835': { cost: 73.2, commission: 0.27, delivery: 90 },
195 '74324': { cost: 129.6, commission: 0.27, delivery: 90 },
196 '71644': { cost: 132, commission: 0.27, delivery: 90 },
197 '75420': { cost: 106.8, commission: 0.27, delivery: 90 },
198 '74355': { cost: 182.4, commission: 0.27, delivery: 90 },
199 '71651': { cost: 174, commission: 0.27, delivery: 90 },
200 '74973': { cost: 144, commission: 0.27, delivery: 90 },
201 '73341': { cost: 130.8, commission: 0.27, delivery: 90 },
202 '75185': { cost: 157.2, commission: 0.27, delivery: 90 },
203 '74348': { cost: 132, commission: 0.27, delivery: 90 },
204 '75376': { cost: 69.6, commission: 0.27, delivery: 90 },
205 '74942': { cost: 159.6, commission: 0.27, delivery: 90 },
206 '77592': { cost: 0, commission: 0.27, delivery: 90 },
207 '74737': { cost: 183.6, commission: 0.27, delivery: 90 },
208 '76045': { cost: 235.2, commission: 0.27, delivery: 90 },
209 '74256': { cost: 186, commission: 0.27, delivery: 90 },
210 '75208': { cost: 200.4, commission: 0.27, delivery: 90 },
211 '76601': { cost: 313.2, commission: 0.27, delivery: 90 },
212 '75116': { cost: 346.8, commission: 0.27, delivery: 90 },
213 '73464': { cost: 258, commission: 0.27, delivery: 90 },
214 '74577': { cost: 134.4, commission: 0.27, delivery: 90 },
215 '73792': { cost: 120, commission: 0.27, delivery: 90 },
216 '74997': { cost: 159.6, commission: 0.27, delivery: 90 },
217 '75611': { cost: 150, commission: 0.27, delivery: 90 },
218 '74782': { cost: 145.2, commission: 0.27, delivery: 90 },
219 '75031': { cost: 87.6, commission: 0.27, delivery: 90 },
220 '74195': { cost: 171.6, commission: 0.27, delivery: 90 },
221 '75161': { cost: 96, commission: 0.27, delivery: 90 },
222 '74591': { cost: 132, commission: 0.27, delivery: 90 },
223 '20107': { cost: 283.752, commission: 0.27, delivery: 90 },
224 '74935': { cost: 331.2, commission: 0.27, delivery: 90 },
225 '75062': { cost: 99.6, commission: 0.27, delivery: 90 },
226 '74706': { cost: 130.8, commission: 0.27, delivery: 90 },
227 '75147': { cost: 470.4, commission: 0.27, delivery: 90 },
228 '73181': { cost: 234, commission: 0.27, delivery: 90 },
229 '74812': { cost: 366, commission: 0.27, delivery: 90 },
230 '74805': { cost: 421.2, commission: 0.27, delivery: 90 },
231 '74010': { cost: 112.8, commission: 0.27, delivery: 90 },
232 '73167': { cost: 98.4, commission: 0.27, delivery: 90 },
233 '74416': { cost: 91.2, commission: 0.27, delivery: 90 },
234 '75574': { cost: 1517, commission: 0.27, delivery: 90 },
235 '74546': { cost: 240, commission: 0.27, delivery: 90 },
236 '76199': { cost: 271.2, commission: 0.27, delivery: 90 },
237 '74829': { cost: 372, commission: 0.27, delivery: 90 },
238 '80173': { cost: 768, commission: 0.27, delivery: 90 },
239 '75567': { cost: 1497, commission: 0.27, delivery: 90 },
240 '73754': { cost: 392.4, commission: 0.27, delivery: 90 },
241 '76298': { cost: 637.2, commission: 0.27, delivery: 90 },
242 '74454': { cost: 88.8, commission: 0.27, delivery: 90 },
243 '76205': { cost: 273.6, commission: 0.27, delivery: 90 },
244 '76274': { cost: 662.4, commission: 0.27, delivery: 90 },
245 '76441': { cost: 124.8, commission: 0.27, delivery: 90 },
246 '76434': { cost: 145.2, commission: 0.27, delivery: 90 },
247 '80227': { cost: 768, commission: 0.27, delivery: 90 },
248 '76175': { cost: 285.6, commission: 0.27, delivery: 90 },
249 '76304': { cost: 324, commission: 0.27, delivery: 90 },
250 '71682': { cost: 99.6, commission: 0.27, delivery: 90 },
251 '74959': { cost: 183.6, commission: 0.27, delivery: 90 }
252 };
253
254 // Функция расчета прибыли
255 function calculateProfit(article, revenue, orders, drr) {
256 const costData = PRODUCT_COST_DATA[article];
257 if (!costData || !revenue || !orders) return null;
258
259 // Расходы на рекламу = выручка * (ДРР / 100)
260 const adCost = drr ? (revenue * (drr / 100)) : 0;
261
262 // Прибыль = Выручка - (заказы * себестоимость) - (заказы * доставка) - (выручка * комиссия) - расходы на рекламу
263 const profit = revenue - (orders * costData.cost) - (orders * costData.delivery) - (revenue * costData.commission) - adCost;
264 return Math.round(profit); // Округляем до целых
265 }
266
267 // Класс для сбора данных о товарах
268 class ProductDataCollector {
269 constructor() {
270 this.products = [];
271 this.isCollecting = false;
272 }
273
274 // Автоматическая подгрузка всех товаров
275 async loadAllProducts() {
276 console.log('📦 Начинаем загрузку всех товаров...');
277 this.isCollecting = true;
278
279 let previousCount = 0;
280 let stableCount = 0; // Счетчик стабильных попыток
281 let attempts = 0;
282 const maxAttempts = 300; // Увеличили максимум попыток до 300
283 const maxStableAttempts = 5; // Увеличили до 5 стабильных попыток
284
285 while (attempts < maxAttempts) {
286 const loadMoreBtn = document.querySelector('button.styles_loadMoreButton_2RI3D');
287
288 if (!loadMoreBtn) {
289 console.log('✅ Кнопка "Показать ещё" не найдена - все товары загружены');
290 break;
291 }
292
293 // Проверяем, не отключена ли кнопка
294 if (loadMoreBtn.disabled || loadMoreBtn.classList.contains('disabled')) {
295 console.log('✅ Кнопка "Показать ещё" отключена - все товары загружены');
296 break;
297 }
298
299 // Проверяем, есть ли товары с нулевой выручкой (значит дошли до конца)
300 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
301 let hasZeroRevenue = false;
302
303 for (const row of rows) {
304 const cells = row.querySelectorAll('td');
305 if (cells.length >= 3) {
306 const revenueText = cells[2].textContent.trim();
307 // Проверяем, есть ли "0 ₽" или "0₽" в тексте выручки
308 if (revenueText.match(/^0\s*₽/) || revenueText === '0') {
309 hasZeroRevenue = true;
310 console.log('✅ Найден товар с нулевой выручкой - останавливаем загрузку');
311 break;
312 }
313 }
314 }
315
316 if (hasZeroRevenue) {
317 console.log('✅ Достигнут конец списка активных товаров');
318 break;
319 }
320
321 // Прокручиваем к кнопке, чтобы она была видна
322 loadMoreBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
323 await delay(800);
324
325 console.log(`🔄 Клик по кнопке "Показать ещё" (попытка ${attempts + 1})`);
326 loadMoreBtn.click();
327
328 // Увеличили задержку до 4 секунд для полной загрузки данных
329 await delay(4000);
330
331 const currentCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
332 console.log(`📊 Загружено товаров: ${currentCount} (было: ${previousCount})`);
333
334 if (currentCount === previousCount) {
335 stableCount++;
336 console.log(`⏸️ Количество не изменилось (${stableCount}/${maxStableAttempts})`);
337
338 if (stableCount >= maxStableAttempts) {
339 console.log('✅ Количество товаров стабильно - загрузка завершена');
340 break;
341 }
342 } else {
343 stableCount = 0; // Сбрасываем счетчик, если количество изменилось
344 }
345
346 previousCount = currentCount;
347 attempts++;
348 }
349
350 const finalCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
351 console.log(`✅ Загрузка завершена. Всего товаров: ${finalCount}`);
352 this.isCollecting = false;
353 }
354
355 // Сбор данных из таблицы
356 collectProductData() {
357 console.log('📊 Собираем данные о товарах...');
358 this.products = [];
359
360 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
361 console.log(`Найдено строк: ${rows.length}`);
362
363 rows.forEach((row, index) => {
364 try {
365 const cells = row.querySelectorAll('td');
366 if (cells.length < 10) return;
367
368 // Извлекаем данные из ячеек
369 const productData = this.extractProductData(cells);
370 if (productData) {
371 this.products.push(productData);
372 }
373 } catch (error) {
374 console.error(`Ошибка при обработке строки ${index}:`, error);
375 }
376 });
377
378 console.log(`✅ Собрано товаров: ${this.products.length}`);
379 return this.products;
380 }
381
382 // Извлечение данных о товаре из ячеек
383 extractProductData(cells) {
384 try {
385 // Название и артикул (первая ячейка)
386 const nameCell = cells[0];
387 const nameLink = nameCell.querySelector('a.styles_productName_2qRJi');
388 const captionEl = nameCell.querySelector('.styles_productCaption_7MqtH');
389
390 const name = nameLink ? nameLink.textContent.trim() : '';
391 const articleMatch = captionEl ? captionEl.textContent.match(/Арт\.\s*(\d+)/) : null;
392 const article = articleMatch ? articleMatch[1] : '';
393
394 if (!name || !article) return null;
395
396 // Получаем текстовое содержимое всех ячеек
397 const cellTexts = Array.from(cells).map(cell => cell.textContent.trim());
398
399 // Парсим основные показатели по правильным индексам
400 // Выручка - индекс 2
401 const revenue = parseNumber(cellTexts[2]);
402 const revenueChange = parsePercent(cellTexts[2]);
403
404 // Заказано товаров - индекс 20
405 const orders = parseNumber(cellTexts[20]);
406 const ordersChange = parsePercent(cellTexts[20]);
407
408 // Показы всего - индекс 5
409 const impressions = parseNumber(cellTexts[5]);
410 const impressionsChange = parsePercent(cellTexts[5]);
411
412 // Посещения карточки товара - индекс 13
413 const cardVisits = parseNumber(cellTexts[13]);
414 const cardVisitsChange = parsePercent(cellTexts[13]);
415
416 // Конверсия из поиска и каталога в карточку (CTR) - индекс 12
417 const conversionCatalogToCard = parseNumber(cellTexts[12]);
418 const conversionCatalogToCardChange = parsePercent(cellTexts[12]);
419
420 // Конверсия из карточки в корзину (CRL) - индекс 15
421 const conversionCardToCart = parseNumber(cellTexts[15]);
422 const conversionCardToCartChange = parsePercent(cellTexts[15]);
423
424 // Добавления в корзину всего - индекс 18
425 const cartAdditions = parseNumber(cellTexts[18]);
426 const cartAdditionsChange = parsePercent(cellTexts[18]);
427
428 // CR - высчитываем: Заказано товаров / Посещения карточки товаров
429 const cr = (orders && cardVisits && cardVisits > 0) ? parseFloat(((orders / cardVisits) * 100).toFixed(1)) : null;
430 const crChange = null; // Изменение CR нужно высчитывать отдельно
431
432 // Общая ДРР - индекс 32 (парсим как процент, убираем знак %)
433 const drrText = cellTexts[32] || '';
434 const drrMatch = drrText.match(/(\d+(?:\.\d+)?)\s*%/);
435 const drr = drrMatch ? parseFloat(drrMatch[1]) : null;
436 const drrChange = parsePercent(cellTexts[32]);
437
438 // Остаток на конец периода - индекс 35
439 const stockText = cellTexts[35] || '';
440 const stockMatch = stockText.match(/(\d+)/);
441 const stock = stockMatch ? parseInt(stockMatch[1]) : null;
442
443 // Средняя цена - индекс 28 (используем parsePrice для корректного парсинга)
444 const avgPrice = parsePrice(cellTexts[28]);
445 const avgPriceChange = parsePercent(cellTexts[28]);
446
447 // Среднее время доставки - индекс 37
448 const deliveryTime = cellTexts[37] || null;
449
450 // Рассчитываем дни остатков: остаток / заказы (округляем до целых)
451 const daysOfStock = (stock && orders && orders > 0) ? Math.round(stock / orders) : null;
452
453 // Рассчитываем прибыль
454 const profit = calculateProfit(article, revenue, orders, drr);
455
456 // Рассчитываем прибыль в процентах от выручки
457 const profitPercent = (profit !== null && revenue && revenue > 0) ?
458 parseFloat(((profit / revenue) * 100).toFixed(1)) : null;
459
460 const product = {
461 name,
462 article,
463 revenue,
464 revenueChange,
465 orders,
466 ordersChange,
467 impressions,
468 impressionsChange,
469 cardVisits,
470 cardVisitsChange,
471 conversionCatalogToCard,
472 conversionCatalogToCardChange,
473 conversionCardToCart,
474 conversionCardToCartChange,
475 cartAdditions,
476 cartAdditionsChange,
477 cr,
478 crChange,
479 avgPrice,
480 avgPriceChange,
481 drr,
482 drrChange,
483 stock,
484 deliveryTime,
485 daysOfStock,
486 profit,
487 profitPercent,
488 rawData: cellTexts
489 };
490
491 return product;
492 } catch (error) {
493 console.error('Ошибка извлечения данных товара:', error);
494 return null;
495 }
496 }
497 }
498
499 // Класс для AI анализа
500 class AIAnalyzer {
501 // Батч-анализ товаров с умной фильтрацией
502 async analyzeProducts(products, onProgress) {
503 console.log('🤖 Начинаем AI анализ товаров...');
504
505 // Сначала вычисляем средние показатели
506 const avgMetrics = this.calculateAverageMetrics(products);
507 console.log('📊 Средние показатели:', avgMetrics);
508
509 // Разделяем товары на приоритетные и обычные
510 const priorityProducts = [];
511 const normalProducts = [];
512
513 products.forEach(product => {
514 const needsAIAnalysis = this.needsDetailedAnalysis(product, avgMetrics);
515 if (needsAIAnalysis) {
516 priorityProducts.push(product);
517 } else {
518 normalProducts.push(product);
519 }
520 });
521
522 console.log(`📊 Приоритетных товаров для AI анализа: ${priorityProducts.length}`);
523 console.log(`📊 Обычных товаров (базовый анализ): ${normalProducts.length}`);
524
525 const analyzedProducts = [];
526 const batchSize = 10; // Увеличили до 10 товаров одновременно
527
528 // Сначала быстро обрабатываем обычные товары (без AI)
529 normalProducts.forEach(product => {
530 analyzedProducts.push({
531 ...product,
532 analysis: this.basicAnalysis(product, avgMetrics)
533 });
534 });
535
536 // Обновляем прогресс после базового анализа
537 if (onProgress) {
538 const percentage = Math.round((normalProducts.length / products.length) * 100);
539 const remaining = Math.ceil((priorityProducts.length / batchSize) * 2);
540 onProgress(normalProducts.length, products.length, percentage, remaining);
541 }
542
543 // Анализируем приоритетные товары с AI
544 for (let i = 0; i < priorityProducts.length; i += batchSize) {
545 const batch = priorityProducts.slice(i, i + batchSize);
546 const batchPromises = batch.map(product => this.analyzeProduct(product, avgMetrics, true));
547
548 const batchResults = await Promise.all(batchPromises);
549
550 batchResults.forEach((analysis, idx) => {
551 analyzedProducts.push({
552 ...batch[idx],
553 analysis
554 });
555 });
556
557 const progress = Math.min(i + batchSize, priorityProducts.length);
558 const totalProgress = normalProducts.length + progress;
559 const percentage = Math.round((totalProgress / products.length) * 100);
560 const remaining = Math.ceil(((priorityProducts.length - progress) / batchSize) * 2);
561
562 if (onProgress) {
563 onProgress(totalProgress, products.length, percentage, remaining);
564 }
565
566 console.log(`✅ Проанализировано ${progress} из ${priorityProducts.length} приоритетных товаров`);
567 }
568
569 if (onProgress) {
570 onProgress(products.length, products.length, 100, 0);
571 }
572
573 return analyzedProducts;
574 }
575
576 // Определяем, нужен ли детальный AI анализ
577 needsDetailedAnalysis(product, avgMetrics) {
578 const threshold = 5; // Порог отклонения 5%
579
580 // Если есть значительное падение выручки
581 if (product.revenueChange !== null && product.revenueChange < avgMetrics.revenueChange - threshold) {
582 return true;
583 }
584
585 // Если есть значительное падение заказов
586 if (product.ordersChange !== null && product.ordersChange < avgMetrics.ordersChange - threshold) {
587 return true;
588 }
589
590 // Если высокий ДРР
591 if (product.drr !== null && product.drr > 20) {
592 return true;
593 }
594
595 // Если низкие остатки
596 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
597 if (daysOfStock !== null && daysOfStock < 49) {
598 return true;
599 }
600
601 // Если значительный рост (для масштабирования)
602 if (product.revenueChange !== null && product.revenueChange > avgMetrics.revenueChange + 15) {
603 return true;
604 }
605
606 return false;
607 }
608
609 // Базовый анализ без AI (для товаров без проблем)
610 basicAnalysis(product, avgMetrics) {
611 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
612 const isLowStock = daysOfStock !== null && daysOfStock <= 14;
613 const isHighDRR = product.drr !== null && product.drr > 20;
614 const isLowDRR = product.drr !== null && product.drr <= 17;
615 const isGrowth = this.detectGrowth(product, avgMetrics);
616 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
617 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
618 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
619 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
620 (product.profit / product.revenue) < 0.25;
621
622 // Проверяем время доставки (парсим число из строки типа "35 ч")
623 const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
624 const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
625
626 // Генерируем рекомендации на основе проблем
627 const recommendations = [];
628
629 if (isLowImpressions) {
630 // Проверяем ДРР - если снизился, рекомендуем увеличить бюджет
631 if (product.drrChange !== null && product.drrChange < 0) {
632 recommendations.push('Показы упали. ДРР снизился - рекомендуем увеличить бюджет рекламы');
633 }
634 // Проверяем время доставки - если больше 3 дней, это снижает видимость
635 if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
636 recommendations.push('Показы упали. Время доставки высокое - снижает видимость');
637 }
638 // Проверяем остатки - если маленькие, рекомендуем пополнить
639 if (isLowStock) {
640 recommendations.push('Показы упали. Низкие остатки - рекомендуем пополнить склад');
641 }
642 // Если нет конкретных причин
643 if (recommendations.length === 0) {
644 recommendations.push('Показы упали. Проверьте настройки рекламы и позиции товара');
645 }
646 }
647
648 if (isLowCR) {
649 // Проверяем среднюю цену - если выросла, рекомендуем снизить
650 if (product.avgPriceChange !== null && product.avgPriceChange > 0) {
651 recommendations.push('CR упал. Цена выросла - рекомендуем снизить цену для повышения конверсии');
652 }
653 // Проверяем время доставки
654 if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
655 recommendations.push('CR упал. Время доставки высокое - снижает конверсию');
656 }
657 // Проверяем остатки
658 if (isLowStock) {
659 recommendations.push('CR упал. Низкие остатки - снижает конверсию');
660 }
661 // Если нет конкретных причин
662 if (recommendations.length === 0) {
663 recommendations.push('CR упал. Проверьте карточку товара, фото и описание');
664 }
665 }
666
667 if (isLowProfit) {
668 recommendations.push('Низкая прибыль. Проверьте себестоимость, цену и рекламные расходы');
669 }
670
671 if (isLowStock && !isLowImpressions && !isLowCR) {
672 recommendations.push('Низкие остатки - рекомендуем пополнить склад');
673 }
674
675 if (isHighDRR && !isLowImpressions && !isLowCR) {
676 recommendations.push('Высокий ДРР - рекомендуем оптимизировать рекламные кампании');
677 }
678
679 // Если нет проблем - выводим "Всё хорошо"
680 if (recommendations.length === 0) {
681 recommendations.push('Всё хорошо, рекомендаций нет');
682 }
683
684 return {
685 priority: 'low',
686 problems: [],
687 recommendations,
688 daysOfStock,
689 isLowStock,
690 isHighDRR,
691 isLowDRR,
692 isGrowth,
693 isLowImpressions,
694 isLowCR,
695 isLowProfit,
696 isBadDeliveryTime
697 };
698 }
699
700 // Вычисление средних показателей
701 calculateAverageMetrics(products) {
702 const validProducts = products.filter(p => p.revenueChange !== null);
703 if (validProducts.length === 0) return { revenueChange: 0, ordersChange: 0, impressionsChange: 0 };
704
705 const sum = validProducts.reduce((acc, p) => ({
706 revenueChange: acc.revenueChange + (p.revenueChange || 0),
707 ordersChange: acc.ordersChange + (p.ordersChange || 0),
708 impressionsChange: acc.impressionsChange + (p.impressionsChange || 0)
709 }), { revenueChange: 0, ordersChange: 0, impressionsChange: 0 });
710
711 return {
712 revenueChange: sum.revenueChange / validProducts.length,
713 ordersChange: sum.ordersChange / validProducts.length,
714 impressionsChange: sum.impressionsChange / validProducts.length
715 };
716 }
717
718 async analyzeProduct(product, avgMetrics, useAI = true) {
719 try {
720 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
721 const isLowStock = daysOfStock !== null && daysOfStock < 49;
722 const isHighDRR = product.drr !== null && product.drr > 20;
723 const isLowDRR = product.drr !== null && product.drr <= 17;
724 const isGrowth = this.detectGrowth(product, avgMetrics);
725 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
726 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
727 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
728 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
729 (product.profit / product.revenue) < 0.25;
730
731 // Проверяем время доставки
732 const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
733 const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
734
735 if (!useAI) {
736 return this.basicAnalysis(product, avgMetrics);
737 }
738
739 // Формируем промпт для AI
740 const prompt = `Проанализируй товар и определи проблемы:
741
742Товар: ${product.name}
743Показатели:
744- Выручка: ${product.revenue || 'н/д'} ₽ (${product.revenueChange || 0}%)
745- Заказы: ${product.orders || 'н/д'} (${product.ordersChange || 0}%)
746- Показы: ${product.impressions || 'н/д'} (${product.impressionsChange || 0}%)
747- Посещения карточки: ${product.cardVisits || 'н/д'} (${product.cardVisitsChange || 0}%)
748- CTR: ${product.conversionCatalogToCard || 'н/д'}%
749- CRL: ${product.conversionCardToCart || 'н/д'}%
750- CR: ${product.cr || 'н/д'}%
751- ДРР: ${product.drr || 'н/д'}%
752- Остаток: ${product.stock || 'н/д'} шт (хватит на ${daysOfStock || 'н/д'} дней)
753
754Определи приоритет (critical/high/medium/low), проблемы и рекомендации.`;
755
756 const response = await RM.aiCall(prompt, {
757 type: 'json_schema',
758 json_schema: {
759 name: 'product_analysis',
760 schema: {
761 type: 'object',
762 properties: {
763 priority: {
764 type: 'string',
765 enum: ['critical', 'high', 'medium', 'low']
766 },
767 problems: {
768 type: 'array',
769 items: {
770 type: 'object',
771 properties: {
772 type: { type: 'string' },
773 description: { type: 'string' }
774 },
775 required: ['type', 'description']
776 }
777 },
778 recommendations: {
779 type: 'array',
780 items: { type: 'string' }
781 }
782 },
783 required: ['priority', 'problems', 'recommendations']
784 }
785 }
786 });
787
788 return {
789 ...response,
790 daysOfStock,
791 isLowStock,
792 isHighDRR,
793 isLowDRR,
794 isGrowth,
795 isLowImpressions,
796 isLowCR,
797 isLowProfit,
798 isBadDeliveryTime
799 };
800 } catch (error) {
801 console.error('Ошибка AI анализа:', error);
802 return this.basicAnalysis(product, avgMetrics);
803 }
804 }
805
806 // Определение роста на основе средних показателей
807 detectGrowth(product, avgMetrics) {
808 const threshold = 15; // Порог отклонения от среднего в %
809
810 // Если выручка растет значительно выше среднего
811 if (product.revenueChange !== null &&
812 product.revenueChange > avgMetrics.revenueChange + threshold) {
813 return true;
814 }
815
816 // Если заказы растут значительно выше среднего
817 if (product.ordersChange !== null &&
818 product.ordersChange > avgMetrics.ordersChange + threshold) {
819 return true;
820 }
821
822 return false;
823 }
824 }
825
826 // Класс для UI
827 class AnalyticsUI {
828 constructor() {
829 this.container = null;
830 this.filteredProducts = [];
831 this.allProducts = [];
832 this.currentFilter = 'all';
833 this.isCollapsed = false;
834 this.isDragging = false;
835 this.isResizing = false;
836 this.dragStartX = 0;
837 this.dragStartY = 0;
838 this.containerStartX = 0;
839 this.containerStartY = 0;
840 this.resizeStartWidth = 0;
841 this.resizeStartHeight = 0;
842 }
843
844 createUI() {
845 console.log('🎨 Создаем UI...');
846
847 // Создаем контейнер для нашего UI
848 this.container = document.createElement('div');
849 this.container.id = 'ozon-ai-analytics';
850 this.container.style.cssText = `
851 position: fixed;
852 top: 80px;
853 right: 20px;
854 width: 500px;
855 max-height: 85vh;
856 background: white;
857 border-radius: 12px;
858 box-shadow: 0 4px 20px rgba(0,0,0,0.15);
859 z-index: 10000;
860 overflow: hidden;
861 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
862 resize: both;
863 min-width: 400px;
864 min-height: 200px;
865 `;
866
867 // Заголовок (с возможностью перетаскивания)
868 const header = document.createElement('div');
869 header.id = 'ozon-ai-header';
870 header.style.cssText = `
871 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
872 color: white;
873 padding: 18px 24px;
874 font-weight: 600;
875 font-size: 18px;
876 display: flex;
877 justify-content: space-between;
878 align-items: center;
879 cursor: move;
880 user-select: none;
881 `;
882 header.innerHTML = `
883 <span>🤖 AI Аналитик Продаж</span>
884 <div style="display: flex; gap: 8px; align-items: center;">
885 <button id="ozon-ai-collapse" style="background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 28px; height: 28px;" title="Свернуть/Развернуть">−</button>
886 <button id="ozon-ai-close" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; width: 28px; height: 28px;" title="Закрыть">×</button>
887 </div>
888 `;
889
890 // Кнопка запуска анализа
891 const startButton = document.createElement('button');
892 startButton.id = 'ozon-ai-start';
893 startButton.textContent = '🚀 Запустить анализ';
894 startButton.style.cssText = `
895 width: calc(100% - 40px);
896 margin: 20px;
897 padding: 16px;
898 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
899 color: white;
900 border: none;
901 border-radius: 8px;
902 font-size: 16px;
903 font-weight: 600;
904 cursor: pointer;
905 transition: transform 0.2s;
906 `;
907 startButton.onmouseover = () => startButton.style.transform = 'scale(1.02)';
908 startButton.onmouseout = () => startButton.style.transform = 'scale(1)';
909
910 // Контейнер для контента
911 const content = document.createElement('div');
912 content.id = 'ozon-ai-content';
913 content.style.cssText = `
914 padding: 20px;
915 max-height: calc(85vh - 140px);
916 overflow-y: auto;
917 `;
918
919 // Индикатор изменения размера
920 const resizeHandle = document.createElement('div');
921 resizeHandle.id = 'ozon-ai-resize';
922 resizeHandle.style.cssText = `
923 position: absolute;
924 bottom: 0;
925 right: 0;
926 width: 20px;
927 height: 20px;
928 cursor: nwse-resize;
929 background: linear-gradient(135deg, transparent 0%, transparent 50%, #667eea 50%, #667eea 100%);
930 border-bottom-right-radius: 12px;
931 `;
932
933 this.container.appendChild(header);
934 this.container.appendChild(startButton);
935 this.container.appendChild(content);
936 this.container.appendChild(resizeHandle);
937
938 document.body.appendChild(this.container);
939
940 // События для перетаскивания
941 header.addEventListener('mousedown', (e) => this.startDragging(e));
942 document.addEventListener('mousemove', (e) => this.drag(e));
943 document.addEventListener('mouseup', () => this.stopDragging());
944
945 // События для изменения размера
946 resizeHandle.addEventListener('mousedown', (e) => this.startResizing(e));
947
948 // События кнопок
949 document.getElementById('ozon-ai-close').addEventListener('click', () => {
950 this.container.style.display = 'none';
951 });
952
953 document.getElementById('ozon-ai-collapse').addEventListener('click', () => {
954 this.toggleCollapse();
955 });
956
957 document.getElementById('ozon-ai-start').addEventListener('click', () => {
958 this.startAnalysis();
959 });
960
961 console.log('✅ UI создан');
962 }
963
964 startDragging(e) {
965 if (e.target.closest('button')) return; // Не перетаскиваем при клике на кнопки
966 this.isDragging = true;
967 this.dragStartX = e.clientX;
968 this.dragStartY = e.clientY;
969 const rect = this.container.getBoundingClientRect();
970 this.containerStartX = rect.left;
971 this.containerStartY = rect.top;
972 this.container.style.transition = 'none';
973 }
974
975 drag(e) {
976 if (this.isDragging) {
977 const deltaX = e.clientX - this.dragStartX;
978 const deltaY = e.clientY - this.dragStartY;
979 this.container.style.left = `${this.containerStartX + deltaX}px`;
980 this.container.style.top = `${this.containerStartY + deltaY}px`;
981 this.container.style.right = 'auto';
982 } else if (this.isResizing) {
983 const deltaX = e.clientX - this.dragStartX;
984 const deltaY = e.clientY - this.dragStartY;
985 const newWidth = Math.max(400, this.resizeStartWidth + deltaX);
986 const newHeight = Math.max(200, this.resizeStartHeight + deltaY);
987 this.container.style.width = `${newWidth}px`;
988 this.container.style.maxHeight = `${newHeight}px`;
989 }
990 }
991
992 stopDragging() {
993 this.isDragging = false;
994 this.isResizing = false;
995 this.container.style.transition = '';
996 }
997
998 startResizing(e) {
999 e.stopPropagation();
1000 this.isResizing = true;
1001 this.dragStartX = e.clientX;
1002 this.dragStartY = e.clientY;
1003 this.resizeStartWidth = this.container.offsetWidth;
1004 this.resizeStartHeight = this.container.offsetHeight;
1005 }
1006
1007 toggleCollapse() {
1008 this.isCollapsed = !this.isCollapsed;
1009 const content = document.getElementById('ozon-ai-content');
1010 const startButton = document.getElementById('ozon-ai-start');
1011 const resizeHandle = document.getElementById('ozon-ai-resize');
1012 const collapseButton = document.getElementById('ozon-ai-collapse');
1013
1014 if (this.isCollapsed) {
1015 content.style.display = 'none';
1016 startButton.style.display = 'none';
1017 resizeHandle.style.display = 'none';
1018 collapseButton.textContent = '+';
1019 this.container.style.maxHeight = 'auto';
1020 } else {
1021 content.style.display = 'block';
1022 startButton.style.display = 'block';
1023 resizeHandle.style.display = 'block';
1024 collapseButton.textContent = '−';
1025 this.container.style.maxHeight = '85vh';
1026 }
1027 }
1028
1029 async startAnalysis() {
1030 const content = document.getElementById('ozon-ai-content');
1031 const startButton = document.getElementById('ozon-ai-start');
1032
1033 startButton.disabled = true;
1034 startButton.textContent = '⏳ Загрузка товаров...';
1035
1036 try {
1037 // Шаг 1: Загрузка всех товаров
1038 const collector = new ProductDataCollector();
1039 await collector.loadAllProducts();
1040
1041 startButton.textContent = '📊 Сбор данных...';
1042
1043 // Шаг 2: Сбор данных
1044 const products = collector.collectProductData();
1045
1046 if (products.length === 0) {
1047 content.innerHTML = '<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Не удалось найти товары. Убедитесь, что вы на странице аналитики.</p>';
1048 startButton.disabled = false;
1049 startButton.textContent = '🚀 Запустить анализ';
1050 return;
1051 }
1052
1053 // Шаг 3: AI анализ с прогрессом
1054 const analyzer = new AIAnalyzer();
1055
1056 const onProgress = (current, total, percentage, remaining) => {
1057 const remainingText = remaining > 0 ? ` (~${remaining} сек)` : '';
1058 startButton.textContent = `🤖 AI анализ: ${current}/${total} (${percentage}%)${remainingText}`;
1059 };
1060
1061 const analyzedProducts = await analyzer.analyzeProducts(products, onProgress);
1062
1063 this.allProducts = analyzedProducts;
1064 this.filteredProducts = analyzedProducts;
1065
1066 // Шаг 4: Отображение результатов
1067 this.displayResults(analyzedProducts);
1068
1069 startButton.textContent = '✅ Анализ завершен';
1070 startButton.disabled = false;
1071
1072 } catch (error) {
1073 console.error('Ошибка анализа:', error);
1074 content.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Ошибка: ${error.message}</p>`;
1075 startButton.disabled = false;
1076 startButton.textContent = '🚀 Запустить анализ';
1077 }
1078 }
1079
1080 displayResults(products) {
1081 const content = document.getElementById('ozon-ai-content');
1082
1083 // Фильтры
1084 const filters = this.createFilters(products);
1085
1086 // Список товаров
1087 const productsList = this.createProductsList(products);
1088 content.innerHTML = '';
1089 content.appendChild(filters);
1090 content.appendChild(productsList);
1091 }
1092
1093 createFilters(products) {
1094 const filtersContainer = document.createElement('div');
1095 filtersContainer.style.cssText = `
1096 margin-bottom: 20px;
1097 `;
1098
1099 // Поле поиска
1100 const searchContainer = document.createElement('div');
1101 searchContainer.style.cssText = `
1102 margin-bottom: 12px;
1103 `;
1104
1105 const searchInput = document.createElement('input');
1106 searchInput.type = 'text';
1107 searchInput.placeholder = '🔍 Поиск по названию или артикулу...';
1108 searchInput.id = 'ozon-ai-search';
1109 searchInput.style.cssText = `
1110 width: 100%;
1111 padding: 10px 12px;
1112 border: 2px solid #ecf0f1;
1113 border-radius: 6px;
1114 font-size: 14px;
1115 font-family: inherit;
1116 outline: none;
1117 transition: border-color 0.2s;
1118 `;
1119
1120 searchInput.addEventListener('focus', () => {
1121 searchInput.style.borderColor = '#667eea';
1122 });
1123
1124 searchInput.addEventListener('blur', () => {
1125 searchInput.style.borderColor = '#ecf0f1';
1126 });
1127
1128 searchInput.addEventListener('input', (e) => {
1129 this.applySearch(e.target.value);
1130 });
1131
1132 searchContainer.appendChild(searchInput);
1133 filtersContainer.appendChild(searchContainer);
1134
1135 // Кнопки фильтров
1136 const buttonsContainer = document.createElement('div');
1137 buttonsContainer.style.cssText = `
1138 display: flex;
1139 flex-wrap: wrap;
1140 gap: 8px;
1141 `;
1142
1143 // Подсчет товаров по категориям
1144 const critical = products.filter(p => p.analysis.priority === 'critical').length;
1145 const high = products.filter(p => p.analysis.priority === 'high').length;
1146 const lowStock = products.filter(p => p.analysis.isLowStock).length;
1147 const highDRR = products.filter(p => p.analysis.isHighDRR).length;
1148 const lowDRR = products.filter(p => p.analysis.isLowDRR).length;
1149 const growth = products.filter(p => p.analysis.isGrowth).length;
1150 const lowImpressions = products.filter(p => p.analysis.isLowImpressions).length;
1151 const lowCR = products.filter(p => p.analysis.isLowCR).length;
1152 const lowProfit = products.filter(p => p.analysis.isLowProfit).length;
1153 const badDeliveryTime = products.filter(p => p.analysis.isBadDeliveryTime).length;
1154
1155 const filterButtons = [
1156 { id: 'all', label: `Все (${products.length})`, color: '#95a5a6' },
1157 { id: 'critical', label: `🔴 Критичные (${critical})`, color: '#e74c3c' },
1158 { id: 'high', label: `🟠 Высокий (${high})`, color: '#f39c12' },
1159 { id: 'lowStock', label: `📦 Низкие остатки (${lowStock})`, color: '#e67e22' },
1160 { id: 'highDRR', label: `💰 Высокий ДРР (${highDRR})`, color: '#c0392b' },
1161 { id: 'lowDRR', label: `📊 Повысить ДРР (${lowDRR})`, color: '#16a085' },
1162 { id: 'lowImpressions', label: `📉 Упали показы (${lowImpressions})`, color: '#9b59b6' },
1163 { id: 'lowCR', label: `📊 Упал CR (${lowCR})`, color: '#e91e63' },
1164 { id: 'lowProfit', label: `💸 Низкая прибыль (${lowProfit})`, color: '#d32f2f' },
1165 { id: 'badDeliveryTime', label: `⏱️ Плохое время (${badDeliveryTime})`, color: '#8e44ad' },
1166 { id: 'growth', label: `📈 Рост (${growth})`, color: '#27ae60' }
1167 ];
1168
1169 filterButtons.forEach(filter => {
1170 const btn = document.createElement('button');
1171 btn.textContent = filter.label;
1172 btn.style.cssText = `
1173 padding: 8px 12px;
1174 background: ${this.currentFilter === filter.id ? filter.color : '#ecf0f1'};
1175 color: ${this.currentFilter === filter.id ? 'white' : '#2c3e50'};
1176 border: none;
1177 border-radius: 6px;
1178 font-size: 13px;
1179 font-weight: 500;
1180 cursor: pointer;
1181 transition: all 0.2s;
1182 `;
1183
1184 btn.addEventListener('click', () => {
1185 this.currentFilter = filter.id;
1186 this.applyFilter(filter.id);
1187 });
1188
1189 buttonsContainer.appendChild(btn);
1190 });
1191
1192 filtersContainer.appendChild(buttonsContainer);
1193 return filtersContainer;
1194 }
1195
1196 applySearch(searchTerm) {
1197 const term = searchTerm.toLowerCase().trim();
1198
1199 console.log(`🔍 Поиск по запросу: "${term}"`);
1200
1201 if (!term) {
1202 // Если поиск пустой, применяем текущий фильтр
1203 this.applyFilter(this.currentFilter);
1204 return;
1205 }
1206
1207 // Фильтруем по поисковому запросу
1208 const filtered = this.allProducts.filter(p => {
1209 const nameMatch = p.name.toLowerCase().includes(term);
1210 const articleMatch = p.article.includes(term);
1211 return nameMatch || articleMatch;
1212 });
1213
1214 console.log(`✅ Найдено товаров: ${filtered.length}`);
1215
1216 // Сортируем по выручке
1217 filtered.sort((a, b) => {
1218 const revenueA = a.revenue || 0;
1219 const revenueB = b.revenue || 0;
1220 return revenueB - revenueA;
1221 });
1222
1223 this.filteredProducts = filtered;
1224
1225 // Обновляем только список товаров, не трогая фильтры
1226 const content = document.getElementById('ozon-ai-content');
1227 const productsList = this.createProductsList(filtered);
1228
1229 // Находим и удаляем только список товаров (второй div в content)
1230 const children = content.children;
1231 if (children.length > 1) {
1232 children[1].remove();
1233 }
1234
1235 content.appendChild(productsList);
1236 }
1237
1238 applyFilter(filterId) {
1239 let filtered = this.allProducts;
1240
1241 console.log(`🔍 Применяем фильтр: ${filterId}`);
1242
1243 // Очищаем поле поиска при смене фильтра
1244 const searchInput = document.getElementById('ozon-ai-search');
1245 if (searchInput) {
1246 searchInput.value = '';
1247 }
1248
1249 switch(filterId) {
1250 case 'critical':
1251 filtered = this.allProducts.filter(p => p.analysis.priority === 'critical');
1252 break;
1253 case 'high':
1254 filtered = this.allProducts.filter(p => p.analysis.priority === 'high');
1255 break;
1256 case 'lowStock':
1257 filtered = this.allProducts.filter(p => p.analysis.isLowStock);
1258 break;
1259 case 'highDRR':
1260 filtered = this.allProducts.filter(p => p.analysis.isHighDRR);
1261 break;
1262 case 'lowDRR':
1263 filtered = this.allProducts.filter(p => p.analysis.isLowDRR);
1264 break;
1265 case 'lowImpressions':
1266 filtered = this.allProducts.filter(p => p.analysis.isLowImpressions);
1267 console.log('📊 Товары с упавшими показами:', filtered.map(p => `${p.article} (${p.impressionsChange}%)`));
1268 break;
1269 case 'lowCR':
1270 filtered = this.allProducts.filter(p => p.analysis.isLowCR);
1271 break;
1272 case 'lowProfit':
1273 filtered = this.allProducts.filter(p => p.analysis.isLowProfit);
1274 break;
1275 case 'badDeliveryTime':
1276 filtered = this.allProducts.filter(p => p.analysis.isBadDeliveryTime);
1277 break;
1278 case 'growth':
1279 filtered = this.allProducts.filter(p => p.analysis.isGrowth);
1280 break;
1281 }
1282
1283 console.log(`✅ Найдено товаров: ${filtered.length}`);
1284
1285 // Сортируем по выручке (от большей к меньшей)
1286 filtered.sort((a, b) => {
1287 const revenueA = a.revenue || 0;
1288 const revenueB = b.revenue || 0;
1289 return revenueB - revenueA;
1290 });
1291
1292 this.filteredProducts = filtered;
1293 this.displayResults(filtered);
1294 }
1295
1296 createProductsList(products) {
1297 const list = document.createElement('div');
1298 list.style.cssText = `
1299 display: flex;
1300 flex-direction: column;
1301 gap: 12px;
1302 `;
1303
1304 products.forEach(product => {
1305 const card = this.createProductCard(product);
1306 list.appendChild(card);
1307 });
1308
1309 return list;
1310 }
1311
1312 formatMetric(value, change, isPercent = false) {
1313 // Округляем проценты до десятых
1314 let displayValue = value;
1315 if (isPercent && value !== null && value !== undefined) {
1316 displayValue = parseFloat(value.toFixed(1));
1317 }
1318
1319 const valueStr = displayValue !== null && displayValue !== undefined ?
1320 (isPercent ? `${displayValue}%` : displayValue.toLocaleString()) : '—';
1321
1322 if (change === null || change === undefined) return valueStr;
1323
1324 // Округляем изменение до десятых
1325 const roundedChange = parseFloat(change.toFixed(1));
1326 const changeStr = roundedChange > 0 ? `+${roundedChange}%` : `${roundedChange}%`;
1327 const color = roundedChange > 0 ? '#27ae60' : '#e74c3c';
1328
1329 return `${valueStr} <span style="color: ${color}; font-size: 11px;">(${changeStr})</span>`;
1330 }
1331
1332 createProductCard(product) {
1333 const card = document.createElement('div');
1334
1335 const priorityColors = {
1336 critical: '#e74c3c',
1337 high: '#f39c12',
1338 medium: '#3498db',
1339 low: '#95a5a6'
1340 };
1341
1342 const priorityLabels = {
1343 critical: '🔴 Критичный',
1344 high: '🟠 Высокий',
1345 medium: '🟡 Средний',
1346 low: '🟢 Низкий'
1347 };
1348
1349 // Определяем цвет прибыли
1350 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1351
1352 card.style.cssText = `
1353 background: white;
1354 border: 2px solid ${priorityColors[product.analysis.priority]};
1355 border-radius: 8px;
1356 padding: 14px;
1357 cursor: pointer;
1358 transition: all 0.2s;
1359 `;
1360
1361 card.innerHTML = `
1362 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
1363 <div style="flex: 1;">
1364 <div style="font-weight: 600; font-size: 14px; color: #2c3e50; margin-bottom: 4px;">${product.name}</div>
1365 <div class="article-copy" style="font-size: 12px; color: #7f8c8d; cursor: pointer; user-select: none;" title="Нажмите, чтобы скопировать артикул">Арт. ${product.article}</div>
1366 </div>
1367 <div style="background: ${priorityColors[product.analysis.priority]}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; white-space: nowrap;">
1368 ${priorityLabels[product.analysis.priority]}
1369 </div>
1370 </div>
1371
1372 ${product.analysis.problems.length > 0 ? `
1373 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 500; color: #856404;">
1374 ${product.analysis.problems.slice(0, 2).map(p => `
1375 <div style="margin-bottom: 4px;">⚠️ ${p.description}</div>
1376 `).join('')}
1377 </div>
1378 ` : ''}
1379
1380 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 12px;">
1381 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1382 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
1383 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'} ${product.profitPercent !== null ? `(${product.profitPercent}%)` : ''}</span></div>
1384 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1385 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1386 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
1387 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1388 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1389 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1390 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
1391 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
1392 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
1393 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
1394 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
1395 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
1396 </div>
1397 </div>
1398
1399 <div style="font-size: 11px; color: #7f8c8d; border-top: 1px solid #ecf0f1; padding-top: 8px;">
1400 <strong>Рекомендации:</strong>
1401 ${product.analysis.recommendations.slice(0, 1).map(r => `<div>• ${r}</div>`).join('')}
1402 </div>
1403 `;
1404
1405 // Обработчик копирования артикула
1406 const articleElement = card.querySelector('.article-copy');
1407 articleElement.addEventListener('click', async (e) => {
1408 e.stopPropagation();
1409 try {
1410 await GM.setClipboard(product.article);
1411 const originalText = articleElement.textContent;
1412 articleElement.textContent = '✓ Скопировано!';
1413 articleElement.style.color = '#27ae60';
1414 setTimeout(() => {
1415 articleElement.textContent = originalText;
1416 articleElement.style.color = '#7f8c8d';
1417 }, 1500);
1418 } catch (error) {
1419 console.error('Ошибка копирования:', error);
1420 }
1421 });
1422
1423 card.addEventListener('click', () => {
1424 this.showProductDetails(product);
1425 });
1426
1427 return card;
1428 }
1429
1430 showProductDetails(product) {
1431 // Определяем цвет прибыли
1432 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1433
1434 // Создаем модальное окно с детальной информацией
1435 const modal = document.createElement('div');
1436 modal.style.cssText = `
1437 position: fixed;
1438 top: 0;
1439 left: 0;
1440 right: 0;
1441 bottom: 0;
1442 background: rgba(0,0,0,0.7);
1443 z-index: 10001;
1444 display: flex;
1445 align-items: center;
1446 justify-content: center;
1447 padding: 20px;
1448 `;
1449
1450 const modalContent = document.createElement('div');
1451 modalContent.style.cssText = `
1452 background: white;
1453 border-radius: 12px;
1454 padding: 24px;
1455 max-width: 700px;
1456 max-height: 80vh;
1457 overflow-y: auto;
1458 width: 100%;
1459 `;
1460
1461 modalContent.innerHTML = `
1462 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
1463 <h2 style="margin: 0; font-size: 18px; color: #2c3e50;">${product.name}</h2>
1464 <button id="close-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #95a5a6;">×</button>
1465 </div>
1466
1467 <div style="background: #f8f9fa; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
1468 <div style="font-size: 12px; color: #7f8c8d; margin-bottom: 12px;">Артикул: ${product.article}</div>
1469
1470 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; font-size: 13px;">
1471 <div>
1472 <div style="color: #7f8c8d; font-size: 11px;">Выручка</div>
1473 <div style="font-weight: 600;">${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
1474 </div>
1475
1476 <div>
1477 <div style="color: #7f8c8d; font-size: 11px;">Прибыль</div>
1478 <div style="font-weight: 600; color: ${profitColor};">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</div>
1479 </div>
1480
1481 <div>
1482 <div style="color: #7f8c8d; font-size: 11px;">Прибыль %</div>
1483 <div style="font-weight: 600; color: ${profitColor};">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</div>
1484 </div>
1485
1486 <div>
1487 <div style="color: #7f8c8d; font-size: 11px;">Заказы</div>
1488 <div style="font-weight: 600;">${this.formatMetric(product.orders, product.ordersChange)}</div>
1489 </div>
1490
1491 <div>
1492 <div style="color: #7f8c8d; font-size: 11px;">Показы всего</div>
1493 <div style="font-weight: 600;">${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1494 </div>
1495
1496 <div>
1497 <div style="color: #7f8c8d; font-size: 11px;">Посещения карточки</div>
1498 <div style="font-weight: 600;">${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1499 </div>
1500
1501 <div>
1502 <div style="color: #7f8c8d; font-size: 11px;">Добавления в корзину</div>
1503 <div style="font-weight: 600;">${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1504 </div>
1505
1506 <div>
1507 <div style="color: #7f8c8d; font-size: 11px;">CTR (каталог→карточка)</div>
1508 <div style="font-weight: 600;">${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1509 </div>
1510
1511 <div>
1512 <div style="color: #7f8c8d; font-size: 11px;">CRL (карточка→корзина)</div>
1513 <div style="font-weight: 600;">${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1514 </div>
1515
1516 <div>
1517 <div style="color: #7f8c8d; font-size: 11px;">CR (заказы/карточка)</div>
1518 <div style="font-weight: 600;">${this.formatMetric(product.cr, product.crChange, true)}</div>
1519 </div>
1520
1521 <div>
1522 <div style="color: #7f8c8d; font-size: 11px;">ДРР</div>
1523 <div style="font-weight: 600;">${this.formatMetric(product.drr, product.drrChange, true)}</div>
1524 </div>
1525
1526 <div>
1527 <div style="color: #7f8c8d; font-size: 11px;">Время доставки</div>
1528 <div style="font-weight: 600;">${product.deliveryTime || '—'}</div>
1529 </div>
1530
1531 <div>
1532 <div style="color: #7f8c8d; font-size: 11px;">Остаток на конец периода</div>
1533 <div style="font-weight: 600;">${product.stock || '—'} шт</div>
1534 </div>
1535
1536 <div>
1537 <div style="color: #7f8c8d; font-size: 11px;">Хватит на дней</div>
1538 <div style="font-weight: 600;">${product.daysOfStock || '—'}</div>
1539 </div>
1540 </div>
1541 </div>
1542
1543 <div style="margin-bottom: 16px;">
1544 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">🔍 Выявленные проблемы</h3>
1545 ${product.analysis.problems.length > 0 ? product.analysis.problems.map(p => `
1546 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1547 <strong>${p.type}:</strong> ${p.description}
1548 </div>
1549 `).join('') : '<div style="color: #27ae60; font-size: 12px;">✅ Проблем не выявлено</div>'}
1550 </div>
1551
1552 <div>
1553 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">💡 Рекомендации</h3>
1554 ${product.analysis.recommendations.map(r => `
1555 <div style="background: #d4edda; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1556 ${r}
1557 </div>
1558 `).join('')}
1559 </div>
1560
1561 <button id="filter-by-article" style="
1562 width: 100%;
1563 margin-top: 16px;
1564 padding: 12px;
1565 background: #667eea;
1566 color: white;
1567 border: none;
1568 border-radius: 8px;
1569 font-size: 14px;
1570 font-weight: 600;
1571 cursor: pointer;
1572 ">
1573 🔍 Показать только этот товар в таблице
1574 </button>
1575 `;
1576
1577 modal.appendChild(modalContent);
1578 document.body.appendChild(modal);
1579
1580 // Закрытие модального окна
1581 modal.addEventListener('click', (e) => {
1582 if (e.target === modal) {
1583 modal.remove();
1584 }
1585 });
1586
1587 modalContent.querySelector('#close-modal').addEventListener('click', () => {
1588 modal.remove();
1589 });
1590
1591 // Фильтрация по артикулу
1592 modalContent.querySelector('#filter-by-article').addEventListener('click', () => {
1593 this.filterByArticle(product.article);
1594 modal.remove();
1595 });
1596 }
1597
1598 filterByArticle(article) {
1599 console.log(`🔍 Фильтруем по артикулу: ${article}`);
1600
1601 // Находим поле фильтра по артикулу на странице
1602 const articleInput = document.querySelector('input[placeholder*="артикул"], input[name*="article"]');
1603
1604 if (articleInput) {
1605 articleInput.value = article;
1606 articleInput.dispatchEvent(new Event('input', { bubbles: true }));
1607 articleInput.dispatchEvent(new Event('change', { bubbles: true }));
1608
1609 // Ищем кнопку "Применить" - ищем все кнопки и проверяем текст
1610 const buttons = document.querySelectorAll('button[type="submit"]');
1611 let applyButton = null;
1612 for (const btn of buttons) {
1613 if (btn.textContent.includes('Применить')) {
1614 applyButton = btn;
1615 break;
1616 }
1617 }
1618
1619 if (applyButton) {
1620 setTimeout(() => applyButton.click(), 300);
1621 }
1622 } else {
1623 console.warn('Не найдено поле для ввода артикула');
1624 }
1625 }
1626 }
1627
1628 // Инициализация
1629 async function init() {
1630 console.log('🎯 Инициализация AI Аналитика Продаж...');
1631
1632 // Проверяем, что мы на странице аналитики
1633 if (!window.location.href.includes('seller.ozon.ru/app/analytics')) {
1634 console.log('⚠️ Не на странице аналитики, ожидаем...');
1635 return;
1636 }
1637
1638 // Ждем загрузки таблицы
1639 const waitForTable = setInterval(() => {
1640 const table = document.querySelector('table.ct590-a');
1641 if (table) {
1642 clearInterval(waitForTable);
1643 console.log('✅ Таблица найдена, создаем UI');
1644
1645 const ui = new AnalyticsUI();
1646 ui.createUI();
1647 }
1648 }, 1000);
1649 }
1650
1651 // Запуск при загрузке страницы
1652 if (document.readyState === 'loading') {
1653 document.addEventListener('DOMContentLoaded', init);
1654 } else {
1655 init();
1656 }
1657
1658 // Отслеживание изменений URL (для SPA)
1659 let lastUrl = location.href;
1660 new MutationObserver(() => {
1661 const url = location.href;
1662 if (url !== lastUrl) {
1663 lastUrl = url;
1664 init();
1665 }
1666 }).observe(document, { subtree: true, childList: true });
1667
1668})();