Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
Size
70.8 KB
Version
1.1.14
Created
Dec 4, 2025
Updated
8 days ago
1// ==UserScript==
2// @name Ozon AI Аналитик Продаж
3// @description Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
4// @version 1.1.14
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: 129.6, 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: 136.8, 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 '74744': { cost: 194.4, commission: 0.27, delivery: 90 },
229 '80241': { cost: 163.056, commission: 0.27, delivery: 90 },
230 '75000': { cost: 171.6, commission: 0.27, delivery: 90 },
231 '74270': { cost: 112.8, commission: 0.27, delivery: 90 },
232 '76595': { cost: 274.8, commission: 0.27, delivery: 90 },
233 '77646': { cost: 240, commission: 0.27, delivery: 90 },
234 '76526': { cost: 283.2, commission: 0.27, delivery: 90 },
235 '77608': { cost: 208.8, commission: 0.27, delivery: 90 },
236 '76557': { cost: 309.6, commission: 0.27, delivery: 90 },
237 '74867': { cost: 67.2, commission: 0.27, delivery: 90 },
238 '76571': { cost: 310.8, commission: 0.27, delivery: 90 },
239 '74409': { cost: 116.4, commission: 0.27, delivery: 90 },
240 '75635': { cost: 1267, commission: 0.27, delivery: 90 },
241 '73204': { cost: 216, commission: 0.27, delivery: 90 },
242 '74515': { cost: 223.2, commission: 0.27, delivery: 90 },
243 '70494': { cost: 358.8, commission: 0.27, delivery: 90 },
244 '73150': { cost: 216, commission: 0.27, delivery: 90 },
245 '75543': { cost: 1537, commission: 0.27, delivery: 90 },
246 '73181': { cost: 234, commission: 0.27, delivery: 90 },
247 '74812': { cost: 366, commission: 0.27, delivery: 90 },
248 '74805': { cost: 421.2, commission: 0.27, delivery: 90 },
249 '74010': { cost: 112.8, commission: 0.27, delivery: 90 },
250 '73167': { cost: 98.4, commission: 0.27, delivery: 90 },
251 '74416': { cost: 91.2, commission: 0.27, delivery: 90 },
252 '75574': { cost: 1517, commission: 0.27, delivery: 90 },
253 '74546': { cost: 240, commission: 0.27, delivery: 90 },
254 '76199': { cost: 271.2, commission: 0.27, delivery: 90 },
255 '74829': { cost: 372, commission: 0.27, delivery: 90 },
256 '80173': { cost: 768, commission: 0.27, delivery: 90 },
257 '75567': { cost: 1497, commission: 0.27, delivery: 90 },
258 '73754': { cost: 392.4, commission: 0.27, delivery: 90 },
259 '76298': { cost: 637.2, commission: 0.27, delivery: 90 },
260 '74454': { cost: 88.8, commission: 0.27, delivery: 90 },
261 '76205': { cost: 273.6, commission: 0.27, delivery: 90 },
262 '76274': { cost: 662.4, commission: 0.27, delivery: 90 },
263 '76441': { cost: 124.8, commission: 0.27, delivery: 90 },
264 '76434': { cost: 145.2, commission: 0.27, delivery: 90 },
265 '80227': { cost: 768, commission: 0.27, delivery: 90 },
266 '76175': { cost: 285.6, commission: 0.27, delivery: 90 },
267 '76359': { cost: 340.8, commission: 0.27, delivery: 90 },
268 '76243': { cost: 273.6, commission: 0.27, delivery: 90 },
269 '76267': { cost: 267.6, commission: 0.27, delivery: 90 },
270 '76250': { cost: 264, commission: 0.27, delivery: 90 },
271 '74478': { cost: 90, commission: 0.27, delivery: 90 },
272 '72856': { cost: 51.6, commission: 0.27, delivery: 90 },
273 '72887': { cost: 55.2, commission: 0.27, delivery: 90 },
274 '74461': { cost: 88.8, commission: 0.27, delivery: 90 },
275 '72894': { cost: 55.2, commission: 0.27, delivery: 90 },
276 '72924': { cost: 54, commission: 0.27, delivery: 90 },
277 '72849': { cost: 52.8, commission: 0.27, delivery: 90 },
278 '80180': { cost: 768, commission: 0.27, delivery: 90 },
279 '72825': { cost: 61.2, commission: 0.27, delivery: 90 },
280 '70548': { cost: 106.8, commission: 0.27, delivery: 90 },
281 '74287': { cost: 265.2, commission: 0.27, delivery: 90 },
282 '75215': { cost: 290.4, commission: 0.27, delivery: 90 },
283 '75079': { cost: 104.4, commission: 0.27, delivery: 90 },
284 '73990': { cost: 252, commission: 0.27, delivery: 90 },
285 '73983': { cost: 273.6, commission: 0.27, delivery: 90 },
286 '82090': { cost: 344.4, commission: 0.27, delivery: 90 },
287 '76090': { cost: 192, commission: 0.27, delivery: 90 },
288 '76304': { cost: 324, commission: 0.27, delivery: 90 },
289 '71682': { cost: 99.6, commission: 0.27, delivery: 90 },
290 '74959': { cost: 183.6, commission: 0.27, delivery: 90 }
291 };
292
293 // Функция расчета прибыли
294 function calculateProfit(article, revenue, orders, drr) {
295 const costData = PRODUCT_COST_DATA[article];
296 if (!costData || !revenue || !orders) return null;
297
298 // Расходы на рекламу = выручка * (ДРР / 100)
299 const adCost = drr ? (revenue * (drr / 100)) : 0;
300
301 // Прибыль = Выручка - (заказы * себестоимость) - (заказы * доставка) - (выручка * комиссия) - расходы на рекламу
302 const profit = revenue - (orders * costData.cost) - (orders * costData.delivery) - (revenue * costData.commission) - adCost;
303 return Math.round(profit * 100) / 100; // Округляем до копеек
304 }
305
306 // Класс для сбора данных о товарах
307 class ProductDataCollector {
308 constructor() {
309 this.products = [];
310 this.isCollecting = false;
311 }
312
313 // Автоматическая подгрузка всех товаров
314 async loadAllProducts() {
315 console.log('📦 Начинаем загрузку всех товаров...');
316 this.isCollecting = true;
317
318 let previousCount = 0;
319 let attempts = 0;
320 const maxAttempts = 100; // Максимум 100 попыток
321
322 while (attempts < maxAttempts) {
323 const loadMoreBtn = document.querySelector('button.styles_loadMoreButton_2RI3D');
324
325 if (!loadMoreBtn) {
326 console.log('✅ Кнопка "Показать ещё" не найдена - все товары загружены');
327 break;
328 }
329
330 // Проверяем, не отключена ли кнопка
331 if (loadMoreBtn.disabled || loadMoreBtn.classList.contains('disabled')) {
332 console.log('✅ Кнопка "Показать ещё" отключена - все товары загружены');
333 break;
334 }
335
336 console.log(`🔄 Клик по кнопке "Показать ещё" (попытка ${attempts + 1})`);
337 loadMoreBtn.click();
338
339 await delay(2000); // Ждем загрузки
340
341 const currentCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
342 console.log(`📊 Загружено товаров: ${currentCount}`);
343
344 if (currentCount === previousCount) {
345 console.log('✅ Количество товаров не изменилось - загрузка завершена');
346 break;
347 }
348
349 previousCount = currentCount;
350 attempts++;
351 }
352
353 console.log(`✅ Загрузка завершена. Всего товаров: ${previousCount}`);
354 this.isCollecting = false;
355 }
356
357 // Сбор данных из таблицы
358 collectProductData() {
359 console.log('📊 Собираем данные о товарах...');
360 this.products = [];
361
362 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
363 console.log(`Найдено строк: ${rows.length}`);
364
365 rows.forEach((row, index) => {
366 try {
367 const cells = row.querySelectorAll('td');
368 if (cells.length < 10) return;
369
370 // Извлекаем данные из ячеек
371 const productData = this.extractProductData(cells);
372 if (productData) {
373 this.products.push(productData);
374 }
375 } catch (error) {
376 console.error(`Ошибка при обработке строки ${index}:`, error);
377 }
378 });
379
380 console.log(`✅ Собрано товаров: ${this.products.length}`);
381 return this.products;
382 }
383
384 // Извлечение данных о товаре из ячеек
385 extractProductData(cells) {
386 try {
387 // Название и артикул (первая ячейка)
388 const nameCell = cells[0];
389 const nameLink = nameCell.querySelector('a.styles_productName_2qRJi');
390 const captionEl = nameCell.querySelector('.styles_productCaption_7MqtH');
391
392 const name = nameLink ? nameLink.textContent.trim() : '';
393 const articleMatch = captionEl ? captionEl.textContent.match(/Арт\.\s*(\d+)/) : null;
394 const article = articleMatch ? articleMatch[1] : '';
395
396 if (!name || !article) return null;
397
398 // Получаем текстовое содержимое всех ячеек
399 const cellTexts = Array.from(cells).map(cell => cell.textContent.trim());
400
401 // Парсим основные показатели по правильным индексам
402 // Выручка - индекс 2
403 const revenue = parseNumber(cellTexts[2]);
404 const revenueChange = parsePercent(cellTexts[2]);
405
406 // Заказано товаров - индекс 20
407 const orders = parseNumber(cellTexts[20]);
408 const ordersChange = parsePercent(cellTexts[20]);
409
410 // Показы всего - индекс 5
411 const impressions = parseNumber(cellTexts[5]);
412 const impressionsChange = parsePercent(cellTexts[5]);
413
414 // Посещения карточки товара - индекс 13
415 const cardVisits = parseNumber(cellTexts[13]);
416 const cardVisitsChange = parsePercent(cellTexts[13]);
417
418 // Конверсия из поиска и каталога в карточку (CTR) - индекс 12
419 const conversionCatalogToCard = parseNumber(cellTexts[12]);
420 const conversionCatalogToCardChange = parsePercent(cellTexts[12]);
421
422 // Конверсия из карточки в корзину (CRL) - индекс 15
423 const conversionCardToCart = parseNumber(cellTexts[15]);
424 const conversionCardToCartChange = parsePercent(cellTexts[15]);
425
426 // Добавления в корзину всего - индекс 18
427 const cartAdditions = parseNumber(cellTexts[18]);
428 const cartAdditionsChange = parsePercent(cellTexts[18]);
429
430 // CR - высчитываем: Заказано товаров / Посещения карточки товаров
431 const cr = (orders && cardVisits && cardVisits > 0) ? parseFloat(((orders / cardVisits) * 100).toFixed(1)) : null;
432 const crChange = null; // Изменение CR нужно высчитывать отдельно
433
434 // Общая ДРР - индекс 32 (парсим как процент, убираем знак %)
435 const drrText = cellTexts[32] || '';
436 const drrMatch = drrText.match(/(\d+(?:\.\d+)?)\s*%/);
437 const drr = drrMatch ? parseFloat(drrMatch[1]) : null;
438 const drrChange = parsePercent(cellTexts[32]);
439
440 // Остаток на конец периода - индекс 35
441 const stockText = cellTexts[35] || '';
442 const stockMatch = stockText.match(/(\d+)/);
443 const stock = stockMatch ? parseInt(stockMatch[1]) : null;
444
445 // Средняя цена - индекс 28 (используем parsePrice для корректного парсинга)
446 const avgPrice = parsePrice(cellTexts[28]);
447 const avgPriceChange = parsePercent(cellTexts[28]);
448
449 // Среднее время доставки - индекс 37
450 const deliveryTime = cellTexts[37] || null;
451
452 // Рассчитываем дни остатков: остаток / заказы (округляем до целых)
453 const daysOfStock = (stock && orders && orders > 0) ? Math.round(stock / orders) : null;
454
455 // Рассчитываем прибыль
456 const profit = calculateProfit(article, revenue, orders, drr);
457
458 // Рассчитываем прибыль в процентах от выручки
459 const profitPercent = (profit !== null && revenue && revenue > 0) ?
460 parseFloat(((profit / revenue) * 100).toFixed(1)) : null;
461
462 const product = {
463 name,
464 article,
465 revenue,
466 revenueChange,
467 orders,
468 ordersChange,
469 impressions,
470 impressionsChange,
471 cardVisits,
472 cardVisitsChange,
473 conversionCatalogToCard,
474 conversionCatalogToCardChange,
475 conversionCardToCart,
476 conversionCardToCartChange,
477 cartAdditions,
478 cartAdditionsChange,
479 cr,
480 crChange,
481 avgPrice,
482 avgPriceChange,
483 drr,
484 drrChange,
485 stock,
486 deliveryTime,
487 daysOfStock,
488 profit,
489 profitPercent,
490 rawData: cellTexts
491 };
492
493 return product;
494 } catch (error) {
495 console.error('Ошибка извлечения данных товара:', error);
496 return null;
497 }
498 }
499 }
500
501 // Класс для AI анализа
502 class AIAnalyzer {
503 // Батч-анализ товаров с умной фильтрацией
504 async analyzeProducts(products, onProgress) {
505 console.log('🤖 Начинаем AI анализ товаров...');
506
507 // Сначала вычисляем средние показатели
508 const avgMetrics = this.calculateAverageMetrics(products);
509 console.log('📊 Средние показатели:', avgMetrics);
510
511 // Разделяем товары на приоритетные и обычные
512 const priorityProducts = [];
513 const normalProducts = [];
514
515 products.forEach(product => {
516 const needsAIAnalysis = this.needsDetailedAnalysis(product, avgMetrics);
517 if (needsAIAnalysis) {
518 priorityProducts.push(product);
519 } else {
520 normalProducts.push(product);
521 }
522 });
523
524 console.log(`📊 Приоритетных товаров для AI анализа: ${priorityProducts.length}`);
525 console.log(`📊 Обычных товаров (базовый анализ): ${normalProducts.length}`);
526
527 const analyzedProducts = [];
528 const batchSize = 10; // Увеличили до 10 товаров одновременно
529
530 // Анализируем приоритетные товары с AI
531 for (let i = 0; i < priorityProducts.length; i += batchSize) {
532 const batch = priorityProducts.slice(i, i + batchSize);
533 const batchPromises = batch.map(product => this.analyzeProduct(product, avgMetrics, true));
534
535 const batchResults = await Promise.all(batchPromises);
536
537 batchResults.forEach((analysis, idx) => {
538 analyzedProducts.push({
539 ...batch[idx],
540 analysis
541 });
542 });
543
544 const progress = Math.min(i + batchSize, priorityProducts.length);
545 const totalProgress = progress + normalProducts.length;
546 const percentage = Math.round((totalProgress / products.length) * 100);
547 const remaining = Math.ceil(((products.length - totalProgress) / batchSize) * 2); // ~2 сек на батч
548
549 if (onProgress) {
550 onProgress(totalProgress, products.length, percentage, remaining);
551 }
552
553 console.log(`✅ Проанализировано ${progress} из ${priorityProducts.length} приоритетных товаров`);
554 }
555
556 // Быстрый базовый анализ для обычных товаров
557 normalProducts.forEach(product => {
558 analyzedProducts.push({
559 ...product,
560 analysis: this.basicAnalysis(product, avgMetrics)
561 });
562 });
563
564 if (onProgress) {
565 onProgress(products.length, products.length, 100, 0);
566 }
567
568 return analyzedProducts;
569 }
570
571 // Определяем, нужен ли детальный AI анализ
572 needsDetailedAnalysis(product, avgMetrics) {
573 const threshold = 5; // Порог отклонения 5%
574
575 // Если есть значительное падение выручки
576 if (product.revenueChange !== null && product.revenueChange < avgMetrics.revenueChange - threshold) {
577 return true;
578 }
579
580 // Если есть значительное падение заказов
581 if (product.ordersChange !== null && product.ordersChange < avgMetrics.ordersChange - threshold) {
582 return true;
583 }
584
585 // Если высокий ДРР
586 if (product.drr !== null && product.drr > 20) {
587 return true;
588 }
589
590 // Если низкие остатки
591 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
592 if (daysOfStock !== null && daysOfStock < 49) {
593 return true;
594 }
595
596 // Если значительный рост (для масштабирования)
597 if (product.revenueChange !== null && product.revenueChange > avgMetrics.revenueChange + 15) {
598 return true;
599 }
600
601 return false;
602 }
603
604 // Базовый анализ без AI (для товаров без проблем)
605 basicAnalysis(product, avgMetrics) {
606 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
607 const isLowStock = daysOfStock !== null && daysOfStock < 7;
608 const isHighDRR = product.drr !== null && product.drr > 20;
609 const isGrowth = this.detectGrowth(product, avgMetrics);
610 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
611 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
612 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
613 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
614 (product.profit / product.revenue) < 0.25;
615
616 // Генерируем рекомендации на основе проблем
617 const recommendations = [];
618
619 if (isLowImpressions) {
620 // Проверяем ДРР - если снизился, рекомендуем увеличить бюджет
621 if (product.drrChange !== null && product.drrChange < 0) {
622 recommendations.push('Показы упали. ДРР снизился - рекомендуем увеличить бюджет рекламы');
623 }
624 // Проверяем время доставки - если больше 3 дней, это снижает видимость
625 if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
626 recommendations.push('Показы упали. Время доставки высокое - снижает видимость');
627 }
628 // Проверяем остатки - если маленькие, рекомендуем пополнить
629 if (isLowStock) {
630 recommendations.push('Показы упали. Низкие остатки - рекомендуем пополнить склад');
631 }
632 // Если нет конкретных причин
633 if (recommendations.length === 0) {
634 recommendations.push('Показы упали. Проверьте настройки рекламы и позиции товара');
635 }
636 }
637
638 if (isLowCR) {
639 // Проверяем среднюю цену - если выросла, рекомендуем снизить
640 if (product.avgPriceChange !== null && product.avgPriceChange > 0) {
641 recommendations.push('CR упал. Цена выросла - рекомендуем снизить цену для повышения конверсии');
642 }
643 // Проверяем время доставки
644 if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
645 recommendations.push('CR упал. Время доставки высокое - снижает конверсию');
646 }
647 // Проверяем остатки
648 if (isLowStock) {
649 recommendations.push('CR упал. Низкие остатки - снижает конверсию');
650 }
651 // Если нет конкретных причин
652 if (recommendations.length === 0) {
653 recommendations.push('CR упал. Проверьте карточку товара, фото и описание');
654 }
655 }
656
657 if (isLowProfit) {
658 recommendations.push('Низкая прибыль. Проверьте себестоимость, цену и рекламные расходы');
659 }
660
661 if (isLowStock && !isLowImpressions && !isLowCR) {
662 recommendations.push('Низкие остатки - рекомендуем пополнить склад');
663 }
664
665 if (isHighDRR && !isLowImpressions && !isLowCR) {
666 recommendations.push('Высокий ДРР - рекомендуем оптимизировать рекламные кампании');
667 }
668
669 if (recommendations.length === 0) {
670 recommendations.push('Всё хорошо, рекомендаций нет');
671 }
672
673 return {
674 priority: 'low',
675 problems: [],
676 recommendations,
677 daysOfStock,
678 isLowStock,
679 isHighDRR,
680 isGrowth,
681 isLowImpressions,
682 isLowCR,
683 isLowProfit
684 };
685 }
686
687 // Вычисление средних показателей
688 calculateAverageMetrics(products) {
689 const validProducts = products.filter(p => p.revenueChange !== null);
690 if (validProducts.length === 0) return { revenueChange: 0, ordersChange: 0, impressionsChange: 0 };
691
692 const sum = validProducts.reduce((acc, p) => ({
693 revenueChange: acc.revenueChange + (p.revenueChange || 0),
694 ordersChange: acc.ordersChange + (p.ordersChange || 0),
695 impressionsChange: acc.impressionsChange + (p.impressionsChange || 0)
696 }), { revenueChange: 0, ordersChange: 0, impressionsChange: 0 });
697
698 return {
699 revenueChange: sum.revenueChange / validProducts.length,
700 ordersChange: sum.ordersChange / validProducts.length,
701 impressionsChange: sum.impressionsChange / validProducts.length
702 };
703 }
704
705 async analyzeProduct(product, avgMetrics, useAI = true) {
706 try {
707 const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
708 const isLowStock = daysOfStock !== null && daysOfStock < 49;
709 const isHighDRR = product.drr !== null && product.drr > 20;
710 const isGrowth = this.detectGrowth(product, avgMetrics);
711 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
712 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
713 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
714 (product.profit / product.revenue) < 0.25;
715
716 if (!useAI) {
717 return this.basicAnalysis(product, avgMetrics);
718 }
719
720 // Формируем промпт для AI
721 const prompt = `Проанализируй товар и определи проблемы:
722
723Товар: ${product.name}
724Показатели:
725- Выручка: ${product.revenue || 'н/д'} ₽ (${product.revenueChange || 0}%)
726- Заказы: ${product.orders || 'н/д'} (${product.ordersChange || 0}%)
727- Показы: ${product.impressions || 'н/д'} (${product.impressionsChange || 0}%)
728- Посещения карточки: ${product.cardVisits || 'н/д'} (${product.cardVisitsChange || 0}%)
729- CTR: ${product.conversionCatalogToCard || 'н/д'}%
730- CRL: ${product.conversionCardToCart || 'н/д'}%
731- CR: ${product.cr || 'н/д'}%
732- ДРР: ${product.drr || 'н/д'}%
733- Остаток: ${product.stock || 'н/д'} шт (хватит на ${daysOfStock || 'н/д'} дней)
734
735Определи приоритет (critical/high/medium/low), проблемы и рекомендации.`;
736
737 const response = await RM.aiCall(prompt, {
738 type: 'json_schema',
739 json_schema: {
740 name: 'product_analysis',
741 schema: {
742 type: 'object',
743 properties: {
744 priority: {
745 type: 'string',
746 enum: ['critical', 'high', 'medium', 'low']
747 },
748 problems: {
749 type: 'array',
750 items: {
751 type: 'object',
752 properties: {
753 type: { type: 'string' },
754 description: { type: 'string' }
755 },
756 required: ['type', 'description']
757 }
758 },
759 recommendations: {
760 type: 'array',
761 items: { type: 'string' }
762 }
763 },
764 required: ['priority', 'problems', 'recommendations']
765 }
766 }
767 });
768
769 return {
770 ...response,
771 daysOfStock,
772 isLowStock,
773 isHighDRR,
774 isGrowth,
775 isLowCR,
776 isLowProfit
777 };
778 } catch (error) {
779 console.error('Ошибка AI анализа:', error);
780 return this.basicAnalysis(product, avgMetrics);
781 }
782 }
783
784 // Определение роста на основе средних показателей
785 detectGrowth(product, avgMetrics) {
786 const threshold = 15; // Порог отклонения от среднего в %
787
788 // Если выручка растет значительно выше среднего
789 if (product.revenueChange !== null &&
790 product.revenueChange > avgMetrics.revenueChange + threshold) {
791 return true;
792 }
793
794 // Если заказы растут значительно выше среднего
795 if (product.ordersChange !== null &&
796 product.ordersChange > avgMetrics.ordersChange + threshold) {
797 return true;
798 }
799
800 return false;
801 }
802 }
803
804 // Класс для UI
805 class AnalyticsUI {
806 constructor() {
807 this.container = null;
808 this.filteredProducts = [];
809 this.allProducts = [];
810 this.currentFilter = 'all';
811 }
812
813 createUI() {
814 console.log('🎨 Создаем UI...');
815
816 // Создаем контейнер для нашего UI
817 this.container = document.createElement('div');
818 this.container.id = 'ozon-ai-analytics';
819 this.container.style.cssText = `
820 position: fixed;
821 top: 80px;
822 right: 20px;
823 width: 500px;
824 max-height: 85vh;
825 background: white;
826 border-radius: 12px;
827 box-shadow: 0 4px 20px rgba(0,0,0,0.15);
828 z-index: 10000;
829 overflow: hidden;
830 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
831 `;
832
833 // Заголовок
834 const header = document.createElement('div');
835 header.style.cssText = `
836 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
837 color: white;
838 padding: 18px 24px;
839 font-weight: 600;
840 font-size: 18px;
841 display: flex;
842 justify-content: space-between;
843 align-items: center;
844 `;
845 header.innerHTML = `
846 <span>🤖 AI Аналитик Продаж</span>
847 <button id="ozon-ai-close" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; width: 28px; height: 28px;">×</button>
848 `;
849
850 // Кнопка запуска анализа
851 const startButton = document.createElement('button');
852 startButton.id = 'ozon-ai-start';
853 startButton.textContent = '🚀 Запустить анализ';
854 startButton.style.cssText = `
855 width: calc(100% - 40px);
856 margin: 20px;
857 padding: 16px;
858 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
859 color: white;
860 border: none;
861 border-radius: 8px;
862 font-size: 16px;
863 font-weight: 600;
864 cursor: pointer;
865 transition: transform 0.2s;
866 `;
867 startButton.onmouseover = () => startButton.style.transform = 'scale(1.02)';
868 startButton.onmouseout = () => startButton.style.transform = 'scale(1)';
869
870 // Контейнер для контента
871 const content = document.createElement('div');
872 content.id = 'ozon-ai-content';
873 content.style.cssText = `
874 padding: 20px;
875 max-height: calc(85vh - 140px);
876 overflow-y: auto;
877 `;
878
879 this.container.appendChild(header);
880 this.container.appendChild(startButton);
881 this.container.appendChild(content);
882
883 document.body.appendChild(this.container);
884
885 // События
886 document.getElementById('ozon-ai-close').addEventListener('click', () => {
887 this.container.style.display = 'none';
888 });
889
890 document.getElementById('ozon-ai-start').addEventListener('click', () => {
891 this.startAnalysis();
892 });
893
894 console.log('✅ UI создан');
895 }
896
897 async startAnalysis() {
898 const content = document.getElementById('ozon-ai-content');
899 const startButton = document.getElementById('ozon-ai-start');
900
901 startButton.disabled = true;
902 startButton.textContent = '⏳ Загрузка товаров...';
903
904 try {
905 // Шаг 1: Загрузка всех товаров
906 const collector = new ProductDataCollector();
907 await collector.loadAllProducts();
908
909 startButton.textContent = '📊 Сбор данных...';
910
911 // Шаг 2: Сбор данных
912 const products = collector.collectProductData();
913
914 if (products.length === 0) {
915 content.innerHTML = '<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Не удалось найти товары. Убедитесь, что вы на странице аналитики.</p>';
916 startButton.disabled = false;
917 startButton.textContent = '🚀 Запустить анализ';
918 return;
919 }
920
921 // Шаг 3: AI анализ с прогрессом
922 const analyzer = new AIAnalyzer();
923
924 const onProgress = (current, total, percentage, remaining) => {
925 const remainingText = remaining > 0 ? ` (~${remaining} сек)` : '';
926 startButton.textContent = `🤖 AI анализ: ${current}/${total} (${percentage}%)${remainingText}`;
927 };
928
929 const analyzedProducts = await analyzer.analyzeProducts(products, onProgress);
930
931 this.allProducts = analyzedProducts;
932 this.filteredProducts = analyzedProducts;
933
934 // Шаг 4: Отображение результатов
935 this.displayResults(analyzedProducts);
936
937 startButton.textContent = '✅ Анализ завершен';
938 startButton.disabled = false;
939
940 } catch (error) {
941 console.error('Ошибка анализа:', error);
942 content.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Ошибка: ${error.message}</p>`;
943 startButton.disabled = false;
944 startButton.textContent = '🚀 Запустить анализ';
945 }
946 }
947
948 displayResults(products) {
949 const content = document.getElementById('ozon-ai-content');
950
951 // Фильтры
952 const filters = this.createFilters(products);
953
954 // Список товаров
955 const productsList = this.createProductsList(products);
956
957 content.innerHTML = '';
958 content.appendChild(filters);
959 content.appendChild(productsList);
960 }
961
962 createFilters(products) {
963 const filtersContainer = document.createElement('div');
964 filtersContainer.style.cssText = `
965 margin-bottom: 20px;
966 display: flex;
967 flex-wrap: wrap;
968 gap: 8px;
969 `;
970
971 // Подсчет товаров по категориям
972 const critical = products.filter(p => p.analysis.priority === 'critical').length;
973 const high = products.filter(p => p.analysis.priority === 'high').length;
974 const lowStock = products.filter(p => p.analysis.isLowStock).length;
975 const highDRR = products.filter(p => p.analysis.isHighDRR).length;
976 const growth = products.filter(p => p.analysis.isGrowth).length;
977 const lowImpressions = products.filter(p => p.analysis.isLowImpressions).length;
978 const lowCR = products.filter(p => p.analysis.isLowCR).length;
979 const lowProfit = products.filter(p => p.analysis.isLowProfit).length;
980
981 const filterButtons = [
982 { id: 'all', label: `Все (${products.length})`, color: '#95a5a6' },
983 { id: 'critical', label: `🔴 Критичные (${critical})`, color: '#e74c3c' },
984 { id: 'high', label: `🟠 Высокий (${high})`, color: '#f39c12' },
985 { id: 'lowStock', label: `📦 Низкие остатки (${lowStock})`, color: '#e67e22' },
986 { id: 'highDRR', label: `💰 Высокий ДРР (${highDRR})`, color: '#c0392b' },
987 { id: 'lowImpressions', label: `📉 Упали показы (${lowImpressions})`, color: '#9b59b6' },
988 { id: 'lowCR', label: `📊 Упал CR (${lowCR})`, color: '#e91e63' },
989 { id: 'lowProfit', label: `💸 Низкая прибыль (${lowProfit})`, color: '#d32f2f' },
990 { id: 'growth', label: `📈 Рост (${growth})`, color: '#27ae60' }
991 ];
992
993 filterButtons.forEach(filter => {
994 const btn = document.createElement('button');
995 btn.textContent = filter.label;
996 btn.style.cssText = `
997 padding: 8px 12px;
998 background: ${this.currentFilter === filter.id ? filter.color : '#ecf0f1'};
999 color: ${this.currentFilter === filter.id ? 'white' : '#2c3e50'};
1000 border: none;
1001 border-radius: 6px;
1002 font-size: 13px;
1003 font-weight: 500;
1004 cursor: pointer;
1005 transition: all 0.2s;
1006 `;
1007
1008 btn.addEventListener('click', () => {
1009 this.currentFilter = filter.id;
1010 this.applyFilter(filter.id);
1011 });
1012
1013 filtersContainer.appendChild(btn);
1014 });
1015
1016 return filtersContainer;
1017 }
1018
1019 applyFilter(filterId) {
1020 let filtered = this.allProducts;
1021
1022 switch(filterId) {
1023 case 'critical':
1024 filtered = this.allProducts.filter(p => p.analysis.priority === 'critical');
1025 break;
1026 case 'high':
1027 filtered = this.allProducts.filter(p => p.analysis.priority === 'high');
1028 break;
1029 case 'lowStock':
1030 filtered = this.allProducts.filter(p => p.analysis.isLowStock);
1031 break;
1032 case 'highDRR':
1033 filtered = this.allProducts.filter(p => p.analysis.isHighDRR);
1034 break;
1035 case 'lowImpressions':
1036 filtered = this.allProducts.filter(p => p.analysis.isLowImpressions);
1037 break;
1038 case 'lowCR':
1039 filtered = this.allProducts.filter(p => p.analysis.isLowCR);
1040 break;
1041 case 'lowProfit':
1042 filtered = this.allProducts.filter(p => p.analysis.isLowProfit);
1043 break;
1044 case 'growth':
1045 filtered = this.allProducts.filter(p => p.analysis.isGrowth);
1046 break;
1047 }
1048
1049 this.filteredProducts = filtered;
1050 this.displayResults(filtered);
1051 }
1052
1053 createProductsList(products) {
1054 const list = document.createElement('div');
1055 list.style.cssText = `
1056 display: flex;
1057 flex-direction: column;
1058 gap: 12px;
1059 `;
1060
1061 products.forEach(product => {
1062 const card = this.createProductCard(product);
1063 list.appendChild(card);
1064 });
1065
1066 return list;
1067 }
1068
1069 formatMetric(value, change, isPercent = false) {
1070 // Округляем проценты до десятых
1071 let displayValue = value;
1072 if (isPercent && value !== null && value !== undefined) {
1073 displayValue = parseFloat(value.toFixed(1));
1074 }
1075
1076 const valueStr = displayValue !== null && displayValue !== undefined ?
1077 (isPercent ? `${displayValue}%` : displayValue.toLocaleString()) : '—';
1078
1079 if (change === null || change === undefined) return valueStr;
1080
1081 // Округляем изменение до десятых
1082 const roundedChange = parseFloat(change.toFixed(1));
1083 const changeStr = roundedChange > 0 ? `+${roundedChange}%` : `${roundedChange}%`;
1084 const color = roundedChange > 0 ? '#27ae60' : '#e74c3c';
1085
1086 return `${valueStr} <span style="color: ${color}; font-size: 11px;">(${changeStr})</span>`;
1087 }
1088
1089 createProductCard(product) {
1090 const card = document.createElement('div');
1091
1092 const priorityColors = {
1093 critical: '#e74c3c',
1094 high: '#f39c12',
1095 medium: '#3498db',
1096 low: '#95a5a6'
1097 };
1098
1099 const priorityLabels = {
1100 critical: '🔴 Критичный',
1101 high: '🟠 Высокий',
1102 medium: '🟡 Средний',
1103 low: '🟢 Низкий'
1104 };
1105
1106 // Определяем цвет прибыли
1107 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1108
1109 card.style.cssText = `
1110 background: white;
1111 border: 2px solid ${priorityColors[product.analysis.priority]};
1112 border-radius: 8px;
1113 padding: 14px;
1114 cursor: pointer;
1115 transition: all 0.2s;
1116 `;
1117
1118 card.innerHTML = `
1119 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
1120 <div style="flex: 1;">
1121 <div style="font-weight: 600; font-size: 14px; color: #2c3e50; margin-bottom: 4px;">${product.name}</div>
1122 <div style="font-size: 12px; color: #7f8c8d;">Арт. ${product.article}</div>
1123 </div>
1124 <div style="background: ${priorityColors[product.analysis.priority]}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; white-space: nowrap;">
1125 ${priorityLabels[product.analysis.priority]}
1126 </div>
1127 </div>
1128
1129 ${product.analysis.problems.length > 0 ? `
1130 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 500; color: #856404;">
1131 ${product.analysis.problems.slice(0, 2).map(p => `
1132 <div style="margin-bottom: 4px;">⚠️ ${p.description}</div>
1133 `).join('')}
1134 </div>
1135 ` : ''}
1136
1137 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 12px;">
1138 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1139 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
1140 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'} ${product.profitPercent !== null ? `(${product.profitPercent}%)` : ''}</span></div>
1141 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1142 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1143 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
1144 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1145 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1146 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1147 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
1148 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
1149 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
1150 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
1151 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
1152 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
1153 </div>
1154 </div>
1155
1156 <div style="font-size: 11px; color: #7f8c8d; border-top: 1px solid #ecf0f1; padding-top: 8px;">
1157 <strong>Рекомендации:</strong>
1158 ${product.analysis.recommendations.slice(0, 1).map(r => `<div>• ${r}</div>`).join('')}
1159 </div>
1160 `;
1161
1162 card.addEventListener('click', () => {
1163 this.showProductDetails(product);
1164 });
1165
1166 return card;
1167 }
1168
1169 showProductDetails(product) {
1170 // Определяем цвет прибыли
1171 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1172
1173 // Создаем модальное окно с детальной информацией
1174 const modal = document.createElement('div');
1175 modal.style.cssText = `
1176 position: fixed;
1177 top: 0;
1178 left: 0;
1179 right: 0;
1180 bottom: 0;
1181 background: rgba(0,0,0,0.7);
1182 z-index: 10001;
1183 display: flex;
1184 align-items: center;
1185 justify-content: center;
1186 padding: 20px;
1187 `;
1188
1189 const modalContent = document.createElement('div');
1190 modalContent.style.cssText = `
1191 background: white;
1192 border-radius: 12px;
1193 padding: 24px;
1194 max-width: 600px;
1195 max-height: 80vh;
1196 overflow-y: auto;
1197 width: 100%;
1198 `;
1199
1200 modalContent.innerHTML = `
1201 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
1202 <h2 style="margin: 0; font-size: 18px; color: #2c3e50;">${product.name}</h2>
1203 <button id="close-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #95a5a6;">×</button>
1204 </div>
1205
1206 <div style="background: #f8f9fa; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
1207 <div style="font-size: 12px; color: #7f8c8d; margin-bottom: 8px;">Артикул: ${product.article}</div>
1208
1209 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
1210 <div>
1211 <div style="color: #7f8c8d; font-size: 11px;">Выручка</div>
1212 <div style="font-weight: 600;">${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
1213 </div>
1214
1215 <div>
1216 <div style="color: #7f8c8d; font-size: 11px;">Прибыль</div>
1217 <div style="font-weight: 600; color: ${profitColor};">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</div>
1218 </div>
1219
1220 <div>
1221 <div style="color: #7f8c8d; font-size: 11px;">Прибыль %</div>
1222 <div style="font-weight: 600; color: ${profitColor};">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</div>
1223 </div>
1224
1225 <div>
1226 <div style="color: #7f8c8d; font-size: 11px;">Заказы</div>
1227 <div style="font-weight: 600;">${this.formatMetric(product.orders, product.ordersChange)}</div>
1228 </div>
1229
1230 <div>
1231 <div style="color: #7f8c8d; font-size: 11px;">Показы всего</div>
1232 <div style="font-weight: 600;">${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1233 </div>
1234
1235 <div>
1236 <div style="color: #7f8c8d; font-size: 11px;">Посещения карточки</div>
1237 <div style="font-weight: 600;">${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1238 </div>
1239
1240 <div>
1241 <div style="color: #7f8c8d; font-size: 11px;">CTR (каталог→карточка)</div>
1242 <div style="font-weight: 600;">${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1243 </div>
1244
1245 <div>
1246 <div style="color: #7f8c8d; font-size: 11px;">CRL (карточка→корзина)</div>
1247 <div style="font-weight: 600;">${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1248 </div>
1249
1250 <div>
1251 <div style="color: #7f8c8d; font-size: 11px;">CR (заказы/карточка)</div>
1252 <div style="font-weight: 600;">${this.formatMetric(product.cr, product.crChange, true)}</div>
1253 </div>
1254
1255 <div>
1256 <div style="color: #7f8c8d; font-size: 11px;">Средняя цена</div>
1257 <div style="font-weight: 600;">${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
1258 </div>
1259
1260 <div>
1261 <div style="color: #7f8c8d; font-size: 11px;">Общая ДРР</div>
1262 <div style="font-weight: 600;">${this.formatMetric(product.drr, product.drrChange, true)}</div>
1263 </div>
1264
1265 <div>
1266 <div style="color: #7f8c8d; font-size: 11px;">Остаток на конец периода</div>
1267 <div style="font-weight: 600;">${product.stock || '—'} шт</div>
1268 </div>
1269
1270 <div>
1271 <div style="color: #7f8c8d; font-size: 11px;">Хватит на дней</div>
1272 <div style="font-weight: 600;">${product.daysOfStock || '—'}</div>
1273 </div>
1274 </div>
1275 </div>
1276
1277 <div style="margin-bottom: 16px;">
1278 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">🔍 Выявленные проблемы</h3>
1279 ${product.analysis.problems.length > 0 ? product.analysis.problems.map(p => `
1280 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1281 <strong>${p.type}:</strong> ${p.description}
1282 </div>
1283 `).join('') : '<div style="color: #27ae60; font-size: 12px;">✅ Проблем не выявлено</div>'}
1284 </div>
1285
1286 <div>
1287 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">💡 Рекомендации</h3>
1288 ${product.analysis.recommendations.map(r => `
1289 <div style="background: #d4edda; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1290 ${r}
1291 </div>
1292 `).join('')}
1293 </div>
1294
1295 <button id="filter-by-article" style="
1296 width: 100%;
1297 margin-top: 16px;
1298 padding: 12px;
1299 background: #667eea;
1300 color: white;
1301 border: none;
1302 border-radius: 8px;
1303 font-size: 14px;
1304 font-weight: 600;
1305 cursor: pointer;
1306 ">
1307 🔍 Показать только этот товар в таблице
1308 </button>
1309 `;
1310
1311 modal.appendChild(modalContent);
1312 document.body.appendChild(modal);
1313
1314 // Закрытие модального окна
1315 modal.addEventListener('click', (e) => {
1316 if (e.target === modal) {
1317 modal.remove();
1318 }
1319 });
1320
1321 modalContent.querySelector('#close-modal').addEventListener('click', () => {
1322 modal.remove();
1323 });
1324
1325 // Фильтрация по артикулу
1326 modalContent.querySelector('#filter-by-article').addEventListener('click', () => {
1327 this.filterByArticle(product.article);
1328 modal.remove();
1329 });
1330 }
1331
1332 filterByArticle(article) {
1333 console.log(`🔍 Фильтруем по артикулу: ${article}`);
1334
1335 // Находим поле фильтра по артикулу на странице
1336 const articleInput = document.querySelector('input[placeholder*="артикул"], input[name*="article"]');
1337
1338 if (articleInput) {
1339 articleInput.value = article;
1340 articleInput.dispatchEvent(new Event('input', { bubbles: true }));
1341 articleInput.dispatchEvent(new Event('change', { bubbles: true }));
1342
1343 // Ищем кнопку "Применить" - ищем все кнопки и проверяем текст
1344 const buttons = document.querySelectorAll('button[type="submit"]');
1345 let applyButton = null;
1346 for (const btn of buttons) {
1347 if (btn.textContent.includes('Применить')) {
1348 applyButton = btn;
1349 break;
1350 }
1351 }
1352
1353 if (applyButton) {
1354 setTimeout(() => applyButton.click(), 300);
1355 }
1356 } else {
1357 console.warn('Не найдено поле для ввода артикула');
1358 }
1359 }
1360 }
1361
1362 // Инициализация
1363 async function init() {
1364 console.log('🎯 Инициализация AI Аналитика Продаж...');
1365
1366 // Проверяем, что мы на странице аналитики
1367 if (!window.location.href.includes('seller.ozon.ru/app/analytics')) {
1368 console.log('⚠️ Не на странице аналитики, ожидаем...');
1369 return;
1370 }
1371
1372 // Ждем загрузки таблицы
1373 const waitForTable = setInterval(() => {
1374 const table = document.querySelector('table.ct590-a');
1375 if (table) {
1376 clearInterval(waitForTable);
1377 console.log('✅ Таблица найдена, создаем UI');
1378
1379 const ui = new AnalyticsUI();
1380 ui.createUI();
1381 }
1382 }, 1000);
1383 }
1384
1385 // Запуск при загрузке страницы
1386 if (document.readyState === 'loading') {
1387 document.addEventListener('DOMContentLoaded', init);
1388 } else {
1389 init();
1390 }
1391
1392 // Отслеживание изменений URL (для SPA)
1393 let lastUrl = location.href;
1394 new MutationObserver(() => {
1395 const url = location.href;
1396 if (url !== lastUrl) {
1397 lastUrl = url;
1398 init();
1399 }
1400 }).observe(document, { subtree: true, childList: true });
1401
1402})();