Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
Size
71.3 KB
Version
1.1.18
Created
Dec 5, 2025
Updated
7 days ago
1// ==UserScript==
2// @name Ozon AI Analyzer 2.0
3// @description Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
4// @version 1.1.18
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: 192, 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: 186, 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 * 100) / 100; // Округляем до копеек
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 < 7;
613 const isHighDRR = product.drr !== null && product.drr > 20;
614 const isGrowth = this.detectGrowth(product, avgMetrics);
615 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
616 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
617 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
618 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
619 (product.profit / product.revenue) < 0.25;
620
621 // Генерируем рекомендации на основе проблем
622 const recommendations = [];
623
624 if (isLowImpressions) {
625 // Проверяем ДРР - если снизился, рекомендуем увеличить бюджет
626 if (product.drrChange !== null && product.drrChange < 0) {
627 recommendations.push('Показы упали. ДРР снизился - рекомендуем увеличить бюджет рекламы');
628 }
629 // Проверяем время доставки - если больше 3 дней, это снижает видимость
630 if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
631 recommendations.push('Показы упали. Время доставки высокое - снижает видимость');
632 }
633 // Проверяем остатки - если маленькие, рекомендуем пополнить
634 if (isLowStock) {
635 recommendations.push('Показы упали. Низкие остатки - рекомендуем пополнить склад');
636 }
637 // Если нет конкретных причин
638 if (recommendations.length === 0) {
639 recommendations.push('Показы упали. Проверьте настройки рекламы и позиции товара');
640 }
641 }
642
643 if (isLowCR) {
644 // Проверяем среднюю цену - если выросла, рекомендуем снизить
645 if (product.avgPriceChange !== null && product.avgPriceChange > 0) {
646 recommendations.push('CR упал. Цена выросла - рекомендуем снизить цену для повышения конверсии');
647 }
648 // Проверяем время доставки
649 if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
650 recommendations.push('CR упал. Время доставки высокое - снижает конверсию');
651 }
652 // Проверяем остатки
653 if (isLowStock) {
654 recommendations.push('CR упал. Низкие остатки - снижает конверсию');
655 }
656 // Если нет конкретных причин
657 if (recommendations.length === 0) {
658 recommendations.push('CR упал. Проверьте карточку товара, фото и описание');
659 }
660 }
661
662 if (isLowProfit) {
663 recommendations.push('Низкая прибыль. Проверьте себестоимость, цену и рекламные расходы');
664 }
665
666 if (isLowStock && !isLowImpressions && !isLowCR) {
667 recommendations.push('Низкие остатки - рекомендуем пополнить склад');
668 }
669
670 if (isHighDRR && !isLowImpressions && !isLowCR) {
671 recommendations.push('Высокий ДРР - рекомендуем оптимизировать рекламные кампании');
672 }
673
674 // Если нет проблем - выводим "Всё хорошо"
675 if (recommendations.length === 0) {
676 recommendations.push('Всё хорошо, рекомендаций нет');
677 }
678
679 return {
680 priority: 'low',
681 problems: [],
682 recommendations,
683 daysOfStock,
684 isLowStock,
685 isHighDRR,
686 isGrowth,
687 isLowImpressions,
688 isLowCR,
689 isLowProfit
690 };
691 }
692
693 // Вычисление средних показателей
694 calculateAverageMetrics(products) {
695 const validProducts = products.filter(p => p.revenueChange !== null);
696 if (validProducts.length === 0) return { revenueChange: 0, ordersChange: 0, impressionsChange: 0 };
697
698 const sum = validProducts.reduce((acc, p) => ({
699 revenueChange: acc.revenueChange + (p.revenueChange || 0),
700 ordersChange: acc.ordersChange + (p.ordersChange || 0),
701 impressionsChange: acc.impressionsChange + (p.impressionsChange || 0)
702 }), { revenueChange: 0, ordersChange: 0, impressionsChange: 0 });
703
704 return {
705 revenueChange: sum.revenueChange / validProducts.length,
706 ordersChange: sum.ordersChange / validProducts.length,
707 impressionsChange: sum.impressionsChange / validProducts.length
708 };
709 }
710
711 async analyzeProduct(product, avgMetrics, useAI = true) {
712 try {
713 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
714 const isLowStock = daysOfStock !== null && daysOfStock < 49;
715 const isHighDRR = product.drr !== null && product.drr > 20;
716 const isGrowth = this.detectGrowth(product, avgMetrics);
717 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
718 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
719 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
720 (product.profit / product.revenue) < 0.25;
721
722 if (!useAI) {
723 return this.basicAnalysis(product, avgMetrics);
724 }
725
726 // Формируем промпт для AI
727 const prompt = `Проанализируй товар и определи проблемы:
728
729Товар: ${product.name}
730Показатели:
731- Выручка: ${product.revenue || 'н/д'} ₽ (${product.revenueChange || 0}%)
732- Заказы: ${product.orders || 'н/д'} (${product.ordersChange || 0}%)
733- Показы: ${product.impressions || 'н/д'} (${product.impressionsChange || 0}%)
734- Посещения карточки: ${product.cardVisits || 'н/д'} (${product.cardVisitsChange || 0}%)
735- CTR: ${product.conversionCatalogToCard || 'н/д'}%
736- CRL: ${product.conversionCardToCart || 'н/д'}%
737- CR: ${product.cr || 'н/д'}%
738- ДРР: ${product.drr || 'н/д'}%
739- Остаток: ${product.stock || 'н/д'} шт (хватит на ${daysOfStock || 'н/д'} дней)
740
741Определи приоритет (critical/high/medium/low), проблемы и рекомендации.`;
742
743 const response = await RM.aiCall(prompt, {
744 type: 'json_schema',
745 json_schema: {
746 name: 'product_analysis',
747 schema: {
748 type: 'object',
749 properties: {
750 priority: {
751 type: 'string',
752 enum: ['critical', 'high', 'medium', 'low']
753 },
754 problems: {
755 type: 'array',
756 items: {
757 type: 'object',
758 properties: {
759 type: { type: 'string' },
760 description: { type: 'string' }
761 },
762 required: ['type', 'description']
763 }
764 },
765 recommendations: {
766 type: 'array',
767 items: { type: 'string' }
768 }
769 },
770 required: ['priority', 'problems', 'recommendations']
771 }
772 }
773 });
774
775 return {
776 ...response,
777 daysOfStock,
778 isLowStock,
779 isHighDRR,
780 isGrowth,
781 isLowCR,
782 isLowProfit
783 };
784 } catch (error) {
785 console.error('Ошибка AI анализа:', error);
786 return this.basicAnalysis(product, avgMetrics);
787 }
788 }
789
790 // Определение роста на основе средних показателей
791 detectGrowth(product, avgMetrics) {
792 const threshold = 15; // Порог отклонения от среднего в %
793
794 // Если выручка растет значительно выше среднего
795 if (product.revenueChange !== null &&
796 product.revenueChange > avgMetrics.revenueChange + threshold) {
797 return true;
798 }
799
800 // Если заказы растут значительно выше среднего
801 if (product.ordersChange !== null &&
802 product.ordersChange > avgMetrics.ordersChange + threshold) {
803 return true;
804 }
805
806 return false;
807 }
808 }
809
810 // Класс для UI
811 class AnalyticsUI {
812 constructor() {
813 this.container = null;
814 this.filteredProducts = [];
815 this.allProducts = [];
816 this.currentFilter = 'all';
817 }
818
819 createUI() {
820 console.log('🎨 Создаем UI...');
821
822 // Создаем контейнер для нашего UI
823 this.container = document.createElement('div');
824 this.container.id = 'ozon-ai-analytics';
825 this.container.style.cssText = `
826 position: fixed;
827 top: 80px;
828 right: 20px;
829 width: 500px;
830 max-height: 85vh;
831 background: white;
832 border-radius: 12px;
833 box-shadow: 0 4px 20px rgba(0,0,0,0.15);
834 z-index: 10000;
835 overflow: hidden;
836 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
837 `;
838
839 // Заголовок
840 const header = document.createElement('div');
841 header.style.cssText = `
842 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
843 color: white;
844 padding: 18px 24px;
845 font-weight: 600;
846 font-size: 18px;
847 display: flex;
848 justify-content: space-between;
849 align-items: center;
850 `;
851 header.innerHTML = `
852 <span>🤖 AI Аналитик Продаж</span>
853 <button id="ozon-ai-close" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; width: 28px; height: 28px;">×</button>
854 `;
855
856 // Кнопка запуска анализа
857 const startButton = document.createElement('button');
858 startButton.id = 'ozon-ai-start';
859 startButton.textContent = '🚀 Запустить анализ';
860 startButton.style.cssText = `
861 width: calc(100% - 40px);
862 margin: 20px;
863 padding: 16px;
864 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
865 color: white;
866 border: none;
867 border-radius: 8px;
868 font-size: 16px;
869 font-weight: 600;
870 cursor: pointer;
871 transition: transform 0.2s;
872 `;
873 startButton.onmouseover = () => startButton.style.transform = 'scale(1.02)';
874 startButton.onmouseout = () => startButton.style.transform = 'scale(1)';
875
876 // Контейнер для контента
877 const content = document.createElement('div');
878 content.id = 'ozon-ai-content';
879 content.style.cssText = `
880 padding: 20px;
881 max-height: calc(85vh - 140px);
882 overflow-y: auto;
883 `;
884
885 this.container.appendChild(header);
886 this.container.appendChild(startButton);
887 this.container.appendChild(content);
888
889 document.body.appendChild(this.container);
890
891 // События
892 document.getElementById('ozon-ai-close').addEventListener('click', () => {
893 this.container.style.display = 'none';
894 });
895
896 document.getElementById('ozon-ai-start').addEventListener('click', () => {
897 this.startAnalysis();
898 });
899
900 console.log('✅ UI создан');
901 }
902
903 async startAnalysis() {
904 const content = document.getElementById('ozon-ai-content');
905 const startButton = document.getElementById('ozon-ai-start');
906
907 startButton.disabled = true;
908 startButton.textContent = '⏳ Загрузка товаров...';
909
910 try {
911 // Шаг 1: Загрузка всех товаров
912 const collector = new ProductDataCollector();
913 await collector.loadAllProducts();
914
915 startButton.textContent = '📊 Сбор данных...';
916
917 // Шаг 2: Сбор данных
918 const products = collector.collectProductData();
919
920 if (products.length === 0) {
921 content.innerHTML = '<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Не удалось найти товары. Убедитесь, что вы на странице аналитики.</p>';
922 startButton.disabled = false;
923 startButton.textContent = '🚀 Запустить анализ';
924 return;
925 }
926
927 // Шаг 3: AI анализ с прогрессом
928 const analyzer = new AIAnalyzer();
929
930 const onProgress = (current, total, percentage, remaining) => {
931 const remainingText = remaining > 0 ? ` (~${remaining} сек)` : '';
932 startButton.textContent = `🤖 AI анализ: ${current}/${total} (${percentage}%)${remainingText}`;
933 };
934
935 const analyzedProducts = await analyzer.analyzeProducts(products, onProgress);
936
937 this.allProducts = analyzedProducts;
938 this.filteredProducts = analyzedProducts;
939
940 // Шаг 4: Отображение результатов
941 this.displayResults(analyzedProducts);
942
943 startButton.textContent = '✅ Анализ завершен';
944 startButton.disabled = false;
945
946 } catch (error) {
947 console.error('Ошибка анализа:', error);
948 content.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Ошибка: ${error.message}</p>`;
949 startButton.disabled = false;
950 startButton.textContent = '🚀 Запустить анализ';
951 }
952 }
953
954 displayResults(products) {
955 const content = document.getElementById('ozon-ai-content');
956
957 // Фильтры
958 const filters = this.createFilters(products);
959
960 // Список товаров
961 const productsList = this.createProductsList(products);
962
963 content.innerHTML = '';
964 content.appendChild(filters);
965 content.appendChild(productsList);
966 }
967
968 createFilters(products) {
969 const filtersContainer = document.createElement('div');
970 filtersContainer.style.cssText = `
971 margin-bottom: 20px;
972 display: flex;
973 flex-wrap: wrap;
974 gap: 8px;
975 `;
976
977 // Подсчет товаров по категориям
978 const critical = products.filter(p => p.analysis.priority === 'critical').length;
979 const high = products.filter(p => p.analysis.priority === 'high').length;
980 const lowStock = products.filter(p => p.analysis.isLowStock).length;
981 const highDRR = products.filter(p => p.analysis.isHighDRR).length;
982 const growth = products.filter(p => p.analysis.isGrowth).length;
983 const lowImpressions = products.filter(p => p.analysis.isLowImpressions).length;
984 const lowCR = products.filter(p => p.analysis.isLowCR).length;
985 const lowProfit = products.filter(p => p.analysis.isLowProfit).length;
986
987 const filterButtons = [
988 { id: 'all', label: `Все (${products.length})`, color: '#95a5a6' },
989 { id: 'critical', label: `🔴 Критичные (${critical})`, color: '#e74c3c' },
990 { id: 'high', label: `🟠 Высокий (${high})`, color: '#f39c12' },
991 { id: 'lowStock', label: `📦 Низкие остатки (${lowStock})`, color: '#e67e22' },
992 { id: 'highDRR', label: `💰 Высокий ДРР (${highDRR})`, color: '#c0392b' },
993 { id: 'lowImpressions', label: `📉 Упали показы (${lowImpressions})`, color: '#9b59b6' },
994 { id: 'lowCR', label: `📊 Упал CR (${lowCR})`, color: '#e91e63' },
995 { id: 'lowProfit', label: `💸 Низкая прибыль (${lowProfit})`, color: '#d32f2f' },
996 { id: 'growth', label: `📈 Рост (${growth})`, color: '#27ae60' }
997 ];
998
999 filterButtons.forEach(filter => {
1000 const btn = document.createElement('button');
1001 btn.textContent = filter.label;
1002 btn.style.cssText = `
1003 padding: 8px 12px;
1004 background: ${this.currentFilter === filter.id ? filter.color : '#ecf0f1'};
1005 color: ${this.currentFilter === filter.id ? 'white' : '#2c3e50'};
1006 border: none;
1007 border-radius: 6px;
1008 font-size: 13px;
1009 font-weight: 500;
1010 cursor: pointer;
1011 transition: all 0.2s;
1012 `;
1013
1014 btn.addEventListener('click', () => {
1015 this.currentFilter = filter.id;
1016 this.applyFilter(filter.id);
1017 });
1018
1019 filtersContainer.appendChild(btn);
1020 });
1021
1022 return filtersContainer;
1023 }
1024
1025 applyFilter(filterId) {
1026 let filtered = this.allProducts;
1027
1028 switch(filterId) {
1029 case 'critical':
1030 filtered = this.allProducts.filter(p => p.analysis.priority === 'critical');
1031 break;
1032 case 'high':
1033 filtered = this.allProducts.filter(p => p.analysis.priority === 'high');
1034 break;
1035 case 'lowStock':
1036 filtered = this.allProducts.filter(p => p.analysis.isLowStock);
1037 break;
1038 case 'highDRR':
1039 filtered = this.allProducts.filter(p => p.analysis.isHighDRR);
1040 break;
1041 case 'lowImpressions':
1042 filtered = this.allProducts.filter(p => p.analysis.isLowImpressions);
1043 break;
1044 case 'lowCR':
1045 filtered = this.allProducts.filter(p => p.analysis.isLowCR);
1046 break;
1047 case 'lowProfit':
1048 filtered = this.allProducts.filter(p => p.analysis.isLowProfit);
1049 break;
1050 case 'growth':
1051 filtered = this.allProducts.filter(p => p.analysis.isGrowth);
1052 break;
1053 }
1054
1055 this.filteredProducts = filtered;
1056 this.displayResults(filtered);
1057 }
1058
1059 createProductsList(products) {
1060 const list = document.createElement('div');
1061 list.style.cssText = `
1062 display: flex;
1063 flex-direction: column;
1064 gap: 12px;
1065 `;
1066
1067 products.forEach(product => {
1068 const card = this.createProductCard(product);
1069 list.appendChild(card);
1070 });
1071
1072 return list;
1073 }
1074
1075 formatMetric(value, change, isPercent = false) {
1076 // Округляем проценты до десятых
1077 let displayValue = value;
1078 if (isPercent && value !== null && value !== undefined) {
1079 displayValue = parseFloat(value.toFixed(1));
1080 }
1081
1082 const valueStr = displayValue !== null && displayValue !== undefined ?
1083 (isPercent ? `${displayValue}%` : displayValue.toLocaleString()) : '—';
1084
1085 if (change === null || change === undefined) return valueStr;
1086
1087 // Округляем изменение до десятых
1088 const roundedChange = parseFloat(change.toFixed(1));
1089 const changeStr = roundedChange > 0 ? `+${roundedChange}%` : `${roundedChange}%`;
1090 const color = roundedChange > 0 ? '#27ae60' : '#e74c3c';
1091
1092 return `${valueStr} <span style="color: ${color}; font-size: 11px;">(${changeStr})</span>`;
1093 }
1094
1095 createProductCard(product) {
1096 const card = document.createElement('div');
1097
1098 const priorityColors = {
1099 critical: '#e74c3c',
1100 high: '#f39c12',
1101 medium: '#3498db',
1102 low: '#95a5a6'
1103 };
1104
1105 const priorityLabels = {
1106 critical: '🔴 Критичный',
1107 high: '🟠 Высокий',
1108 medium: '🟡 Средний',
1109 low: '🟢 Низкий'
1110 };
1111
1112 // Определяем цвет прибыли
1113 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1114
1115 card.style.cssText = `
1116 background: white;
1117 border: 2px solid ${priorityColors[product.analysis.priority]};
1118 border-radius: 8px;
1119 padding: 14px;
1120 cursor: pointer;
1121 transition: all 0.2s;
1122 `;
1123
1124 card.innerHTML = `
1125 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
1126 <div style="flex: 1;">
1127 <div style="font-weight: 600; font-size: 14px; color: #2c3e50; margin-bottom: 4px;">${product.name}</div>
1128 <div style="font-size: 12px; color: #7f8c8d;">Арт. ${product.article}</div>
1129 </div>
1130 <div style="background: ${priorityColors[product.analysis.priority]}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; white-space: nowrap;">
1131 ${priorityLabels[product.analysis.priority]}
1132 </div>
1133 </div>
1134
1135 ${product.analysis.problems.length > 0 ? `
1136 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 500; color: #856404;">
1137 ${product.analysis.problems.slice(0, 2).map(p => `
1138 <div style="margin-bottom: 4px;">⚠️ ${p.description}</div>
1139 `).join('')}
1140 </div>
1141 ` : ''}
1142
1143 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 12px;">
1144 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1145 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
1146 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'} ${product.profitPercent !== null ? `(${product.profitPercent}%)` : ''}</span></div>
1147 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1148 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1149 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
1150 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1151 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1152 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1153 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
1154 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
1155 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
1156 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
1157 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
1158 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
1159 </div>
1160 </div>
1161
1162 <div style="font-size: 11px; color: #7f8c8d; border-top: 1px solid #ecf0f1; padding-top: 8px;">
1163 <strong>Рекомендации:</strong>
1164 ${product.analysis.recommendations.slice(0, 1).map(r => `<div>• ${r}</div>`).join('')}
1165 </div>
1166 `;
1167
1168 card.addEventListener('click', () => {
1169 this.showProductDetails(product);
1170 });
1171
1172 return card;
1173 }
1174
1175 showProductDetails(product) {
1176 // Определяем цвет прибыли
1177 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1178
1179 // Создаем модальное окно с детальной информацией
1180 const modal = document.createElement('div');
1181 modal.style.cssText = `
1182 position: fixed;
1183 top: 0;
1184 left: 0;
1185 right: 0;
1186 bottom: 0;
1187 background: rgba(0,0,0,0.7);
1188 z-index: 10001;
1189 display: flex;
1190 align-items: center;
1191 justify-content: center;
1192 padding: 20px;
1193 `;
1194
1195 const modalContent = document.createElement('div');
1196 modalContent.style.cssText = `
1197 background: white;
1198 border-radius: 12px;
1199 padding: 24px;
1200 max-width: 700px;
1201 max-height: 80vh;
1202 overflow-y: auto;
1203 width: 100%;
1204 `;
1205
1206 modalContent.innerHTML = `
1207 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
1208 <h2 style="margin: 0; font-size: 18px; color: #2c3e50;">${product.name}</h2>
1209 <button id="close-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #95a5a6;">×</button>
1210 </div>
1211
1212 <div style="background: #f8f9fa; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
1213 <div style="font-size: 12px; color: #7f8c8d; margin-bottom: 12px;">Артикул: ${product.article}</div>
1214
1215 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; font-size: 13px;">
1216 <div>
1217 <div style="color: #7f8c8d; font-size: 11px;">Выручка</div>
1218 <div style="font-weight: 600;">${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
1219 </div>
1220
1221 <div>
1222 <div style="color: #7f8c8d; font-size: 11px;">Прибыль</div>
1223 <div style="font-weight: 600; color: ${profitColor};">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</div>
1224 </div>
1225
1226 <div>
1227 <div style="color: #7f8c8d; font-size: 11px;">Прибыль %</div>
1228 <div style="font-weight: 600; color: ${profitColor};">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</div>
1229 </div>
1230
1231 <div>
1232 <div style="color: #7f8c8d; font-size: 11px;">Заказы</div>
1233 <div style="font-weight: 600;">${this.formatMetric(product.orders, product.ordersChange)}</div>
1234 </div>
1235
1236 <div>
1237 <div style="color: #7f8c8d; font-size: 11px;">Показы всего</div>
1238 <div style="font-weight: 600;">${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1239 </div>
1240
1241 <div>
1242 <div style="color: #7f8c8d; font-size: 11px;">Посещения карточки</div>
1243 <div style="font-weight: 600;">${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1244 </div>
1245
1246 <div>
1247 <div style="color: #7f8c8d; font-size: 11px;">Добавления в корзину</div>
1248 <div style="font-weight: 600;">${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1249 </div>
1250
1251 <div>
1252 <div style="color: #7f8c8d; font-size: 11px;">CTR (каталог→карточка)</div>
1253 <div style="font-weight: 600;">${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1254 </div>
1255
1256 <div>
1257 <div style="color: #7f8c8d; font-size: 11px;">CRL (карточка→корзина)</div>
1258 <div style="font-weight: 600;">${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1259 </div>
1260
1261 <div>
1262 <div style="color: #7f8c8d; font-size: 11px;">CR (заказы/карточка)</div>
1263 <div style="font-weight: 600;">${this.formatMetric(product.cr, product.crChange, true)}</div>
1264 </div>
1265
1266 <div>
1267 <div style="color: #7f8c8d; font-size: 11px;">ДРР</div>
1268 <div style="font-weight: 600;">${this.formatMetric(product.drr, product.drrChange, true)}</div>
1269 </div>
1270
1271 <div>
1272 <div style="color: #7f8c8d; font-size: 11px;">Время доставки</div>
1273 <div style="font-weight: 600;">${product.deliveryTime || '—'}</div>
1274 </div>
1275
1276 <div>
1277 <div style="color: #7f8c8d; font-size: 11px;">Остаток на конец периода</div>
1278 <div style="font-weight: 600;">${product.stock || '—'} шт</div>
1279 </div>
1280
1281 <div>
1282 <div style="color: #7f8c8d; font-size: 11px;">Хватит на дней</div>
1283 <div style="font-weight: 600;">${product.daysOfStock || '—'}</div>
1284 </div>
1285 </div>
1286 </div>
1287
1288 <div style="margin-bottom: 16px;">
1289 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">🔍 Выявленные проблемы</h3>
1290 ${product.analysis.problems.length > 0 ? product.analysis.problems.map(p => `
1291 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1292 <strong>${p.type}:</strong> ${p.description}
1293 </div>
1294 `).join('') : '<div style="color: #27ae60; font-size: 12px;">✅ Проблем не выявлено</div>'}
1295 </div>
1296
1297 <div>
1298 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">💡 Рекомендации</h3>
1299 ${product.analysis.recommendations.map(r => `
1300 <div style="background: #d4edda; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1301 ${r}
1302 </div>
1303 `).join('')}
1304 </div>
1305
1306 <button id="filter-by-article" style="
1307 width: 100%;
1308 margin-top: 16px;
1309 padding: 12px;
1310 background: #667eea;
1311 color: white;
1312 border: none;
1313 border-radius: 8px;
1314 font-size: 14px;
1315 font-weight: 600;
1316 cursor: pointer;
1317 ">
1318 🔍 Показать только этот товар в таблице
1319 </button>
1320 `;
1321
1322 modal.appendChild(modalContent);
1323 document.body.appendChild(modal);
1324
1325 // Закрытие модального окна
1326 modal.addEventListener('click', (e) => {
1327 if (e.target === modal) {
1328 modal.remove();
1329 }
1330 });
1331
1332 modalContent.querySelector('#close-modal').addEventListener('click', () => {
1333 modal.remove();
1334 });
1335
1336 // Фильтрация по артикулу
1337 modalContent.querySelector('#filter-by-article').addEventListener('click', () => {
1338 this.filterByArticle(product.article);
1339 modal.remove();
1340 });
1341 }
1342
1343 filterByArticle(article) {
1344 console.log(`🔍 Фильтруем по артикулу: ${article}`);
1345
1346 // Находим поле фильтра по артикулу на странице
1347 const articleInput = document.querySelector('input[placeholder*="артикул"], input[name*="article"]');
1348
1349 if (articleInput) {
1350 articleInput.value = article;
1351 articleInput.dispatchEvent(new Event('input', { bubbles: true }));
1352 articleInput.dispatchEvent(new Event('change', { bubbles: true }));
1353
1354 // Ищем кнопку "Применить" - ищем все кнопки и проверяем текст
1355 const buttons = document.querySelectorAll('button[type="submit"]');
1356 let applyButton = null;
1357 for (const btn of buttons) {
1358 if (btn.textContent.includes('Применить')) {
1359 applyButton = btn;
1360 break;
1361 }
1362 }
1363
1364 if (applyButton) {
1365 setTimeout(() => applyButton.click(), 300);
1366 }
1367 } else {
1368 console.warn('Не найдено поле для ввода артикула');
1369 }
1370 }
1371 }
1372
1373 // Инициализация
1374 async function init() {
1375 console.log('🎯 Инициализация AI Аналитика Продаж...');
1376
1377 // Проверяем, что мы на странице аналитики
1378 if (!window.location.href.includes('seller.ozon.ru/app/analytics')) {
1379 console.log('⚠️ Не на странице аналитики, ожидаем...');
1380 return;
1381 }
1382
1383 // Ждем загрузки таблицы
1384 const waitForTable = setInterval(() => {
1385 const table = document.querySelector('table.ct590-a');
1386 if (table) {
1387 clearInterval(waitForTable);
1388 console.log('✅ Таблица найдена, создаем UI');
1389
1390 const ui = new AnalyticsUI();
1391 ui.createUI();
1392 }
1393 }, 1000);
1394 }
1395
1396 // Запуск при загрузке страницы
1397 if (document.readyState === 'loading') {
1398 document.addEventListener('DOMContentLoaded', init);
1399 } else {
1400 init();
1401 }
1402
1403 // Отслеживание изменений URL (для SPA)
1404 let lastUrl = location.href;
1405 new MutationObserver(() => {
1406 const url = location.href;
1407 if (url !== lastUrl) {
1408 lastUrl = url;
1409 init();
1410 }
1411 }).observe(document, { subtree: true, childList: true });
1412
1413})();